From 03ada8a3e84ab3453f1efce83f373b42196b0f85 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Thu, 15 Sep 2022 00:27:02 -0700 Subject: [PATCH] Rework how markdown server works with documents (#160948) * Rework how markdown server works with documents This rewrites how the markdown server works with documents. The goal is to better handle switching between in-memory versions of a doc (from `TextDocument`) and versions of the same doc on disk. From the markdown service's POV, there is only one type of document As part of this, I've also adopted the newest markdown language service version * Bump package-lock versions --- .../server/package.json | 2 +- .../src/languageFeatures/diagnostics.ts | 7 +- .../server/src/server.ts | 2 +- .../server/src/workspace.ts | 193 +++++++++++++++--- .../server/yarn.lock | 24 +-- 5 files changed, 179 insertions(+), 49 deletions(-) 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"