Move md workspace symbol search to language service (#154874)

* Move md workspace symbol search to language service

Also implements more of IWorkspace for the server

* Revert extra change
This commit is contained in:
Matt Bierner
2022-07-12 07:04:25 -07:00
committed by GitHub
parent cb67591f25
commit eeb8d49317
17 changed files with 421 additions and 323 deletions

View File

@@ -8,23 +8,30 @@ import * as vscode from 'vscode';
import { BaseLanguageClient, LanguageClientOptions, RequestType } from 'vscode-languageclient';
import * as nls from 'vscode-nls';
import { IMdParser } from './markdownEngine';
import { markdownFileExtensions } from './util/file';
import { IMdWorkspace } from './workspace';
const localize = nls.loadMessageBundle();
const parseRequestType: RequestType<{ uri: string }, Token[], any> = new RequestType('markdown/parse');
const readFileRequestType: RequestType<{ uri: string }, number[], any> = new RequestType('markdown/readFile');
const findFilesRequestTypes: RequestType<{}, string[], any> = new RequestType('markdown/findFiles');
export type LanguageClientConstructor = (name: string, description: string, clientOptions: LanguageClientOptions) => BaseLanguageClient;
export async function startClient(factory: LanguageClientConstructor, workspace: IMdWorkspace, parser: IMdParser): Promise<BaseLanguageClient> {
const documentSelector = ['markdown'];
const mdFileGlob = `**/*.{${markdownFileExtensions.join(',')}}`;
const clientOptions: LanguageClientOptions = {
documentSelector,
synchronize: {
configurationSection: ['markdown']
configurationSection: ['markdown'],
fileEvents: vscode.workspace.createFileSystemWatcher(mdFileGlob),
},
initializationOptions: {}
};
@@ -43,6 +50,15 @@ export async function startClient(factory: LanguageClientConstructor, workspace:
}
});
client.onRequest(readFileRequestType, async (e): Promise<number[]> => {
const uri = vscode.Uri.parse(e.uri);
return Array.from(await vscode.workspace.fs.readFile(uri));
});
client.onRequest(findFilesRequestTypes, async (): Promise<string[]> => {
return (await vscode.workspace.findFiles(mdFileGlob, '**/node_modules/**')).map(x => x.toString());
});
await client.start();
return client;

View File

