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
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 421 additions and 323 deletions

View file

@ -6,26 +6,7 @@
"name": "Attach",
"type": "node",
"request": "attach",
"port": 6044,
"protocol": "inspector",
"sourceMaps": true,
"outFiles": ["${workspaceFolder}/out/**/*.js"]
},
{
"name": "Unit Tests",
"type": "node",
"request": "launch",
"program": "${workspaceFolder}/../../../node_modules/mocha/bin/_mocha",
"stopOnEntry": false,
"args": [
"--timeout",
"999999",
"--colors"
],
"cwd": "${workspaceFolder}",
"runtimeExecutable": null,
"runtimeArgs": [],
"env": {},
"port": 7675,
"sourceMaps": true,
"outFiles": ["${workspaceFolder}/out/**/*.js"]
}

View file

@ -1,9 +1,27 @@
{
"version": "0.1.0",
"version": "2.0.0",
"command": "npm",
"isShellCommand": true,
"showOutput": "silent",
"args": ["run", "watch"],
"isWatching": true,
"problemMatcher": "$tsc-watch"
"args": [
"run",
"watch"
],
"isBackground": true,
"problemMatcher": "$tsc-watch",
"tasks": [
{
"label": "npm",
"type": "shell",
"command": "npm",
"args": [
"run",
"watch"
],
"isBackground": true,
"problemMatcher": "$tsc-watch",
"group": {
"_id": "build",
"isDefault": false
}
}
]
}

View file

