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
This commit is contained in:
Matt Bierner
2022-09-15 00:27:02 -07:00
committed by GitHub
parent cfc0119755
commit 03ada8a3e8
5 changed files with 179 additions and 49 deletions

View File

@@ -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<md.ITextDocument>();
@@ -28,7 +88,7 @@ export class VsCodeClientWorkspace implements md.IWorkspaceWithWatching {
private readonly _onDidDeleteMarkdownDocument = new Emitter<URI>();
public readonly onDidDeleteMarkdownDocument = this._onDidDeleteMarkdownDocument.event;
private readonly _documentCache = new ResourceMap<md.ITextDocument>();
private readonly _documentCache = new ResourceMap<VsCodeDocument>();
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);
}
}