diff --git a/extensions/markdown-language-features/server/package.json b/extensions/markdown-language-features/server/package.json index 75a23458d76..db8132af4ea 100644 --- a/extensions/markdown-language-features/server/package.json +++ b/extensions/markdown-language-features/server/package.json @@ -1,7 +1,7 @@ { "name": "vscode-markdown-languageserver", "description": "Markdown language server", - "version": "0.0.0-alpha-3", + "version": "0.0.0-alpha-5", "author": "Microsoft Corporation", "license": "MIT", "engines": { diff --git a/extensions/markdown-language-features/server/src/languageFeatures/diagnostics.ts b/extensions/markdown-language-features/server/src/languageFeatures/diagnostics.ts index 7caa1d45a9f..092f337f949 100644 --- a/extensions/markdown-language-features/server/src/languageFeatures/diagnostics.ts +++ b/extensions/markdown-language-features/server/src/languageFeatures/diagnostics.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Connection, FullDocumentDiagnosticReport, UnchangedDocumentDiagnosticReport } from 'vscode-languageserver'; +import { Connection, FullDocumentDiagnosticReport, TextDocuments, UnchangedDocumentDiagnosticReport } from 'vscode-languageserver'; import * as md from 'vscode-markdown-languageservice'; import { disposeAll } from 'vscode-markdown-languageservice/out/util/dispose'; import { Disposable } from 'vscode-notebook-renderer/events'; @@ -45,6 +45,7 @@ function getDiagnosticsOptions(config: ConfigurationManager): md.DiagnosticOptio export function registerValidateSupport( connection: Connection, workspace: md.IWorkspace, + documents: TextDocuments, ls: md.IMdLanguageService, config: ConfigurationManager, logger: md.ILogger, @@ -95,6 +96,10 @@ export function registerValidateSupport( connection.languages.diagnostics.refresh(); })); + subs.push(documents.onDidClose(e => { + manager.disposeDocumentResources(URI.parse(e.document.uri)); + })); + return { dispose: () => { disposeAll(subs); diff --git a/extensions/markdown-language-features/server/src/server.ts b/extensions/markdown-language-features/server/src/server.ts index d5f6b03238c..a45607af422 100644 --- a/extensions/markdown-language-features/server/src/server.ts +++ b/extensions/markdown-language-features/server/src/server.ts @@ -79,7 +79,7 @@ export async function startServer(connection: Connection, serverConfig: { }); registerCompletionsSupport(connection, documents, mdLs, configurationManager); - registerValidateSupport(connection, workspace, mdLs, configurationManager, serverConfig.logger); + registerValidateSupport(connection, workspace, documents, mdLs, configurationManager, serverConfig.logger); return { capabilities: { diff --git a/extensions/markdown-language-features/server/src/workspace.ts b/extensions/markdown-language-features/server/src/workspace.ts index 9063a64fbd8..e0988e8fe97 100644 --- a/extensions/markdown-language-features/server/src/workspace.ts +++ b/extensions/markdown-language-features/server/src/workspace.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Connection, Emitter, FileChangeType, NotebookDocuments, TextDocuments } from 'vscode-languageserver'; +import { Connection, Emitter, FileChangeType, NotebookDocuments, Position, Range, TextDocuments } from 'vscode-languageserver'; import { TextDocument } from 'vscode-languageserver-textdocument'; import * as md from 'vscode-markdown-languageservice'; import { ContainingDocumentContext, FileWatcherOptions, IFileSystemWatcher } from 'vscode-markdown-languageservice/out/workspace'; @@ -17,6 +17,66 @@ import { Schemes } from './util/schemes'; declare const TextDecoder: any; +class VsCodeDocument implements md.ITextDocument { + + private inMemoryDoc?: TextDocument; + private onDiskDoc?: TextDocument; + + readonly uri: string; + + constructor(uri: string, init: { inMemoryDoc: TextDocument }); + constructor(uri: string, init: { onDiskDoc: TextDocument }); + constructor(uri: string, init: { inMemoryDoc?: TextDocument; onDiskDoc?: TextDocument }) { + this.uri = uri; + this.inMemoryDoc = init?.inMemoryDoc; + this.onDiskDoc = init?.onDiskDoc; + } + + get version(): number { + return this.inMemoryDoc?.version ?? this.onDiskDoc?.version ?? 0; + } + + get lineCount(): number { + return this.inMemoryDoc?.lineCount ?? this.onDiskDoc?.lineCount ?? 0; + } + + getText(range?: Range): string { + if (this.inMemoryDoc) { + return this.inMemoryDoc.getText(range); + } + + if (this.onDiskDoc) { + return this.onDiskDoc.getText(range); + } + + throw new Error('Document has been closed'); + } + + positionAt(offset: number): Position { + if (this.inMemoryDoc) { + return this.inMemoryDoc.positionAt(offset); + } + + if (this.onDiskDoc) { + return this.onDiskDoc.positionAt(offset); + } + + throw new Error('Document has been closed'); + } + + isDetached(): boolean { + return !this.onDiskDoc && !this.inMemoryDoc; + } + + setInMemoryDoc(doc: TextDocument | undefined) { + this.inMemoryDoc = doc; + } + + setOnDiskDoc(doc: TextDocument | undefined) { + this.onDiskDoc = doc; + } +} + export class VsCodeClientWorkspace implements md.IWorkspaceWithWatching { private readonly _onDidCreateMarkdownDocument = new Emitter(); @@ -28,7 +88,7 @@ export class VsCodeClientWorkspace implements md.IWorkspaceWithWatching { private readonly _onDidDeleteMarkdownDocument = new Emitter(); public readonly onDidDeleteMarkdownDocument = this._onDidDeleteMarkdownDocument.event; - private readonly _documentCache = new ResourceMap(); + private readonly _documentCache = new ResourceMap(); private readonly _utf8Decoder = new TextDecoder('utf-8'); @@ -49,31 +109,68 @@ export class VsCodeClientWorkspace implements md.IWorkspaceWithWatching { private readonly logger: md.ILogger, ) { documents.onDidOpen(e => { - this._documentCache.delete(URI.parse(e.document.uri)); - if (this.isRelevantMarkdownDocument(e.document)) { - this._onDidCreateMarkdownDocument.fire(e.document); - } - }); - - documents.onDidChangeContent(e => { - if (this.isRelevantMarkdownDocument(e.document)) { - this._onDidChangeMarkdownDocument.fire(e.document); - } - }); - - documents.onDidClose(async e => { - const uri = URI.parse(e.document.uri); - this._documentCache.delete(uri); - if (!this.isRelevantMarkdownDocument(e.document)) { return; } - // When the document has closed, the file on disk may still exist. - // In this case, we want to replace the existing entry with the one from disk - const doc = await this.openMarkdownDocumentFromFs(uri); + this.logger.log(md.LogLevel.Trace, 'VsCodeClientWorkspace: TextDocument.onDidOpen', `${e.document.uri}`); + + const uri = URI.parse(e.document.uri); + const doc = this._documentCache.get(uri); + + if (doc) { + // File already existed on disk + doc.setInMemoryDoc(e.document); + + // The content visible to the language service may have changed since the in-memory doc + // may differ from the one on-disk. To be safe we always fire a change event. + this._onDidChangeMarkdownDocument.fire(doc); + } else { + // We're creating the file for the first time + const doc = new VsCodeDocument(e.document.uri, { inMemoryDoc: e.document }); + this._documentCache.set(uri, doc); + this._onDidCreateMarkdownDocument.fire(doc); + } + }); + + documents.onDidChangeContent(e => { + if (!this.isRelevantMarkdownDocument(e.document)) { + return; + } + + this.logger.log(md.LogLevel.Trace, 'VsCodeClientWorkspace: TextDocument.onDidChanceContent', `${e.document.uri}`); + + const uri = URI.parse(e.document.uri); + const entry = this._documentCache.get(uri); + if (entry) { + entry.setInMemoryDoc(e.document); + this._onDidChangeMarkdownDocument.fire(entry); + } + }); + + documents.onDidClose(async e => { + if (!this.isRelevantMarkdownDocument(e.document)) { + return; + } + + this.logger.log(md.LogLevel.Trace, 'VsCodeClientWorkspace: TextDocument.onDidClose', `${e.document.uri}`); + + const uri = URI.parse(e.document.uri); + const doc = this._documentCache.get(uri); if (!doc) { - this._onDidDeleteMarkdownDocument.fire(uri); + // Document was never opened + return; + } + + doc.setInMemoryDoc(undefined); + if (doc.isDetached()) { + // The document has been fully closed + this.doDeleteDocument(uri); + } else { + // The document still exists on disk + // To be safe, tell the service that the document has changed because the + // in-memory doc contents may be different than the disk doc contents. + this._onDidChangeMarkdownDocument.fire(doc); } }); @@ -83,23 +180,35 @@ export class VsCodeClientWorkspace implements md.IWorkspaceWithWatching { this.logger.log(md.LogLevel.Trace, 'VsCodeClientWorkspace: onDidChangeWatchedFiles', `${change.type}: ${resource}`); switch (change.type) { case FileChangeType.Changed: { - this._documentCache.delete(resource); - const document = await this.openMarkdownDocument(resource); - if (document) { - this._onDidChangeMarkdownDocument.fire(document); + const entry = this._documentCache.get(resource); + if (entry) { + // Refresh the on-disk state + const document = await this.openMarkdownDocumentFromFs(resource); + if (document) { + this._onDidChangeMarkdownDocument.fire(document); + } } break; } case FileChangeType.Created: { - const document = await this.openMarkdownDocument(resource); - if (document) { - this._onDidCreateMarkdownDocument.fire(document); + const entry = this._documentCache.get(resource); + if (entry) { + // Create or update the on-disk state + const document = await this.openMarkdownDocumentFromFs(resource); + if (document) { + this._onDidCreateMarkdownDocument.fire(document); + } } break; } case FileChangeType.Deleted: { - this._documentCache.delete(resource); - this._onDidDeleteMarkdownDocument.fire(resource); + const entry = this._documentCache.get(resource); + if (entry) { + entry.setOnDiskDoc(undefined); + if (entry.isDetached()) { + this.doDeleteDocument(resource); + } + } break; } } @@ -182,8 +291,15 @@ export class VsCodeClientWorkspace implements md.IWorkspaceWithWatching { const matchingDocument = this.documents.get(resource.toString()); if (matchingDocument) { - this._documentCache.set(resource, matchingDocument); - return matchingDocument; + let entry = this._documentCache.get(resource); + if (entry) { + entry.setInMemoryDoc(matchingDocument); + } else { + entry = new VsCodeDocument(resource.toString(), { inMemoryDoc: matchingDocument }); + this._documentCache.set(resource, entry); + } + + return entry; } return this.openMarkdownDocumentFromFs(resource); @@ -201,7 +317,9 @@ export class VsCodeClientWorkspace implements md.IWorkspaceWithWatching { // We assume that markdown is in UTF-8 const text = this._utf8Decoder.decode(bytes); - const doc = TextDocument.create(resource.toString(), 'markdown', 0, text); + const doc = new VsCodeDocument(resource.toString(), { + onDiskDoc: TextDocument.create(resource.toString(), 'markdown', 0, text) + }); this._documentCache.set(resource, doc); return doc; } catch (e) { @@ -270,4 +388,11 @@ export class VsCodeClientWorkspace implements md.IWorkspaceWithWatching { private isRelevantMarkdownDocument(doc: TextDocument) { return isMarkdownFile(doc) && URI.parse(doc.uri).scheme !== 'vscode-bulkeditpreview'; } + + private doDeleteDocument(uri: URI) { + this.logger.log(md.LogLevel.Trace, 'VsCodeClientWorkspace: deleteDocument', `${uri}`); + + this._documentCache.delete(uri); + this._onDidDeleteMarkdownDocument.fire(uri); + } } diff --git a/extensions/markdown-language-features/server/yarn.lock b/extensions/markdown-language-features/server/yarn.lock index 451427c1331..00602221b1f 100644 --- a/extensions/markdown-language-features/server/yarn.lock +++ b/extensions/markdown-language-features/server/yarn.lock @@ -3,9 +3,9 @@ "@types/node@16.x": - version "16.11.47" - resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.47.tgz#efa9e3e0f72e7aa6a138055dace7437a83d9f91c" - integrity sha512-fpP+jk2zJ4VW66+wAMFoBJlx1bxmBKx4DUFf68UHgdGCOuyUTDlLWqsaNPJh7xhNDykyJ9eIzAygilP/4WoN8g== + version "16.11.59" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.59.tgz#823f238b9063ccc3b3b7f13186f143a57926c4f6" + integrity sha512-6u+36Dj3aDzhfBVUf/mfmc92OEdzQ2kx2jcXGdigfl70E/neV21ZHE6UCz4MDzTRcVqGAM27fk+DLXvyDsn3Jw== picomatch@^2.3.1: version "2.3.1" @@ -26,9 +26,9 @@ vscode-languageserver-protocol@3.17.2: vscode-languageserver-types "3.17.2" vscode-languageserver-textdocument@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.5.tgz#838769940ece626176ec5d5a2aa2d0aa69f5095c" - integrity sha512-1ah7zyQjKBudnMiHbZmxz5bYNM9KKZYz+5VQLj+yr8l+9w3g+WAhCkUkWbhMEdC5u0ub4Ndiye/fDyS8ghIKQg== + version "1.0.7" + resolved "https://registry.yarnpkg.com/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.7.tgz#16df468d5c2606103c90554ae05f9f3d335b771b" + integrity sha512-bFJH7UQxlXT8kKeyiyu41r22jCZXG8kuuVVA33OEJn1diWOZK5n8zBSPZFHVBOu8kXZ6h0LIRhf5UnCo61J4Hg== vscode-languageserver-types@3.17.2, vscode-languageserver-types@^3.17.1: version "3.17.2" @@ -43,9 +43,9 @@ vscode-languageserver@^8.0.2: vscode-languageserver-protocol "3.17.2" vscode-markdown-languageservice@^0.1.0-alpha.3: - version "0.1.0-alpha.3" - resolved "https://registry.yarnpkg.com/vscode-markdown-languageservice/-/vscode-markdown-languageservice-0.1.0-alpha.3.tgz#530e3a793cc7145bb14d6476a2cdd8cd7342b3a2" - integrity sha512-KOlkj1jgjvVvEjaS9YvGGMVfooKwnoEz3HGpKEmPd8Q0u11jDlXR9O7ZZRDytUWNbKHUzwYlVkm2NQ01CruzJQ== + version "0.1.0-alpha.5" + resolved "https://registry.yarnpkg.com/vscode-markdown-languageservice/-/vscode-markdown-languageservice-0.1.0-alpha.5.tgz#4f3ad1259f13f1095f9a51205b0bc2e9e7f8da86" + integrity sha512-B8W9RyDo2ZO+XbLcGjAeFJaNoWU3ljmm0WePCCTubwHmGdqVUmxyHUxuSL6kEYpIrl7x6InlbxSPMb7lh8WZ1w== dependencies: picomatch "^2.3.1" vscode-languageserver-textdocument "^1.0.5" @@ -54,9 +54,9 @@ vscode-markdown-languageservice@^0.1.0-alpha.3: vscode-uri "^3.0.3" vscode-nls@^5.0.1: - version "5.1.0" - resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-5.1.0.tgz#443b301a7465d88c81c0f4e1914f9857f0dce1e4" - integrity sha512-37Ha44QrLFwR2IfSSYdOArzUvOyoWbOYTwQC+wS0NfqKjhW7s0WQ1lMy5oJXgSZy9sAiZS5ifELhbpXodeMR8w== + version "5.2.0" + resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-5.2.0.tgz#3cb6893dd9bd695244d8a024bdf746eea665cc3f" + integrity sha512-RAaHx7B14ZU04EU31pT+rKz2/zSl7xMsfIZuo8pd+KZO6PXtQmpevpq3vxvWNcrGbdmhM/rr5Uw5Mz+NBfhVng== vscode-uri@^3.0.3: version "3.0.3"