@ -14,7 +14,7 @@
"vscode-uri": "^3.0.3",
"vscode-languageserver-textdocument": "^1.0.5",
"vscode-languageserver-types": "^3.17.1",
"vscode-markdown-languageservice": "mjbvz/vscode-markdown-languageservice"
"vscode-markdown-languageservice": "microsoft/vscode-markdown-languageservice"
},
"devDependencies": {
"@types/node": "16.x"

View file

@ -0,0 +1,15 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { RequestType } from 'vscode-languageserver';
import * as md from 'vscode-markdown-languageservice';
declare const TextDecoder: any;
export const parseRequestType: RequestType<{ uri: string }, md.Token[], any> = new RequestType('markdown/parse');
export const readFileRequestType: RequestType<{ uri: string }, number[], any> = new RequestType('markdown/readFile');
export const findFilesRequestTypes: RequestType<{}, string[], any> = new RequestType('markdown/findFiles');

View file

@ -3,37 +3,15 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Connection, Emitter, Event, InitializeParams, InitializeResult, RequestType, TextDocuments } from 'vscode-languageserver';
import { Connection, InitializeParams, InitializeResult, TextDocuments } from 'vscode-languageserver';
import { TextDocument } from 'vscode-languageserver-textdocument';
import * as lsp from 'vscode-languageserver-types';
import * as md from 'vscode-markdown-languageservice';
import { URI } from 'vscode-uri';
import { LogFunctionLogger } from './logging';
import { parseRequestType } from './protocol';
import { VsCodeClientWorkspace } from './workspace';
const parseRequestType: RequestType<{ uri: string }, md.Token[], any> = new RequestType('markdown/parse');
class TextDocumentToITextDocumentAdapter implements md.ITextDocument {
public readonly uri: md.IUri;
public get version(): number { return this._doc.version; }
public get lineCount(): number { return this._doc.lineCount; }
constructor(
private readonly _doc: TextDocument,
) {
this.uri = URI.parse(this._doc.uri);
}
getText(range?: md.IRange | undefined): string {
return this._doc.getText(range);
}
positionAt(offset: number): md.IPosition {
return this._doc.positionAt(offset);
}
}
declare const TextDecoder: any;
export function startServer(connection: Connection) {
const documents = new TextDocuments(TextDocument);
@ -45,11 +23,11 @@ export function startServer(connection: Connection) {
documentSymbolProvider: true,
foldingRangeProvider: true,
selectionRangeProvider: true,
workspaceSymbolProvider: true,
}
};
});
const parser = new class implements md.IMdParser {
slugifier = md.githubSlugifier;
@ -58,42 +36,15 @@ export function startServer(connection: Connection) {
}
};
const workspace = new class implements md.IMdWorkspace {
private readonly _onDidChangeMarkdownDocument = new Emitter<md.ITextDocument>();
onDidChangeMarkdownDocument: Event<md.ITextDocument> = this._onDidChangeMarkdownDocument.event;
private readonly _onDidCreateMarkdownDocument = new Emitter<md.ITextDocument>();
onDidCreateMarkdownDocument: Event<md.ITextDocument> = this._onDidCreateMarkdownDocument.event;
private readonly _onDidDeleteMarkdownDocument = new Emitter<md.IUri>();
onDidDeleteMarkdownDocument: Event<md.IUri> = this._onDidDeleteMarkdownDocument.event;
async getAllMarkdownDocuments(): Promise<Iterable<md.ITextDocument>> {
return documents.all().map(doc => new TextDocumentToITextDocumentAdapter(doc));
}
hasMarkdownDocument(resource: md.IUri): boolean {
return !!documents.get(resource.toString());
}
async getOrLoadMarkdownDocument(_resource: md.IUri): Promise<md.ITextDocument | undefined> {
return undefined;
}
async pathExists(_resource: md.IUri): Promise<boolean> {
return false;
}
async readDirectory(_resource: md.IUri): Promise<[string, { isDir: boolean }][]> {
return [];
}
};
const workspace = new VsCodeClientWorkspace(connection, documents);
const logger = new LogFunctionLogger(connection.console.log.bind(connection.console));
const provider = md.createLanguageService(workspace, parser, logger);
const provider = md.createLanguageService({ workspace, parser, logger });
connection.onDocumentSymbol(async (params, token): Promise<lsp.DocumentSymbol[]> => {
try {
const document = documents.get(params.textDocument.uri);
if (document) {
return await provider.provideDocumentSymbols(new TextDocumentToITextDocumentAdapter(document), token);
return await provider.provideDocumentSymbols(document, token);
}
} catch (e) {
console.error(e.stack);
@ -105,7 +56,7 @@ export function startServer(connection: Connection) {
try {
const document = documents.get(params.textDocument.uri);
if (document) {
return await provider.provideFoldingRanges(new TextDocumentToITextDocumentAdapter(document), token);
return await provider.provideFoldingRanges(document, token);
}
} catch (e) {
console.error(e.stack);
@ -117,7 +68,7 @@ export function startServer(connection: Connection) {
try {
const document = documents.get(params.textDocument.uri);
if (document) {
return await provider.provideSelectionRanges(new TextDocumentToITextDocumentAdapter(document), params.positions, token);
return await provider.provideSelectionRanges(document, params.positions, token);
}
} catch (e) {
console.error(e.stack);
@ -125,5 +76,15 @@ export function startServer(connection: Connection) {
return [];
});
connection.onWorkspaceSymbol(async (params, token): Promise<lsp.WorkspaceSymbol[]> => {
try {
return await provider.provideWorkspaceSymbols(params.query, token);
} catch (e) {
console.error(e.stack);
}
return [];
});
connection.listen();
}

View file

@ -0,0 +1,11 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/**
* @returns New array with all falsy values removed. The original array IS NOT modified.
*/
export function coalesce<T>(array: ReadonlyArray<T | undefined | null>): T[] {
return <T[]>array.filter(e => !!e);
}

View file

@ -0,0 +1,27 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { TextDocument } from 'vscode-languageserver-textdocument';
import * as URI from 'vscode-uri';
const markdownFileExtensions = Object.freeze<string[]>([
'.md',
'.mkd',
'.mdwn',
'.mdown',
'.markdown',
'.markdn',
'.mdtxt',
'.mdtext',
'.workbook',
]);
export function looksLikeMarkdownPath(resolvedHrefPath: URI.URI) {
return markdownFileExtensions.includes(URI.Utils.extname(URI.URI.from(resolvedHrefPath)).toLowerCase());
}
export function isMarkdownDocument(document: TextDocument): boolean {
return document.languageId === 'markdown';
}

View file

@ -0,0 +1,67 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
interface ILimitedTaskFactory<T> {
factory: ITask<Promise<T>>;
c: (value: T | Promise<T>) => void;
e: (error?: unknown) => void;
}
interface ITask<T> {
(): T;
}
/**
* A helper to queue N promises and run them all with a max degree of parallelism. The helper
* ensures that at any time no more than M promises are running at the same time.
*
* Taken from 'src/vs/base/common/async.ts'
*/
export class Limiter<T> {
private _size = 0;
private runningPromises: number;
private readonly maxDegreeOfParalellism: number;
private readonly outstandingPromises: ILimitedTaskFactory<T>[];
constructor(maxDegreeOfParalellism: number) {
this.maxDegreeOfParalellism = maxDegreeOfParalellism;
this.outstandingPromises = [];
this.runningPromises = 0;
}
get size(): number {
return this._size;
}
queue(factory: ITask<Promise<T>>): Promise<T> {
this._size++;
return new Promise<T>((c, e) => {
this.outstandingPromises.push({ factory, c, e });
this.consume();
});
}
private consume(): void {
while (this.outstandingPromises.length && this.runningPromises < this.maxDegreeOfParalellism) {
const iLimitedTask = this.outstandingPromises.shift()!;
this.runningPromises++;
const promise = iLimitedTask.factory();
promise.then(iLimitedTask.c, iLimitedTask.e);
promise.then(() => this.consumed(), () => this.consumed());
}
}
private consumed(): void {
this._size--;
this.runningPromises--;
if (this.outstandingPromises.length > 0) {
this.consume();
}
}
}

View file

@ -0,0 +1,69 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { URI } from 'vscode-uri';
type ResourceToKey = (uri: URI) => string;
const defaultResourceToKey = (resource: URI): string => resource.toString();
export class ResourceMap<T> {
private readonly map = new Map<string, { readonly uri: URI; readonly value: T }>();
private readonly toKey: ResourceToKey;
constructor(toKey: ResourceToKey = defaultResourceToKey) {
this.toKey = toKey;
}
public set(uri: URI, value: T): this {
this.map.set(this.toKey(uri), { uri, value });
return this;
}
public get(resource: URI): T | undefined {
return this.map.get(this.toKey(resource))?.value;
}
public has(resource: URI): boolean {
return this.map.has(this.toKey(resource));
}
public get size(): number {
return this.map.size;
}
public clear(): void {
this.map.clear();
}
public delete(resource: URI): boolean {
return this.map.delete(this.toKey(resource));
}
public *values(): IterableIterator<T> {
for (const entry of this.map.values()) {
yield entry.value;
}
}
public *keys(): IterableIterator<URI> {
for (const entry of this.map.values()) {
yield entry.uri;
}
}
public *entries(): IterableIterator<[URI, T]> {
for (const entry of this.map.values()) {
yield [entry.uri, entry.value];
}
}
public [Symbol.iterator](): IterableIterator<[URI, T]> {
return this.entries();
}
}

View file

@ -0,0 +1,155 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Connection, Emitter, FileChangeType, TextDocuments } from 'vscode-languageserver';
import { TextDocument } from 'vscode-languageserver-textdocument';
import * as md from 'vscode-markdown-languageservice';
import { URI } from 'vscode-uri';
import * as protocol from './protocol';
import { coalesce } from './util/arrays';
import { isMarkdownDocument, looksLikeMarkdownPath } from './util/file';
import { Limiter } from './util/limiter';
import { ResourceMap } from './util/resourceMap';
declare const TextDecoder: any;
export class VsCodeClientWorkspace implements md.IWorkspace {
private readonly _onDidCreateMarkdownDocument = new Emitter<md.ITextDocument>();
public readonly onDidCreateMarkdownDocument = this._onDidCreateMarkdownDocument.event;
private readonly _onDidChangeMarkdownDocument = new Emitter<md.ITextDocument>();
public readonly onDidChangeMarkdownDocument = this._onDidChangeMarkdownDocument.event;
private readonly _onDidDeleteMarkdownDocument = new Emitter<URI>();
public readonly onDidDeleteMarkdownDocument = this._onDidDeleteMarkdownDocument.event;
private readonly _documentCache = new ResourceMap<md.ITextDocument>();
private readonly _utf8Decoder = new TextDecoder('utf-8');
constructor(
private readonly connection: Connection,
private readonly documents: TextDocuments<TextDocument>,
) {
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(e => {
this._documentCache.delete(URI.parse(e.document.uri));
});
connection.onDidChangeWatchedFiles(async ({ changes }) => {
for (const change of changes) {
const resource = URI.parse(change.uri);
switch (change.type) {
case FileChangeType.Changed: {
this._documentCache.delete(resource);
const document = await this.getOrLoadMarkdownDocument(resource);
if (document) {
this._onDidChangeMarkdownDocument.fire(document);
}
break;
}
case FileChangeType.Created: {
const document = await this.getOrLoadMarkdownDocument(resource);
if (document) {
this._onDidCreateMarkdownDocument.fire(document);
}
break;
}
case FileChangeType.Deleted: {
this._documentCache.delete(resource);
this._onDidDeleteMarkdownDocument.fire(resource);
break;
}
}
}
});
}
async getAllMarkdownDocuments(): Promise<Iterable<md.ITextDocument>> {
const maxConcurrent = 20;
const foundFiles = new ResourceMap<void>();
const limiter = new Limiter<md.ITextDocument | undefined>(maxConcurrent);
// Add files on disk
const resources = await this.connection.sendRequest(protocol.findFilesRequestTypes, {});
const onDiskResults = await Promise.all(resources.map(strResource => {
return limiter.queue(async () => {
const resource = URI.parse(strResource);
const doc = await this.getOrLoadMarkdownDocument(resource);
if (doc) {
foundFiles.set(resource);
}
return doc;
});
}));
// Add opened files (such as untitled files)
const openTextDocumentResults = await Promise.all(this.documents.all()
.filter(doc => !foundFiles.has(URI.parse(doc.uri)) && this.isRelevantMarkdownDocument(doc)));
return coalesce([...onDiskResults, ...openTextDocumentResults]);
}
hasMarkdownDocument(resource: URI): boolean {
return !!this.documents.get(resource.toString());
}
async getOrLoadMarkdownDocument(resource: URI): Promise<md.ITextDocument | undefined> {
const existing = this._documentCache.get(resource);
if (existing) {
return existing;
}
const matchingDocument = this.documents.get(resource.toString());
if (matchingDocument) {
this._documentCache.set(resource, matchingDocument);
return matchingDocument;
}
if (!looksLikeMarkdownPath(resource)) {
return undefined;
}
try {
const response = await this.connection.sendRequest(protocol.readFileRequestType, { uri: resource.toString() });
// TODO: LSP doesn't seem to handle Array buffers well
const bytes = new Uint8Array(response);
// We assume that markdown is in UTF-8
const text = this._utf8Decoder.decode(bytes);
const doc = new md.InMemoryDocument(resource, text, 0);
this._documentCache.set(resource, doc);
return doc;
} catch (e) {
return undefined;
}
}
async pathExists(_resource: URI): Promise<boolean> {
return false;
}
async readDirectory(_resource: URI): Promise<[string, { isDir: boolean }][]> {
return [];
}
private isRelevantMarkdownDocument(doc: TextDocument) {
return isMarkdownDocument(doc) && URI.parse(doc.uri).scheme !== 'vscode-bulkeditpreview';
}
}

View file

@ -42,9 +42,9 @@ vscode-languageserver@^8.0.2-next.4:
dependencies:
vscode-languageserver-protocol "3.17.2-next.6"
vscode-markdown-languageservice@mjbvz/vscode-markdown-languageservice:
version "0.0.0-alpha.1"
resolved "https://codeload.github.com/mjbvz/vscode-markdown-languageservice/tar.gz/e1a0e00bf6a99cc543da64964cc0995537647d15"
vscode-markdown-languageservice@microsoft/vscode-markdown-languageservice:
version "0.0.0-alpha.2"
resolved "https://codeload.github.com/microsoft/vscode-markdown-languageservice/tar.gz/db497ada376aae9a335519dbfb406c6a1f873446"
dependencies:
vscode-languageserver-types "^3.17.1"
vscode-uri "^3.0.3"

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('.', ''));
}