Add table of contents provider abstraction (#152504)

We currently re-compute the same table of contents for markdown files multiple times. This is because multiple language features all need table of contents

With this change, we introduce a new `TableOfContentsProvider` which maintains a cache of the table of contents per file. This provider is then passed into every caller that needs a toc
This commit is contained in:
Matt Bierner 2022-06-17 11:20:02 -07:00 committed by GitHub
parent 5947c2a93c
commit dea813ff7c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 133 additions and 86 deletions

View file

@ -26,6 +26,7 @@ import { MarkdownContentProvider } from './preview/previewContentProvider';
import { MarkdownPreviewManager } from './preview/previewManager';
import { ContentSecurityPolicyArbiter, ExtensionContentSecurityPolicyArbiter, PreviewSecuritySelector } from './preview/security';
import { githubSlugifier } from './slugify';
import { MdTableOfContentsProvider } from './tableOfContents';
import { loadDefaultTelemetryReporter, TelemetryReporter } from './telemetryReporter';
import { VsCodeMdWorkspaceContents } from './workspaceContents';
@ -64,27 +65,29 @@ function registerMarkdownLanguageFeatures(
const workspaceContents = new VsCodeMdWorkspaceContents();
const linkProvider = new MdLinkProvider(engine, workspaceContents);
const referencesProvider = new MdReferencesProvider(engine, workspaceContents);
const symbolProvider = new MdDocumentSymbolProvider(engine);
const tocProvider = new MdTableOfContentsProvider(engine, workspaceContents);
const referencesProvider = new MdReferencesProvider(engine, workspaceContents, tocProvider);
const symbolProvider = new MdDocumentSymbolProvider(tocProvider);
return vscode.Disposable.from(
workspaceContents,
linkProvider,
referencesProvider,
tocProvider,
// Language features
registerDefinitionSupport(selector, referencesProvider),
registerDiagnosticSupport(selector, engine, workspaceContents, linkProvider, commandManager, referencesProvider),
registerDiagnosticSupport(selector, engine, workspaceContents, linkProvider, commandManager, referencesProvider, tocProvider),
registerDocumentLinkSupport(selector, linkProvider),
registerDocumentSymbolSupport(selector, engine),
registerDocumentSymbolSupport(selector, tocProvider),
registerDropIntoEditorSupport(selector),
registerFindFileReferenceSupport(commandManager, referencesProvider),
registerFoldingSupport(selector, engine),
registerFoldingSupport(selector, engine, tocProvider),
registerPasteSupport(selector),
registerPathCompletionSupport(selector, engine, linkProvider),
registerReferencesSupport(selector, referencesProvider),
registerRenameSupport(selector, workspaceContents, referencesProvider, engine.slugifier),
registerSmartSelectSupport(selector, engine),
registerSmartSelectSupport(selector, engine, tocProvider),
registerWorkspaceSymbolSupport(workspaceContents, symbolProvider),
);
}

View file

@ -8,16 +8,16 @@ import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import { CommandManager } from '../commandManager';
import { MarkdownEngine } from '../markdownEngine';
import { TableOfContents } from '../tableOfContents';
import { MdTableOfContentsProvider } from '../tableOfContents';
import { MdTableOfContentsWatcher } from '../test/tableOfContentsWatcher';
import { Delayer } from '../util/async';
import { noopToken } from '../util/cancellation';
import { Disposable } from '../util/dispose';
import { isMarkdownFile } from '../util/file';
import { Limiter } from '../util/limiter';
import { ResourceMap } from '../util/resourceMap';
import { MdTableOfContentsWatcher } from '../test/tableOfContentsWatcher';
import { MdWorkspaceContents, SkinnyTextDocument } from '../workspaceContents';
import { InternalHref, MdLink, MdLinkSource, MdLinkProvider, LinkDefinitionSet } from './documentLinkProvider';
import { InternalHref, LinkDefinitionSet, MdLink, MdLinkProvider, MdLinkSource } from './documentLinkProvider';
import { MdReferencesProvider, tryFindMdDocumentForLink } from './references';
const localize = nls.loadMessageBundle();
@ -448,9 +448,9 @@ class FileLinkMap {
export class DiagnosticComputer {
constructor(
private readonly engine: MarkdownEngine,
private readonly workspaceContents: MdWorkspaceContents,
private readonly linkProvider: MdLinkProvider,
private readonly tocProvider: MdTableOfContentsProvider,
) { }
public async getDiagnostics(doc: SkinnyTextDocument, options: DiagnosticOptions, token: vscode.CancellationToken): Promise<{ readonly diagnostics: vscode.Diagnostic[]; readonly links: readonly MdLink[] }> {
@ -475,7 +475,7 @@ export class DiagnosticComputer {
return [];
}
const toc = await TableOfContents.create(this.engine, doc);
const toc = await this.tocProvider.get(doc.uri);
if (token.isCancellationRequested) {
return [];
}
@ -552,7 +552,7 @@ export class DiagnosticComputer {
// Validate each of the links to headers in the file
const fragmentLinks = links.filter(x => x.fragment);
if (fragmentLinks.length) {
const toc = await TableOfContents.create(this.engine, hrefDoc);
const toc = await this.tocProvider.get(hrefDoc.uri);
for (const link of fragmentLinks) {
if (!toc.lookup(link.fragment) && !this.isIgnoredLink(options, link.source.pathText) && !this.isIgnoredLink(options, link.source.text)) {
const msg = localize('invalidLinkToHeaderInOtherFile', 'Header does not exist in file: {0}', link.fragment);
@ -625,16 +625,17 @@ export function registerDiagnosticSupport(
workspaceContents: MdWorkspaceContents,
linkProvider: MdLinkProvider,
commandManager: CommandManager,
referenceComputer: MdReferencesProvider,
referenceProvider: MdReferencesProvider,
tocProvider: MdTableOfContentsProvider,
): vscode.Disposable {
const configuration = new VSCodeDiagnosticConfiguration();
const manager = new DiagnosticManager(
engine,
workspaceContents,
new DiagnosticComputer(engine, workspaceContents, linkProvider),
new DiagnosticComputer(workspaceContents, linkProvider, tocProvider),
configuration,
new DiagnosticCollectionReporter(),
referenceComputer);
referenceProvider);
return vscode.Disposable.from(
configuration,
manager,

View file

@ -4,8 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { MarkdownEngine } from '../markdownEngine';
import { TableOfContents, TocEntry } from '../tableOfContents';
import { MdTableOfContentsProvider, TocEntry } from '../tableOfContents';
import { SkinnyTextDocument } from '../workspaceContents';
interface MarkdownSymbol {
@ -17,16 +16,16 @@ interface MarkdownSymbol {
export class MdDocumentSymbolProvider implements vscode.DocumentSymbolProvider {
constructor(
private readonly engine: MarkdownEngine
private readonly tocProvider: MdTableOfContentsProvider,
) { }
public async provideDocumentSymbolInformation(document: SkinnyTextDocument): Promise<vscode.SymbolInformation[]> {
const toc = await TableOfContents.create(this.engine, document);
const toc = await this.tocProvider.get(document.uri);
return toc.entries.map(entry => this.toSymbolInformation(entry));
}
public async provideDocumentSymbols(document: SkinnyTextDocument): Promise<vscode.DocumentSymbol[]> {
const toc = await TableOfContents.create(this.engine, document);
const toc = await this.tocProvider.get(document.uri);
const root: MarkdownSymbol = {
level: -Infinity,
children: [],
@ -77,7 +76,7 @@ export class MdDocumentSymbolProvider implements vscode.DocumentSymbolProvider {
export function registerDocumentSymbolSupport(
selector: vscode.DocumentSelector,
engine: MarkdownEngine,
tocProvider: MdTableOfContentsProvider,
): vscode.Disposable {
return vscode.languages.registerDocumentSymbolProvider(selector, new MdDocumentSymbolProvider(engine));
return vscode.languages.registerDocumentSymbolProvider(selector, new MdDocumentSymbolProvider(tocProvider));
}

View file

@ -6,7 +6,7 @@
import Token = require('markdown-it/lib/token');
import * as vscode from 'vscode';
import { MarkdownEngine } from '../markdownEngine';
import { TableOfContents } from '../tableOfContents';
import { MdTableOfContentsProvider } from '../tableOfContents';
import { SkinnyTextDocument } from '../workspaceContents';
const rangeLimit = 5000;
@ -18,7 +18,8 @@ interface MarkdownItTokenWithMap extends Token {
export class MdFoldingProvider implements vscode.FoldingRangeProvider {
constructor(
private readonly engine: MarkdownEngine
private readonly engine: MarkdownEngine,
private readonly tocProvide: MdTableOfContentsProvider,
) { }
public async provideFoldingRanges(
@ -54,8 +55,8 @@ export class MdFoldingProvider implements vscode.FoldingRangeProvider {
.filter((region: vscode.FoldingRange | null): region is vscode.FoldingRange => !!region);
}
private async getHeaderFoldingRanges(document: SkinnyTextDocument) {
const toc = await TableOfContents.create(this.engine, document);
private async getHeaderFoldingRanges(document: SkinnyTextDocument): Promise<vscode.FoldingRange[]> {
const toc = await this.tocProvide.get(document.uri);
return toc.entries.map(entry => {
let endLine = entry.sectionLocation.range.end.line;
if (document.lineAt(endLine).isEmptyOrWhitespace && endLine >= entry.line + 1) {
@ -115,6 +116,7 @@ const isFoldableToken = (token: Token): token is MarkdownItTokenWithMap => {
export function registerFoldingSupport(
selector: vscode.DocumentSelector,
engine: MarkdownEngine,
tocProvider: MdTableOfContentsProvider,
): vscode.Disposable {
return vscode.languages.registerFoldingRangeProvider(selector, new MdFoldingProvider(engine));
return vscode.languages.registerFoldingRangeProvider(selector, new MdFoldingProvider(engine, tocProvider));
}

View file

@ -5,7 +5,7 @@
import * as vscode from 'vscode';
import * as uri from 'vscode-uri';
import { MarkdownEngine } from '../markdownEngine';
import { TableOfContents, TocEntry } from '../tableOfContents';
import { MdTableOfContentsProvider, TocEntry } from '../tableOfContents';
import { noopToken } from '../util/cancellation';
import { Disposable } from '../util/dispose';
import { MdWorkspaceContents, SkinnyTextDocument } from '../workspaceContents';
@ -69,6 +69,7 @@ export class MdReferencesProvider extends Disposable {
public constructor(
private readonly engine: MarkdownEngine,
private readonly workspaceContents: MdWorkspaceContents,
private readonly tocProvider: MdTableOfContentsProvider,
) {
super();
@ -77,7 +78,7 @@ export class MdReferencesProvider extends Disposable {
}
public async getReferencesAtPosition(document: SkinnyTextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise<MdReference[]> {
const toc = await TableOfContents.create(this.engine, document);
const toc = await this.tocProvider.get(document.uri);
if (token.isCancellationRequested) {
return [];
}
@ -184,7 +185,7 @@ export class MdReferencesProvider extends Disposable {
const references: MdReference[] = [];
if (targetDoc && sourceLink.href.fragment && sourceLink.source.fragmentRange?.contains(triggerPosition)) {
const toc = await TableOfContents.create(this.engine, targetDoc);
const toc = await this.tocProvider.get(targetDoc.uri);
const entry = toc.lookup(sourceLink.href.fragment);
if (entry) {
references.push({

View file

@ -5,7 +5,7 @@
import Token = require('markdown-it/lib/token');
import * as vscode from 'vscode';
import { MarkdownEngine } from '../markdownEngine';
import { TableOfContents, TocEntry } from '../tableOfContents';
import { MdTableOfContentsProvider, TocEntry } from '../tableOfContents';
import { SkinnyTextDocument } from '../workspaceContents';
interface MarkdownItTokenWithMap extends Token {
@ -15,7 +15,8 @@ interface MarkdownItTokenWithMap extends Token {
export class MdSmartSelect implements vscode.SelectionRangeProvider {
constructor(
private readonly engine: MarkdownEngine
private readonly engine: MarkdownEngine,
private readonly tocProvider: MdTableOfContentsProvider,
) { }
public async provideSelectionRanges(document: SkinnyTextDocument, positions: vscode.Position[], _token: vscode.CancellationToken): Promise<vscode.SelectionRange[] | undefined> {
@ -54,7 +55,7 @@ export class MdSmartSelect implements vscode.SelectionRangeProvider {
}
private async getHeaderSelectionRange(document: SkinnyTextDocument, position: vscode.Position): Promise<vscode.SelectionRange | undefined> {
const toc = await TableOfContents.create(this.engine, document);
const toc = await this.tocProvider.get(document.uri);
const headerInfo = getHeadersForPosition(toc.entries, position);
@ -253,6 +254,7 @@ function getFirstChildHeader(document: SkinnyTextDocument, header?: TocEntry, to
export function registerSmartSelectSupport(
selector: vscode.DocumentSelector,
engine: MarkdownEngine,
tocProvider: MdTableOfContentsProvider,
): vscode.Disposable {
return vscode.languages.registerSelectionRangeProvider(selector, new MdSmartSelect(engine));
return vscode.languages.registerSelectionRangeProvider(selector, new MdSmartSelect(engine, tocProvider));
}

View file

@ -4,10 +4,12 @@
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { MdDocumentInfoCache } from './languageFeatures/workspaceCache';
import { MarkdownEngine } from './markdownEngine';
import { githubSlugifier, Slug } from './slugify';
import { Disposable } from './util/dispose';
import { isMarkdownFile } from './util/file';
import { SkinnyTextDocument } from './workspaceContents';
import { MdWorkspaceContents, SkinnyTextDocument } from './workspaceContents';
export interface TocEntry {
readonly slug: Slug;
@ -161,6 +163,8 @@ export class TableOfContents {
return header.replace(/^\s*#+\s*(.*?)(\s+#+)?$/, (_, word) => word.trim());
}
public static readonly empty = new TableOfContents([]);
private constructor(
public readonly entries: readonly TocEntry[],
) { }
@ -170,3 +174,22 @@ export class TableOfContents {
return this.entries.find(entry => entry.slug.equals(slug));
}
}
export class MdTableOfContentsProvider extends Disposable {
private readonly _cache: MdDocumentInfoCache<TableOfContents>;
constructor(
engine: MarkdownEngine,
workspaceContents: MdWorkspaceContents,
) {
super();
this._cache = this._register(new MdDocumentInfoCache<TableOfContents>(workspaceContents, doc => {
return TableOfContents.create(engine, doc);
}));
}
public async get(resource: vscode.Uri): Promise<TableOfContents> {
return (await this._cache.get(resource)) ?? TableOfContents.empty;
}
}

View file

@ -8,6 +8,7 @@ import 'mocha';
import * as vscode from 'vscode';
import { MdDefinitionProvider } from '../languageFeatures/definitionProvider';
import { MdReferencesProvider } from '../languageFeatures/references';
import { MdTableOfContentsProvider } from '../tableOfContents';
import { noopToken } from '../util/cancellation';
import { InMemoryDocument } from '../util/inMemoryDocument';
import { MdWorkspaceContents } from '../workspaceContents';
@ -18,7 +19,7 @@ import { joinLines, workspacePath } from './util';
function getDefinition(doc: InMemoryDocument, pos: vscode.Position, workspace: MdWorkspaceContents) {
const engine = createNewMarkdownEngine();
const referencesProvider = new MdReferencesProvider(engine, workspace);
const referencesProvider = new MdReferencesProvider(engine, workspace, new MdTableOfContentsProvider(engine, workspace));
const provider = new MdDefinitionProvider(referencesProvider);
return provider.provideDefinition(doc, pos, noopToken);
}

View file

@ -9,6 +9,7 @@ import * as vscode from 'vscode';
import { DiagnosticCollectionReporter, DiagnosticComputer, DiagnosticConfiguration, DiagnosticLevel, DiagnosticManager, DiagnosticOptions, DiagnosticReporter } from '../languageFeatures/diagnostics';
import { MdLinkProvider } from '../languageFeatures/documentLinkProvider';
import { MdReferencesProvider } from '../languageFeatures/references';
import { MdTableOfContentsProvider } from '../tableOfContents';
import { noopToken } from '../util/cancellation';
import { disposeAll } from '../util/dispose';
import { InMemoryDocument } from '../util/inMemoryDocument';
@ -30,7 +31,8 @@ const defaultDiagnosticsOptions = Object.freeze<DiagnosticOptions>({
async function getComputedDiagnostics(doc: InMemoryDocument, workspace: MdWorkspaceContents, options: Partial<DiagnosticOptions> = {}): Promise<vscode.Diagnostic[]> {
const engine = createNewMarkdownEngine();
const linkProvider = new MdLinkProvider(engine, workspace);
const computer = new DiagnosticComputer(engine, workspace, linkProvider);
const tocProvider = new MdTableOfContentsProvider(engine, workspace);
const computer = new DiagnosticComputer(workspace, linkProvider, tocProvider);
return (
await computer.getDiagnostics(doc, { ...defaultDiagnosticsOptions, ...options, }, noopToken)
).diagnostics;
@ -430,11 +432,12 @@ suite('Markdown: Diagnostics manager', () => {
) {
const engine = createNewMarkdownEngine();
const linkProvider = new MdLinkProvider(engine, workspace);
const referencesProvider = new MdReferencesProvider(engine, workspace);
const tocProvider = new MdTableOfContentsProvider(engine, workspace);
const referencesProvider = new MdReferencesProvider(engine, workspace, tocProvider);
const manager = new DiagnosticManager(
engine,
workspace,
new DiagnosticComputer(engine, workspace, linkProvider),
new DiagnosticComputer(workspace, linkProvider, tocProvider),
configuration,
reporter,
referencesProvider,

View file

@ -6,14 +6,18 @@
import * as assert from 'assert';
import 'mocha';
import { MdDocumentSymbolProvider } from '../languageFeatures/documentSymbolProvider';
import { createNewMarkdownEngine } from './engine';
import { MdTableOfContentsProvider } from '../tableOfContents';
import { InMemoryDocument } from '../util/inMemoryDocument';
import { createNewMarkdownEngine } from './engine';
import { InMemoryWorkspaceMarkdownDocuments } from './inMemoryWorkspace';
import { workspacePath } from './util';
function getSymbolsForFile(fileContents: string) {
const doc = new InMemoryDocument(workspacePath('test.md'), fileContents);
const provider = new MdDocumentSymbolProvider(createNewMarkdownEngine());
const workspace = new InMemoryWorkspaceMarkdownDocuments([doc]);
const engine = createNewMarkdownEngine();
const provider = new MdDocumentSymbolProvider(new MdTableOfContentsProvider(engine, workspace));
return provider.provideDocumentSymbols(doc);
}

View file

@ -7,6 +7,7 @@ import * as assert from 'assert';
import 'mocha';
import * as vscode from 'vscode';
import { MdReference, MdReferencesProvider } from '../languageFeatures/references';
import { MdTableOfContentsProvider } from '../tableOfContents';
import { noopToken } from '../util/cancellation';
import { InMemoryDocument } from '../util/inMemoryDocument';
import { MdWorkspaceContents } from '../workspaceContents';
@ -15,9 +16,9 @@ import { InMemoryWorkspaceMarkdownDocuments } from './inMemoryWorkspace';
import { joinLines, workspacePath } from './util';
function getFileReferences(resource: vscode.Uri, workspaceContents: MdWorkspaceContents) {
function getFileReferences(resource: vscode.Uri, workspace: MdWorkspaceContents) {
const engine = createNewMarkdownEngine();
const computer = new MdReferencesProvider(engine, workspaceContents);
const computer = new MdReferencesProvider(engine, workspace, new MdTableOfContentsProvider(engine, workspace));
return computer.getAllReferencesToFile(resource, noopToken);
}

View file

@ -7,8 +7,10 @@ import * as assert from 'assert';
import 'mocha';
import * as vscode from 'vscode';
import { MdFoldingProvider } from '../languageFeatures/foldingProvider';
import { MdTableOfContentsProvider } from '../tableOfContents';
import { InMemoryDocument } from '../util/inMemoryDocument';
import { createNewMarkdownEngine } from './engine';
import { InMemoryWorkspaceMarkdownDocuments } from './inMemoryWorkspace';
import { joinLines } from './util';
const testFileName = vscode.Uri.file('test.md');
@ -218,6 +220,8 @@ suite('markdown.FoldingProvider', () => {
async function getFoldsForDocument(contents: string) {
const doc = new InMemoryDocument(testFileName, contents);
const provider = new MdFoldingProvider(createNewMarkdownEngine());
const workspace = new InMemoryWorkspaceMarkdownDocuments([doc]);
const engine = createNewMarkdownEngine();
const provider = new MdFoldingProvider(engine, new MdTableOfContentsProvider(engine, workspace));
return await provider.provideFoldingRanges(doc, {}, new vscode.CancellationTokenSource().token);
}

View file

@ -7,6 +7,7 @@ import * as assert from 'assert';
import 'mocha';
import * as vscode from 'vscode';
import { MdReferencesProvider, MdVsCodeReferencesProvider } from '../languageFeatures/references';
import { MdTableOfContentsProvider } from '../tableOfContents';
import { noopToken } from '../util/cancellation';
import { InMemoryDocument } from '../util/inMemoryDocument';
import { MdWorkspaceContents } from '../workspaceContents';
@ -15,9 +16,9 @@ import { InMemoryWorkspaceMarkdownDocuments } from './inMemoryWorkspace';
import { joinLines, workspacePath } from './util';
function getReferences(doc: InMemoryDocument, pos: vscode.Position, workspaceContents: MdWorkspaceContents) {
function getReferences(doc: InMemoryDocument, pos: vscode.Position, workspace: MdWorkspaceContents) {
const engine = createNewMarkdownEngine();
const computer = new MdReferencesProvider(engine, workspaceContents);
const computer = new MdReferencesProvider(engine, workspace, new MdTableOfContentsProvider(engine, workspace));
const provider = new MdVsCodeReferencesProvider(computer);
return provider.provideReferences(doc, pos, { includeDeclaration: true }, noopToken);
}

View file

@ -9,6 +9,7 @@ import * as vscode from 'vscode';
import { MdReferencesProvider } from '../languageFeatures/references';
import { MdVsCodeRenameProvider, MdWorkspaceEdit } from '../languageFeatures/rename';
import { githubSlugifier } from '../slugify';
import { MdTableOfContentsProvider } from '../tableOfContents';
import { noopToken } from '../util/cancellation';
import { InMemoryDocument } from '../util/inMemoryDocument';
import { MdWorkspaceContents } from '../workspaceContents';
@ -22,7 +23,7 @@ import { assertRangeEqual, joinLines, workspacePath } from './util';
*/
function prepareRename(doc: InMemoryDocument, pos: vscode.Position, workspace: MdWorkspaceContents): Promise<undefined | { readonly range: vscode.Range; readonly placeholder: string }> {
const engine = createNewMarkdownEngine();
const referenceComputer = new MdReferencesProvider(engine, workspace);
const referenceComputer = new MdReferencesProvider(engine, workspace, new MdTableOfContentsProvider(engine, workspace));
const renameProvider = new MdVsCodeRenameProvider(workspace, referenceComputer, githubSlugifier);
return renameProvider.prepareRename(doc, pos, noopToken);
}
@ -32,7 +33,7 @@ function prepareRename(doc: InMemoryDocument, pos: vscode.Position, workspace: M
*/
function getRenameEdits(doc: InMemoryDocument, pos: vscode.Position, newName: string, workspace: MdWorkspaceContents): Promise<MdWorkspaceEdit | undefined> {
const engine = createNewMarkdownEngine();
const referencesProvider = new MdReferencesProvider(engine, workspace);
const referencesProvider = new MdReferencesProvider(engine, workspace, new MdTableOfContentsProvider(engine, workspace));
const renameProvider = new MdVsCodeRenameProvider(workspace, referencesProvider, githubSlugifier);
return renameProvider.provideRenameEditsImpl(doc, pos, newName, noopToken);
}

View file

@ -9,6 +9,8 @@ import { MdSmartSelect } from '../languageFeatures/smartSelect';
import { createNewMarkdownEngine } from './engine';
import { InMemoryDocument } from '../util/inMemoryDocument';
import { CURSOR, getCursorPositions, joinLines } from './util';
import { MdTableOfContentsProvider } from '../tableOfContents';
import { InMemoryWorkspaceMarkdownDocuments } from './inMemoryWorkspace';
const testFileName = vscode.Uri.file('test.md');
@ -720,7 +722,9 @@ function assertLineNumbersEqual(selectionRange: vscode.SelectionRange, startLine
function getSelectionRangesForDocument(contents: string, pos?: vscode.Position[]): Promise<vscode.SelectionRange[] | undefined> {
const doc = new InMemoryDocument(testFileName, contents);
const provider = new MdSmartSelect(createNewMarkdownEngine());
const workspace = new InMemoryWorkspaceMarkdownDocuments([doc]);
const engine = createNewMarkdownEngine();
const provider = new MdSmartSelect(engine, new MdTableOfContentsProvider(engine, workspace));
const positions = pos ? pos : getCursorPositions(contents, doc);
return provider.provideSelectionRanges(doc, positions, new vscode.CancellationTokenSource().token);
}

View file

@ -8,29 +8,31 @@ import 'mocha';
import * as vscode from 'vscode';
import { MdDocumentSymbolProvider } from '../languageFeatures/documentSymbolProvider';
import { MdWorkspaceSymbolProvider } from '../languageFeatures/workspaceSymbolProvider';
import { SkinnyTextDocument } from '../workspaceContents';
import { createNewMarkdownEngine } from './engine';
import { MdTableOfContentsProvider } from '../tableOfContents';
import { InMemoryDocument } from '../util/inMemoryDocument';
import { MdWorkspaceContents, SkinnyTextDocument } from '../workspaceContents';
import { createNewMarkdownEngine } from './engine';
import { InMemoryWorkspaceMarkdownDocuments } from './inMemoryWorkspace';
import { workspacePath } from './util';
const symbolProvider = new MdDocumentSymbolProvider(createNewMarkdownEngine());
function getWorkspaceSymbols(workspace: MdWorkspaceContents, query = ''): Promise<vscode.SymbolInformation[]> {
const engine = createNewMarkdownEngine();
const symbolProvider = new MdDocumentSymbolProvider(new MdTableOfContentsProvider(engine, workspace));
return new MdWorkspaceSymbolProvider(symbolProvider, workspace).provideWorkspaceSymbols(query);
}
suite('markdown.WorkspaceSymbolProvider', () => {
test('Should not return anything for empty workspace', async () => {
const provider = new MdWorkspaceSymbolProvider(symbolProvider, new InMemoryWorkspaceMarkdownDocuments([]));
assert.deepStrictEqual(await provider.provideWorkspaceSymbols(''), []);
const workspace = new InMemoryWorkspaceMarkdownDocuments([]);
assert.deepStrictEqual(await getWorkspaceSymbols(workspace, ''), []);
});
test('Should return symbols from workspace with one markdown file', async () => {
const testFileName = vscode.Uri.file('test.md');
const workspace = new InMemoryWorkspaceMarkdownDocuments([
new InMemoryDocument(workspacePath('test.md'), `# header1\nabc\n## header2`)
]);
const provider = new MdWorkspaceSymbolProvider(symbolProvider, new InMemoryWorkspaceMarkdownDocuments([
new InMemoryDocument(testFileName, `# header1\nabc\n## header2`)
]));
const symbols = await provider.provideWorkspaceSymbols('');
const symbols = await getWorkspaceSymbols(workspace, '');
assert.strictEqual(symbols.length, 2);
assert.strictEqual(symbols[0].name, '# header1');
assert.strictEqual(symbols[1].name, '## header2');
@ -40,64 +42,59 @@ suite('markdown.WorkspaceSymbolProvider', () => {
const fileNameCount = 10;
const files: SkinnyTextDocument[] = [];
for (let i = 0; i < fileNameCount; ++i) {
const testFileName = vscode.Uri.file(`test${i}.md`);
const testFileName = workspacePath(`test${i}.md`);
files.push(new InMemoryDocument(testFileName, `# common\nabc\n## header${i}`));
}
const provider = new MdWorkspaceSymbolProvider(symbolProvider, new InMemoryWorkspaceMarkdownDocuments(files));
const workspace = new InMemoryWorkspaceMarkdownDocuments(files);
const symbols = await provider.provideWorkspaceSymbols('');
const symbols = await getWorkspaceSymbols(workspace, '');
assert.strictEqual(symbols.length, fileNameCount * 2);
});
test('Should update results when markdown file changes symbols', async () => {
const testFileName = vscode.Uri.file('test.md');
const workspaceFileProvider = new InMemoryWorkspaceMarkdownDocuments([
const testFileName = workspacePath('test.md');
const workspace = new InMemoryWorkspaceMarkdownDocuments([
new InMemoryDocument(testFileName, `# header1`, 1 /* version */)
]);
const provider = new MdWorkspaceSymbolProvider(symbolProvider, workspaceFileProvider);
assert.strictEqual((await provider.provideWorkspaceSymbols('')).length, 1);
assert.strictEqual((await getWorkspaceSymbols(workspace, '')).length, 1);
// Update file
workspaceFileProvider.updateDocument(new InMemoryDocument(testFileName, `# new header\nabc\n## header2`, 2 /* version */));
const newSymbols = await provider.provideWorkspaceSymbols('');
workspace.updateDocument(new InMemoryDocument(testFileName, `# new header\nabc\n## header2`, 2 /* version */));
const newSymbols = await getWorkspaceSymbols(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', async () => {
const testFileName = vscode.Uri.file('test.md');
const testFileName = workspacePath('test.md');
const workspaceFileProvider = new InMemoryWorkspaceMarkdownDocuments([
const workspace = new InMemoryWorkspaceMarkdownDocuments([
new InMemoryDocument(testFileName, `# header1`)
]);
const provider = new MdWorkspaceSymbolProvider(symbolProvider, workspaceFileProvider);
assert.strictEqual((await provider.provideWorkspaceSymbols('')).length, 1);
assert.strictEqual((await getWorkspaceSymbols(workspace, '')).length, 1);
// delete file
workspaceFileProvider.deleteDocument(testFileName);
const newSymbols = await provider.provideWorkspaceSymbols('');
workspace.deleteDocument(testFileName);
const newSymbols = await getWorkspaceSymbols(workspace, '');
assert.strictEqual(newSymbols.length, 0);
});
test('Should update results when markdown file is created', async () => {
const testFileName = vscode.Uri.file('test.md');
const testFileName = workspacePath('test.md');
const workspaceFileProvider = new InMemoryWorkspaceMarkdownDocuments([
const workspace = new InMemoryWorkspaceMarkdownDocuments([
new InMemoryDocument(testFileName, `# header1`)
]);
const provider = new MdWorkspaceSymbolProvider(symbolProvider, workspaceFileProvider);
assert.strictEqual((await provider.provideWorkspaceSymbols('')).length, 1);
assert.strictEqual((await getWorkspaceSymbols(workspace, '')).length, 1);
// Creat file
workspaceFileProvider.createDocument(new InMemoryDocument(vscode.Uri.file('test2.md'), `# new header\nabc\n## header2`));
const newSymbols = await provider.provideWorkspaceSymbols('');
// Create file
workspace.createDocument(new InMemoryDocument(workspacePath('test2.md'), `# new header\nabc\n## header2`));
const newSymbols = await getWorkspaceSymbols(workspace, '');
assert.strictEqual(newSymbols.length, 3);
});
});