diff --git a/extensions/markdown-language-features/src/features/workspaceSymbolProvider.ts b/extensions/markdown-language-features/src/features/workspaceSymbolProvider.ts index 2b9cb13fe82..f1a69d3f5ef 100644 --- a/extensions/markdown-language-features/src/features/workspaceSymbolProvider.ts +++ b/extensions/markdown-language-features/src/features/workspaceSymbolProvider.ts @@ -10,20 +10,57 @@ import MDDocumentSymbolProvider from './documentSymbolProvider'; export interface WorkspaceMarkdownDocumentProvider { getAllMarkdownDocuments(): Thenable; + + onDidChangeMarkdownDocument: vscode.Event; } class VSCodeWorkspaceMarkdownDocumentProvider implements WorkspaceMarkdownDocumentProvider { + + private readonly _onDidChangeMarkdownDocumentEmitter = new vscode.EventEmitter(); + private _watcher: vscode.FileSystemWatcher | undefined; + private _disposables: vscode.Disposable[] = []; + + public dispose() { + this._onDidChangeMarkdownDocumentEmitter.dispose(); + + if (this._watcher) { + this._watcher.dispose(); + } + + disposeAll(this._disposables); + } + async getAllMarkdownDocuments() { const resources = await vscode.workspace.findFiles('**/*.md'); const documents = await Promise.all( resources.map(resource => vscode.workspace.openTextDocument(resource).then(x => x, () => undefined))); return documents.filter(doc => doc && isMarkdownFile(doc)) as vscode.TextDocument[]; } + + public get onDidChangeMarkdownDocument() { + this.ensureWatcher(); + return this._onDidChangeMarkdownDocumentEmitter.event; + } + + private ensureWatcher(): void { + if (this._watcher) { + return; + } + + this._watcher = vscode.workspace.createFileSystemWatcher('**/*.md'); + + this._watcher.onDidChange(async resource => { + const document = await vscode.workspace.openTextDocument(resource); + if (isMarkdownFile(document)) { + this._onDidChangeMarkdownDocumentEmitter.fire(document); + } + }, this, this._disposables); + } } export default class MarkdownWorkspaceSymbolProvider implements vscode.WorkspaceSymbolProvider { - private _symbolCache = new Map(); + private _symbolCache = new Map>(); private _symbolCachePopulated: boolean = false; private _disposables: vscode.Disposable[] = []; @@ -37,20 +74,18 @@ export default class MarkdownWorkspaceSymbolProvider implements vscode.Workspace await this.populateSymbolCache(); this._symbolCachePopulated = true; - const watcher = vscode.workspace.createFileSystemWatcher('**/*.md'); - this._disposables.push(watcher); - watcher.onDidChange(this.onDidChange, this, this._disposables); + this._workspaceMarkdownDocumentProvider.onDidChangeMarkdownDocument(this.onDidChange, this, this._disposables); } - return Array.prototype.concat.apply([], Array.from(this._symbolCache.values()) - .filter(symbols => symbols.filter(symbolInformation => symbolInformation.name.toLowerCase().indexOf(query.toLowerCase()) !== -1))); + const allSymbolsSets = await Promise.all(Array.from(this._symbolCache.values())); + const allSymbols: vscode.SymbolInformation[] = Array.prototype.concat.apply([], allSymbolsSets); + return allSymbols.filter(symbolInformation => symbolInformation.name.toLowerCase().indexOf(query.toLowerCase()) !== -1); } public async populateSymbolCache(): Promise { const markDownDocumentUris = await this._workspaceMarkdownDocumentProvider.getAllMarkdownDocuments(); for (const document of markDownDocumentUris) { - const symbols = await this.getSymbol(document); - this._symbolCache.set(document.fileName, symbols); + this._symbolCache.set(document.fileName, this.getSymbols(document)); } } @@ -58,15 +93,11 @@ export default class MarkdownWorkspaceSymbolProvider implements vscode.Workspace disposeAll(this._disposables); } - private async getSymbol(document: vscode.TextDocument): Promise { + private getSymbols(document: vscode.TextDocument): Promise { return this._symbolProvider.provideDocumentSymbols(document); } - private async onDidChange(resource: vscode.Uri) { - const document = await vscode.workspace.openTextDocument(resource); - if (isMarkdownFile(document)) { - const symbols = await this.getSymbol(document); - this._symbolCache.set(document.fileName, symbols); - } + private onDidChange(document: vscode.TextDocument) { + this._symbolCache.set(document.fileName, this.getSymbols(document)); } } \ No newline at end of file diff --git a/extensions/markdown-language-features/src/test/workspaceSymbolProvider.test.ts b/extensions/markdown-language-features/src/test/workspaceSymbolProvider.test.ts index 2aa17752b64..edd87af13cf 100644 --- a/extensions/markdown-language-features/src/test/workspaceSymbolProvider.test.ts +++ b/extensions/markdown-language-features/src/test/workspaceSymbolProvider.test.ts @@ -16,11 +16,7 @@ const symbolProvider = new MDDocumentSymbolProvider(createNewMarkdownEngine()); suite('markdown.WorkspaceSymbolProvider', () => { test('Should not return anything for empty workspace', async () => { - const provider = new MarkdownWorkspaceSymbolProvider(symbolProvider, new class implements WorkspaceMarkdownDocumentProvider { - async getAllMarkdownDocuments() { - return []; - } - }); + const provider = new MarkdownWorkspaceSymbolProvider(symbolProvider, new InMemoryWorkspaceMarkdownDocumentProvider([])); assert.deepEqual(await provider.provideWorkspaceSymbols(''), []); }); @@ -28,11 +24,9 @@ suite('markdown.WorkspaceSymbolProvider', () => { test('Should return symbols from workspace with one markdown file', async () => { const testFileName = vscode.Uri.parse('test.md'); - const provider = new MarkdownWorkspaceSymbolProvider(symbolProvider, new class implements WorkspaceMarkdownDocumentProvider { - async getAllMarkdownDocuments() { - return [new InMemoryDocument(testFileName, `# header1\nabc\n## header2`)]; - } - }); + const provider = new MarkdownWorkspaceSymbolProvider(symbolProvider, new InMemoryWorkspaceMarkdownDocumentProvider([ + new InMemoryDocument(testFileName, `# header1\nabc\n## header2`) + ])); const symbols = await provider.provideWorkspaceSymbols(''); assert.strictEqual(symbols.length, 2); @@ -48,13 +42,51 @@ suite('markdown.WorkspaceSymbolProvider', () => { files.push(new InMemoryDocument(testFileName, `# common\nabc\n## header${i}`)); } - const provider = new MarkdownWorkspaceSymbolProvider(symbolProvider, new class implements WorkspaceMarkdownDocumentProvider { - async getAllMarkdownDocuments() { - return files; - } - }); + const provider = new MarkdownWorkspaceSymbolProvider(symbolProvider, new InMemoryWorkspaceMarkdownDocumentProvider(files)); const symbols = await provider.provideWorkspaceSymbols(''); assert.strictEqual(symbols.length, fileNameCount * 2); }); + + test('Should update results when file changes symbols from workspace with one markdown file', async () => { + const testFileName = vscode.Uri.parse('test.md'); + + const workspaceFileProvider = new InMemoryWorkspaceMarkdownDocumentProvider([ + new InMemoryDocument(testFileName, `# header1`) + ]); + + const provider = new MarkdownWorkspaceSymbolProvider(symbolProvider, workspaceFileProvider); + + assert.strictEqual((await provider.provideWorkspaceSymbols('')).length, 1); + + // Update file + workspaceFileProvider.updateDocument(new InMemoryDocument(testFileName, `# new header\nabc\n## header2`)); + const newSymbols = await provider.provideWorkspaceSymbols(''); + assert.strictEqual(newSymbols.length, 2); + assert.strictEqual(newSymbols[0].name, '# new header'); + assert.strictEqual(newSymbols[1].name, '## header2'); + }); }); + + +class InMemoryWorkspaceMarkdownDocumentProvider implements WorkspaceMarkdownDocumentProvider { + private readonly _documents = new Map(); + + constructor(documents: vscode.TextDocument[]) { + for( const doc of documents) { + this._documents.set(doc.fileName, doc); + } + } + + async getAllMarkdownDocuments() { + return Array.from(this._documents.values()); + } + + private readonly _onDidChangeMarkdownDocumentEmitter = new vscode.EventEmitter(); + public onDidChangeMarkdownDocument = this._onDidChangeMarkdownDocumentEmitter.event; + + public updateDocument(document: vscode.TextDocument) { + this._documents.set(document.fileName, document); + this._onDidChangeMarkdownDocumentEmitter.fire(document); + } +}