@@ -10,13 +10,11 @@ import { registerPasteSupport } from './languageFeatures/copyPaste';
import { registerDefinitionSupport } from './languageFeatures/definitions';
import { registerDiagnosticSupport } from './languageFeatures/diagnostics';
import { MdLinkProvider, registerDocumentLinkSupport } from './languageFeatures/documentLinks';
import { MdDocumentSymbolProvider } from './languageFeatures/documentSymbols';
import { registerDropIntoEditorSupport } from './languageFeatures/dropIntoEditor';
import { registerFindFileReferenceSupport } from './languageFeatures/fileReferences';
import { registerPathCompletionSupport } from './languageFeatures/pathCompletions';
import { MdReferencesProvider, registerReferencesSupport } from './languageFeatures/references';
import { registerRenameSupport } from './languageFeatures/rename';
import { registerWorkspaceSymbolSupport } from './languageFeatures/workspaceSymbols';
import { ILogger } from './logging';
import { IMdParser, MarkdownItEngine, MdParsingProvider } from './markdownEngine';
import { MarkdownContributionProvider } from './markdownExtensions';
@@ -67,7 +65,6 @@ function registerMarkdownLanguageFeatures(
const linkProvider = new MdLinkProvider(parser, workspace, logger);
const referencesProvider = new MdReferencesProvider(parser, workspace, tocProvider, logger);
const symbolProvider = new MdDocumentSymbolProvider(tocProvider, logger);
return vscode.Disposable.from(
linkProvider,
@@ -83,7 +80,6 @@ function registerMarkdownLanguageFeatures(
registerPathCompletionSupport(selector, workspace, parser, linkProvider),
registerReferencesSupport(selector, referencesProvider),
registerRenameSupport(selector, workspace, referencesProvider, parser.slugifier),
registerWorkspaceSymbolSupport(workspace, symbolProvider),
);
}

View File

@@ -1,77 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { ILogger } from '../logging';
import { MdTableOfContentsProvider, TocEntry } from '../tableOfContents';
import { ITextDocument } from '../types/textDocument';
interface MarkdownSymbol {
readonly level: number;
readonly parent: MarkdownSymbol | undefined;
readonly children: vscode.DocumentSymbol[];
}
export class MdDocumentSymbolProvider implements vscode.DocumentSymbolProvider {
constructor(
private readonly tocProvider: MdTableOfContentsProvider,
private readonly logger: ILogger,
) { }
public async provideDocumentSymbolInformation(document: ITextDocument): Promise<vscode.SymbolInformation[]> {
this.logger.verbose('DocumentSymbolProvider', `provideDocumentSymbolInformation - ${document.uri}`);
const toc = await this.tocProvider.getForDocument(document);
return toc.entries.map(entry => this.toSymbolInformation(entry));
}
public async provideDocumentSymbols(document: ITextDocument): Promise<vscode.DocumentSymbol[]> {
const toc = await this.tocProvider.getForDocument(document);
const root: MarkdownSymbol = {
level: -Infinity,
children: [],
parent: undefined
};
this.buildTree(root, toc.entries);
return root.children;
}
private buildTree(parent: MarkdownSymbol, entries: readonly TocEntry[]) {
if (!entries.length) {
return;
}
const entry = entries[0];
const symbol = this.toDocumentSymbol(entry);
symbol.children = [];
while (entry.level <= parent.level) {
parent = parent.parent!;
}
parent.children.push(symbol);
this.buildTree({ level: entry.level, children: symbol.children, parent }, entries.slice(1));
}
private toSymbolInformation(entry: TocEntry): vscode.SymbolInformation {
return new vscode.SymbolInformation(
this.getSymbolName(entry),
vscode.SymbolKind.String,
'',
entry.sectionLocation);
}
private toDocumentSymbol(entry: TocEntry) {
return new vscode.DocumentSymbol(
this.getSymbolName(entry),
'',
vscode.SymbolKind.String,
entry.sectionLocation.range,
entry.sectionLocation.range);
}
private getSymbolName(entry: TocEntry): string {
return '#'.repeat(entry.level) + ' ' + entry.text;
}
}

View File

@@ -1,36 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { Disposable } from '../util/dispose';
import { MdWorkspaceInfoCache } from '../util/workspaceCache';
import { IMdWorkspace } from '../workspace';
import { MdDocumentSymbolProvider } from './documentSymbols';
export class MdWorkspaceSymbolProvider extends Disposable implements vscode.WorkspaceSymbolProvider {
private readonly _cache: MdWorkspaceInfoCache<vscode.SymbolInformation[]>;
public constructor(
symbolProvider: MdDocumentSymbolProvider,
workspace: IMdWorkspace,
) {
super();
this._cache = this._register(new MdWorkspaceInfoCache(workspace, doc => symbolProvider.provideDocumentSymbolInformation(doc)));
}
public async provideWorkspaceSymbols(query: string): Promise<vscode.SymbolInformation[]> {
const allSymbols = (await this._cache.values()).flat();
return allSymbols.filter(symbolInformation => symbolInformation.name.toLowerCase().indexOf(query.toLowerCase()) !== -1);
}
}
export function registerWorkspaceSymbolSupport(
workspace: IMdWorkspace,
symbolProvider: MdDocumentSymbolProvider,
): vscode.Disposable {
return vscode.languages.registerWorkspaceSymbolProvider(new MdWorkspaceSymbolProvider(symbolProvider, workspace));
}

View File

@@ -1,105 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import 'mocha';
import * as vscode from 'vscode';
import { MdDocumentSymbolProvider } from '../languageFeatures/documentSymbols';
import { MdWorkspaceSymbolProvider } from '../languageFeatures/workspaceSymbols';
import { MdTableOfContentsProvider } from '../tableOfContents';
import { ITextDocument } from '../types/textDocument';
import { DisposableStore } from '../util/dispose';
import { InMemoryDocument } from '../util/inMemoryDocument';
import { IMdWorkspace } from '../workspace';
import { createNewMarkdownEngine } from './engine';
import { InMemoryMdWorkspace } from './inMemoryWorkspace';
import { nulLogger } from './nulLogging';
import { withStore, workspacePath } from './util';
function getWorkspaceSymbols(store: DisposableStore, workspace: IMdWorkspace, query = ''): Promise<vscode.SymbolInformation[]> {
const engine = createNewMarkdownEngine();
const tocProvider = store.add(new MdTableOfContentsProvider(engine, workspace, nulLogger));
const symbolProvider = new MdDocumentSymbolProvider(tocProvider, nulLogger);
const workspaceSymbolProvider = store.add(new MdWorkspaceSymbolProvider(symbolProvider, workspace));
return workspaceSymbolProvider.provideWorkspaceSymbols(query);
}
suite('markdown.WorkspaceSymbolProvider', () => {
test('Should not return anything for empty workspace', withStore(async (store) => {
const workspace = store.add(new InMemoryMdWorkspace([]));
assert.deepStrictEqual(await getWorkspaceSymbols(store, workspace, ''), []);
}));
test('Should return symbols from workspace with one markdown file', withStore(async (store) => {
const workspace = store.add(new InMemoryMdWorkspace([
new InMemoryDocument(workspacePath('test.md'), `# header1\nabc\n## header2`)
]));
const symbols = await getWorkspaceSymbols(store, workspace, '');
assert.strictEqual(symbols.length, 2);
assert.strictEqual(symbols[0].name, '# header1');
assert.strictEqual(symbols[1].name, '## header2');
}));
test('Should return all content basic workspace', withStore(async (store) => {
const fileNameCount = 10;
const files: ITextDocument[] = [];
for (let i = 0; i < fileNameCount; ++i) {
const testFileName = workspacePath(`test${i}.md`);
files.push(new InMemoryDocument(testFileName, `# common\nabc\n## header${i}`));
}
const workspace = store.add(new InMemoryMdWorkspace(files));
const symbols = await getWorkspaceSymbols(store, workspace, '');
assert.strictEqual(symbols.length, fileNameCount * 2);
}));
test('Should update results when markdown file changes symbols', withStore(async (store) => {
const testFileName = workspacePath('test.md');
const workspace = store.add(new InMemoryMdWorkspace([
new InMemoryDocument(testFileName, `# header1`, 1 /* version */)
]));
assert.strictEqual((await getWorkspaceSymbols(store, workspace, '')).length, 1);
// Update file
workspace.updateDocument(new InMemoryDocument(testFileName, `# new header\nabc\n## header2`, 2 /* version */));
const newSymbols = await getWorkspaceSymbols(store, workspace, '');
assert.strictEqual(newSymbols.length, 2);
assert.strictEqual(newSymbols[0].name, '# new header');
assert.strictEqual(newSymbols[1].name, '## header2');
}));
test('Should remove results when file is deleted', withStore(async (store) => {
const testFileName = workspacePath('test.md');
const workspace = store.add(new InMemoryMdWorkspace([
new InMemoryDocument(testFileName, `# header1`)
]));
assert.strictEqual((await getWorkspaceSymbols(store, workspace, '')).length, 1);
// delete file
workspace.deleteDocument(testFileName);
const newSymbols = await getWorkspaceSymbols(store, workspace, '');
assert.strictEqual(newSymbols.length, 0);
}));
test('Should update results when markdown file is created', withStore(async (store) => {
const testFileName = workspacePath('test.md');
const workspace = store.add(new InMemoryMdWorkspace([
new InMemoryDocument(testFileName, `# header1`)
]));
assert.strictEqual((await getWorkspaceSymbols(store, workspace, '')).length, 1);
// Create file
workspace.createDocument(new InMemoryDocument(workspacePath('test2.md'), `# new header\nabc\n## header2`));
const newSymbols = await getWorkspaceSymbols(store, workspace, '');
assert.strictEqual(newSymbols.length, 3);
}));
});

View File

@@ -6,16 +6,16 @@
import * as vscode from 'vscode';
import * as URI from 'vscode-uri';
const markdownFileExtensions = Object.freeze<string[]>([
'.md',
'.mkd',
'.mdwn',
'.mdown',
'.markdown',
'.markdn',
'.mdtxt',
'.mdtext',
'.workbook',
export const markdownFileExtensions = Object.freeze<string[]>([
'md',
'mkd',
'mdwn',
'mdown',
'markdown',
'markdn',
'mdtxt',
'mdtext',
'workbook',
]);
export function isMarkdownFile(document: vscode.TextDocument) {
@@ -23,5 +23,5 @@ export function isMarkdownFile(document: vscode.TextDocument) {
}
export function looksLikeMarkdownPath(resolvedHrefPath: vscode.Uri) {
return markdownFileExtensions.includes(URI.Utils.extname(resolvedHrefPath).toLowerCase());
return markdownFileExtensions.includes(URI.Utils.extname(resolvedHrefPath).toLowerCase().replace('.', ''));
}