Refactor markdown language features (#152402)

(sorry for the size of this PR)

This change cleans up the markdown language features by making the following changes:

- Use `registerXSupport` public functions to register these
- Expose the slugifier the `MarkdownEngine` uses. You never want to use a different one if you already have a markdown engine
- Sort of clean up names. I'd introduced a bunch of confusing names while iterating in this space. What I'm working towards:

    - `Computer` — Stateless thing that computer data
    - `Provider` — Potentially stateful thing that provides data (which may be cached)
    - `VsCodeProvider` — The actual implementation of the various vscode language features (which should only be used by VS Code and in tests, not shared with other features)
- Introduce `MdLinkProvider` to avoid recomputing links for a given document. Also use this to hide more internals of link computation
This commit is contained in:
Matt Bierner
2022-06-17 01:25:52 -07:00
committed by GitHub
parent 54f5758f81
commit 623f55f437
24 changed files with 333 additions and 181 deletions

View File

@@ -6,7 +6,7 @@
import * as vscode from 'vscode';
import { tryGetUriListSnippet } from './dropIntoEditor';
export function registerPasteProvider(selector: vscode.DocumentSelector) {
export function registerPasteSupport(selector: vscode.DocumentSelector) {
return vscode.languages.registerDocumentPasteEditProvider(selector, new class implements vscode.DocumentPasteEditProvider {
async provideDocumentPasteEdits(

View File

@@ -3,21 +3,25 @@
* 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 { SkinnyTextDocument } from '../workspaceContents';
import { MdReferencesComputer } from './references';
import { MdReferencesProvider } from './references';
export class MdDefinitionProvider extends Disposable implements vscode.DefinitionProvider {
export class MdDefinitionProvider implements vscode.DefinitionProvider {
constructor(
private readonly referencesComputer: MdReferencesComputer
) {
super();
}
private readonly referencesProvider: MdReferencesProvider
) { }
async provideDefinition(document: SkinnyTextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise<vscode.Definition | undefined> {
const allRefs = await this.referencesComputer.getReferencesAtPosition(document, position, token);
const allRefs = await this.referencesProvider.getReferencesAtPosition(document, position, token);
return allRefs.find(ref => ref.kind === 'link' && ref.isDefinition)?.location;
}
}
export function registerDefinitionSupport(
selector: vscode.DocumentSelector,
referencesProvider: MdReferencesProvider,
): vscode.Disposable {
return vscode.languages.registerDefinitionProvider(selector, new MdDefinitionProvider(referencesProvider));
}

View File

@@ -17,8 +17,8 @@ import { Limiter } from '../util/limiter';
import { ResourceMap } from '../util/resourceMap';
import { MdTableOfContentsWatcher } from '../test/tableOfContentsWatcher';
import { MdWorkspaceContents, SkinnyTextDocument } from '../workspaceContents';
import { InternalHref, LinkDefinitionSet, MdLink, MdLinkComputer, MdLinkSource } from './documentLinkProvider';
import { MdReferencesComputer, tryFindMdDocumentForLink } from './references';
import { InternalHref, MdLink, MdLinkSource, MdLinkProvider, LinkDefinitionSet } from './documentLinkProvider';
import { MdReferencesProvider, tryFindMdDocumentForLink } from './references';
const localize = nls.loadMessageBundle();
@@ -305,7 +305,7 @@ export class DiagnosticManager extends Disposable {
private readonly computer: DiagnosticComputer,
private readonly configuration: DiagnosticConfiguration,
private readonly reporter: DiagnosticReporter,
private readonly referencesComputer: MdReferencesComputer,
private readonly referencesProvider: MdReferencesProvider,
delay = 300,
) {
super();
@@ -344,7 +344,7 @@ export class DiagnosticManager extends Disposable {
this._register(this.tableOfContentsWatcher.onTocChanged(async e => {
// When the toc of a document changes, revalidate every file that linked to it too
const triggered = new ResourceMap<void>();
for (const ref of await this.referencesComputer.getAllReferencesToFile(e.uri, noopToken)) {
for (const ref of await this.referencesProvider.getAllReferencesToFile(e.uri, noopToken)) {
const file = ref.location.uri;
if (!triggered.has(file)) {
this.triggerDiagnostics(file);
@@ -450,11 +450,11 @@ export class DiagnosticComputer {
constructor(
private readonly engine: MarkdownEngine,
private readonly workspaceContents: MdWorkspaceContents,
private readonly linkComputer: MdLinkComputer,
private readonly linkProvider: MdLinkProvider,
) { }
public async getDiagnostics(doc: SkinnyTextDocument, options: DiagnosticOptions, token: vscode.CancellationToken): Promise<{ readonly diagnostics: vscode.Diagnostic[]; readonly links: MdLink[] }> {
const links = await this.linkComputer.getAllLinks(doc, token);
public async getDiagnostics(doc: SkinnyTextDocument, options: DiagnosticOptions, token: vscode.CancellationToken): Promise<{ readonly diagnostics: vscode.Diagnostic[]; readonly links: readonly MdLink[] }> {
const { links, definitions } = await this.linkProvider.getLinks(doc);
if (token.isCancellationRequested || !options.enabled) {
return { links, diagnostics: [] };
}
@@ -463,7 +463,7 @@ export class DiagnosticComputer {
links,
diagnostics: (await Promise.all([
this.validateFileLinks(options, links, token),
Array.from(this.validateReferenceLinks(options, links)),
Array.from(this.validateReferenceLinks(options, links, definitions)),
this.validateFragmentLinks(doc, options, links, token),
])).flat()
};
@@ -501,15 +501,14 @@ export class DiagnosticComputer {
return diagnostics;
}
private *validateReferenceLinks(options: DiagnosticOptions, links: readonly MdLink[]): Iterable<vscode.Diagnostic> {
private *validateReferenceLinks(options: DiagnosticOptions, links: readonly MdLink[], definitions: LinkDefinitionSet): Iterable<vscode.Diagnostic> {
const severity = toSeverity(options.validateReferences);
if (typeof severity === 'undefined') {
return [];
}
const definitionSet = new LinkDefinitionSet(links);
for (const link of links) {
if (link.href.kind === 'reference' && !definitionSet.lookup(link.href.ref)) {
if (link.href.kind === 'reference' && !definitions.lookup(link.href.ref)) {
yield new vscode.Diagnostic(
link.source.hrefRange,
localize('invalidReferenceLink', 'No link definition found: \'{0}\'', link.href.ref),
@@ -620,19 +619,19 @@ class AddToIgnoreLinksQuickFixProvider implements vscode.CodeActionProvider {
}
}
export function register(
export function registerDiagnosticSupport(
selector: vscode.DocumentSelector,
engine: MarkdownEngine,
workspaceContents: MdWorkspaceContents,
linkComputer: MdLinkComputer,
linkProvider: MdLinkProvider,
commandManager: CommandManager,
referenceComputer: MdReferencesComputer,
referenceComputer: MdReferencesProvider,
): vscode.Disposable {
const configuration = new VSCodeDiagnosticConfiguration();
const manager = new DiagnosticManager(
engine,
workspaceContents,
new DiagnosticComputer(engine, workspaceContents, linkComputer),
new DiagnosticComputer(engine, workspaceContents, linkProvider),
configuration,
new DiagnosticCollectionReporter(),
referenceComputer);

View File

@@ -9,8 +9,11 @@ import * as uri from 'vscode-uri';
import { OpenDocumentLinkCommand } from '../commands/openDocumentLink';
import { MarkdownEngine } from '../markdownEngine';
import { coalesce } from '../util/arrays';
import { noopToken } from '../util/cancellation';
import { Disposable } from '../util/dispose';
import { getUriForLinkWithKnownExternalScheme, isOfScheme, Schemes } from '../util/schemes';
import { SkinnyTextDocument } from '../workspaceContents';
import { MdWorkspaceContents, SkinnyTextDocument } from '../workspaceContents';
import { MdDocumentInfoCache } from './workspaceCache';
const localize = nls.loadMessageBundle();
@@ -242,6 +245,9 @@ class NoLinkRanges {
}
}
/**
* Stateless object that extracts link information from markdown files.
*/
export class MdLinkComputer {
constructor(
@@ -257,7 +263,7 @@ export class MdLinkComputer {
return Array.from([
...this.getInlineLinks(document, noLinkRanges),
...this.getReferenceLinks(document, noLinkRanges),
...this.getLinkDefinitions2(document, noLinkRanges),
...this.getLinkDefinitions(document, noLinkRanges),
...this.getAutoLinks(document, noLinkRanges),
]);
}
@@ -369,12 +375,7 @@ export class MdLinkComputer {
}
}
public async getLinkDefinitions(document: SkinnyTextDocument): Promise<Iterable<MdLinkDefinition>> {
const noLinkRanges = await NoLinkRanges.compute(document, this.engine);
return this.getLinkDefinitions2(document, noLinkRanges);
}
private *getLinkDefinitions2(document: SkinnyTextDocument, noLinkRanges: NoLinkRanges): Iterable<MdLinkDefinition> {
private *getLinkDefinitions(document: SkinnyTextDocument, noLinkRanges: NoLinkRanges): Iterable<MdLinkDefinition> {
const text = document.getText();
for (const match of text.matchAll(definitionPattern)) {
const pre = match[1];
@@ -419,7 +420,37 @@ export class MdLinkComputer {
}
}
export class LinkDefinitionSet {
/**
* Stateful object which provides links for markdown files the workspace.
*/
export class MdLinkProvider extends Disposable {
private readonly _linkCache: MdDocumentInfoCache<readonly MdLink[]>;
private readonly linkComputer: MdLinkComputer;
constructor(
engine: MarkdownEngine,
workspaceContents: MdWorkspaceContents,
) {
super();
this.linkComputer = new MdLinkComputer(engine);
this._linkCache = this._register(new MdDocumentInfoCache(workspaceContents, doc => this.linkComputer.getAllLinks(doc, noopToken)));
}
public async getLinks(document: SkinnyTextDocument): Promise<{
readonly links: readonly MdLink[];
readonly definitions: LinkDefinitionSet;
}> {
const links = (await this._linkCache.get(document.uri)) ?? [];
return {
links,
definitions: new LinkDefinitionSet(links),
};
}
}
export class LinkDefinitionSet implements Iterable<[string, MdLinkDefinition]> {
private readonly _map = new Map<string, MdLinkDefinition>();
constructor(links: Iterable<MdLink>) {
@@ -430,29 +461,31 @@ export class LinkDefinitionSet {
}
}
public [Symbol.iterator](): Iterator<[string, MdLinkDefinition]> {
return this._map.entries();
}
public lookup(ref: string): MdLinkDefinition | undefined {
return this._map.get(ref);
}
}
export class MdLinkProvider implements vscode.DocumentLinkProvider {
export class MdVsCodeLinkProvider implements vscode.DocumentLinkProvider {
constructor(
private readonly _linkComputer: MdLinkComputer,
private readonly _linkProvider: MdLinkProvider,
) { }
public async provideDocumentLinks(
document: SkinnyTextDocument,
token: vscode.CancellationToken
): Promise<vscode.DocumentLink[]> {
const allLinks = (await this._linkComputer.getAllLinks(document, token)) ?? [];
const { links, definitions } = await this._linkProvider.getLinks(document);
if (token.isCancellationRequested) {
return [];
}
const definitionSet = new LinkDefinitionSet(allLinks);
return coalesce(allLinks
.map(data => this.toValidDocumentLink(data, definitionSet)));
return coalesce(links.map(data => this.toValidDocumentLink(data, definitions)));
}
private toValidDocumentLink(link: MdLink, definitionSet: LinkDefinitionSet): vscode.DocumentLink | undefined {
@@ -482,9 +515,9 @@ export class MdLinkProvider implements vscode.DocumentLinkProvider {
}
}
export function registerDocumentLinkProvider(
export function registerDocumentLinkSupport(
selector: vscode.DocumentSelector,
linkComputer: MdLinkComputer,
linkProvider: MdLinkProvider,
): vscode.Disposable {
return vscode.languages.registerDocumentLinkProvider(selector, new MdLinkProvider(linkComputer));
return vscode.languages.registerDocumentLinkProvider(selector, new MdVsCodeLinkProvider(linkProvider));
}

View File

@@ -74,3 +74,10 @@ export class MdDocumentSymbolProvider implements vscode.DocumentSymbolProvider {
return '#'.repeat(entry.level) + ' ' + entry.text;
}
}
export function registerDocumentSymbolSupport(
selector: vscode.DocumentSelector,
engine: MarkdownEngine,
): vscode.Disposable {
return vscode.languages.registerDocumentSymbolProvider(selector, new MdDocumentSymbolProvider(engine));
}

View File

@@ -23,7 +23,7 @@ const imageFileExtensions = new Set<string>([
'.webp',
]);
export function registerDropIntoEditor(selector: vscode.DocumentSelector) {
export function registerDropIntoEditorSupport(selector: vscode.DocumentSelector) {
return vscode.languages.registerDocumentOnDropEditProvider(selector, new class implements vscode.DocumentOnDropEditProvider {
async provideDocumentOnDropEdits(document: vscode.TextDocument, _position: vscode.Position, dataTransfer: vscode.DataTransfer, token: vscode.CancellationToken): Promise<vscode.DocumentDropEdit | undefined> {
const enabled = vscode.workspace.getConfiguration('markdown', document).get('editor.drop.enabled', true);

View File

@@ -6,7 +6,7 @@
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import { Command, CommandManager } from '../commandManager';
import { MdReferencesComputer } from './references';
import { MdReferencesProvider } from './references';
const localize = nls.loadMessageBundle();
@@ -16,7 +16,7 @@ export class FindFileReferencesCommand implements Command {
public readonly id = 'markdown.findAllFileReferences';
constructor(
private readonly referencesComputer: MdReferencesComputer,
private readonly referencesProvider: MdReferencesProvider,
) { }
public async execute(resource?: vscode.Uri) {
@@ -33,7 +33,7 @@ export class FindFileReferencesCommand implements Command {
location: vscode.ProgressLocation.Window,
title: localize('progress.title', "Finding file references")
}, async (_progress, token) => {
const references = await this.referencesComputer.getAllReferencesToFile(resource!, token);
const references = await this.referencesProvider.getAllReferencesToFile(resource!, token);
const locations = references.map(ref => ref.location);
const config = vscode.workspace.getConfiguration('references');
@@ -49,6 +49,9 @@ export class FindFileReferencesCommand implements Command {
}
}
export function registerFindFileReferences(commandManager: CommandManager, referencesProvider: MdReferencesComputer): vscode.Disposable {
export function registerFindFileReferenceSupport(
commandManager: CommandManager,
referencesProvider: MdReferencesProvider
): vscode.Disposable {
return commandManager.register(new FindFileReferencesCommand(referencesProvider));
}

View File

@@ -111,3 +111,10 @@ const isFoldableToken = (token: Token): token is MarkdownItTokenWithMap => {
return false;
}
};
export function registerFoldingSupport(
selector: vscode.DocumentSelector,
engine: MarkdownEngine,
): vscode.Disposable {
return vscode.languages.registerFoldingRangeProvider(selector, new MdFoldingProvider(engine));
}

View File

@@ -9,7 +9,7 @@ import { MarkdownEngine } from '../markdownEngine';
import { TableOfContents } from '../tableOfContents';
import { resolveUriToMarkdownFile } from '../util/openDocumentLink';
import { SkinnyTextDocument } from '../workspaceContents';
import { MdLinkComputer } from './documentLinkProvider';
import { MdLinkProvider } from './documentLinkProvider';
enum CompletionContextKind {
/** `[...](|)` */
@@ -76,19 +76,14 @@ function tryDecodeUriComponent(str: string): string {
}
}
export class MdPathCompletionProvider implements vscode.CompletionItemProvider {
public static register(
selector: vscode.DocumentSelector,
engine: MarkdownEngine,
linkComputer: MdLinkComputer,
): vscode.Disposable {
return vscode.languages.registerCompletionItemProvider(selector, new MdPathCompletionProvider(engine, linkComputer), '.', '/', '#');
}
/**
* Adds path completions in markdown files by implementing {@link vscode.CompletionItemProvider}.
*/
export class MdVsCodePathCompletionProvider implements vscode.CompletionItemProvider {
constructor(
private readonly engine: MarkdownEngine,
private readonly linkComputer: MdLinkComputer,
private readonly linkProvider: MdLinkProvider,
) { }
public async provideCompletionItems(document: SkinnyTextDocument, position: vscode.Position, _token: vscode.CancellationToken, _context: vscode.CompletionContext): Promise<vscode.CompletionItem[]> {
@@ -240,8 +235,8 @@ export class MdPathCompletionProvider implements vscode.CompletionItemProvider {
const insertionRange = new vscode.Range(context.linkTextStartPosition, position);
const replacementRange = new vscode.Range(insertionRange.start, position.translate({ characterDelta: context.linkSuffix.length }));
const definitions = await this.linkComputer.getLinkDefinitions(document);
for (const def of definitions) {
const { definitions } = await this.linkProvider.getLinks(document);
for (const [_, def] of definitions) {
yield {
kind: vscode.CompletionItemKind.Reference,
label: def.ref.text,
@@ -351,3 +346,11 @@ export class MdPathCompletionProvider implements vscode.CompletionItemProvider {
return document.uri;
}
}
export function registerPathCompletionSupport(
selector: vscode.DocumentSelector,
engine: MarkdownEngine,
linkProvider: MdLinkProvider,
): vscode.Disposable {
return vscode.languages.registerCompletionItemProvider(selector, new MdVsCodePathCompletionProvider(engine, linkProvider), '.', '/', '#');
}

View File

@@ -5,13 +5,12 @@
import * as vscode from 'vscode';
import * as uri from 'vscode-uri';
import { MarkdownEngine } from '../markdownEngine';
import { Slugifier } from '../slugify';
import { TableOfContents, TocEntry } from '../tableOfContents';
import { noopToken } from '../util/cancellation';
import { Disposable } from '../util/dispose';
import { MdWorkspaceContents, SkinnyTextDocument } from '../workspaceContents';
import { InternalHref, MdLink, MdLinkComputer } from './documentLinkProvider';
import { MdWorkspaceCache } from './workspaceCache';
import { MdWorkspaceInfoCache } from './workspaceCache';
/**
@@ -59,19 +58,22 @@ export interface MdHeaderReference {
export type MdReference = MdLinkReference | MdHeaderReference;
export class MdReferencesComputer extends Disposable {
/**
* Stateful object that computes references for markdown files.
*/
export class MdReferencesProvider extends Disposable {
private readonly _linkCache: MdWorkspaceCache<readonly MdLink[]>;
private readonly _linkCache: MdWorkspaceInfoCache<readonly MdLink[]>;
private readonly _linkComputer: MdLinkComputer;
public constructor(
private readonly linkComputer: MdLinkComputer,
private readonly workspaceContents: MdWorkspaceContents,
private readonly engine: MarkdownEngine,
private readonly slugifier: Slugifier,
private readonly workspaceContents: MdWorkspaceContents,
) {
super();
this._linkCache = this._register(new MdWorkspaceCache(workspaceContents, doc => linkComputer.getAllLinks(doc, noopToken)));
this._linkComputer = new MdLinkComputer(engine);
this._linkCache = this._register(new MdWorkspaceInfoCache(workspaceContents, doc => this._linkComputer.getAllLinks(doc, noopToken)));
}
public async getReferencesAtPosition(document: SkinnyTextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise<MdReference[]> {
@@ -110,7 +112,7 @@ export class MdReferencesComputer extends Disposable {
for (const link of links) {
if (link.href.kind === 'internal'
&& this.looksLikeLinkToDoc(link.href, document.uri)
&& this.slugifier.fromHeading(link.href.fragment).value === header.slug.value
&& this.engine.slugifier.fromHeading(link.href.fragment).value === header.slug.value
) {
references.push({
kind: 'link',
@@ -126,7 +128,7 @@ export class MdReferencesComputer extends Disposable {
}
private async getReferencesToLinkAtPosition(document: SkinnyTextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise<MdReference[]> {
const docLinks = await this.linkComputer.getAllLinks(document, token);
const docLinks = await this._linkComputer.getAllLinks(document, token);
for (const link of docLinks) {
if (link.kind === 'definition') {
@@ -200,7 +202,7 @@ export class MdReferencesComputer extends Disposable {
continue;
}
if (this.slugifier.fromHeading(link.href.fragment).equals(this.slugifier.fromHeading(sourceLink.href.fragment))) {
if (this.engine.slugifier.fromHeading(link.href.fragment).equals(this.engine.slugifier.fromHeading(sourceLink.href.fragment))) {
const isTriggerLocation = sourceLink.source.resource.fsPath === link.source.resource.fsPath && sourceLink.source.hrefRange.isEqual(link.source.hrefRange);
references.push({
kind: 'link',
@@ -284,27 +286,27 @@ export class MdReferencesComputer extends Disposable {
}
/**
*
* Implements {@link vscode.ReferenceProvider} for markdown documents.
*/
export class MdVsCodeReferencesProvider implements vscode.ReferenceProvider {
public constructor(
private readonly referencesComputer: MdReferencesComputer
private readonly referencesProvider: MdReferencesProvider
) { }
async provideReferences(document: SkinnyTextDocument, position: vscode.Position, context: vscode.ReferenceContext, token: vscode.CancellationToken): Promise<vscode.Location[]> {
const allRefs = await this.referencesComputer.getReferencesAtPosition(document, position, token);
const allRefs = await this.referencesProvider.getReferencesAtPosition(document, position, token);
return allRefs
.filter(ref => context.includeDeclaration || !ref.isDefinition)
.map(ref => ref.location);
}
}
export function registerReferencesProvider(
export function registerReferencesSupport(
selector: vscode.DocumentSelector,
computer: MdReferencesComputer,
referencesProvider: MdReferencesProvider,
): vscode.Disposable {
return vscode.languages.registerReferenceProvider(selector, new MdVsCodeReferencesProvider(computer));
return vscode.languages.registerReferenceProvider(selector, new MdVsCodeReferencesProvider(referencesProvider));
}
export async function tryFindMdDocumentForLink(href: InternalHref, workspaceContents: MdWorkspaceContents): Promise<SkinnyTextDocument | undefined> {

View File

@@ -11,7 +11,7 @@ import { Disposable } from '../util/dispose';
import { resolveDocumentLink } from '../util/openDocumentLink';
import { MdWorkspaceContents, SkinnyTextDocument } from '../workspaceContents';
import { InternalHref } from './documentLinkProvider';
import { MdHeaderReference, MdLinkReference, MdReference, MdReferencesComputer, tryFindMdDocumentForLink } from './references';
import { MdHeaderReference, MdLinkReference, MdReference, MdReferencesProvider, tryFindMdDocumentForLink } from './references';
const localize = nls.loadMessageBundle();
@@ -45,7 +45,7 @@ function tryDecodeUri(str: string): string {
}
}
export class MdRenameProvider extends Disposable implements vscode.RenameProvider {
export class MdVsCodeRenameProvider extends Disposable implements vscode.RenameProvider {
private cachedRefs?: {
readonly resource: vscode.Uri;
@@ -58,8 +58,8 @@ export class MdRenameProvider extends Disposable implements vscode.RenameProvide
private readonly renameNotSupportedText = localize('invalidRenameLocation', "Rename not supported at location");
public constructor(
private readonly referencesComputer: MdReferencesComputer,
private readonly workspaceContents: MdWorkspaceContents,
private readonly referencesProvider: MdReferencesProvider,
private readonly slugifier: Slugifier,
) {
super();
@@ -253,7 +253,7 @@ export class MdRenameProvider extends Disposable implements vscode.RenameProvide
return this.cachedRefs;
}
const references = await this.referencesComputer.getReferencesAtPosition(document, position, token);
const references = await this.referencesProvider.getReferencesAtPosition(document, position, token);
const triggerRef = references.find(ref => ref.isTriggerLocation);
if (!triggerRef) {
return undefined;
@@ -270,3 +270,12 @@ export class MdRenameProvider extends Disposable implements vscode.RenameProvide
}
}
export function registerRenameSupport(
selector: vscode.DocumentSelector,
workspaceContents: MdWorkspaceContents,
referencesProvider: MdReferencesProvider,
slugifier: Slugifier,
): vscode.Disposable {
return vscode.languages.registerRenameProvider(selector, new MdVsCodeRenameProvider(workspaceContents, referencesProvider, slugifier));
}

View File

@@ -249,3 +249,10 @@ function getFirstChildHeader(document: SkinnyTextDocument, header?: TocEntry, to
}
return undefined;
}
export function registerSmartSelectSupport(
selector: vscode.DocumentSelector,
engine: MarkdownEngine,
): vscode.Disposable {
return vscode.languages.registerSelectionRangeProvider(selector, new MdSmartSelect(engine));
}

View File

@@ -9,12 +9,89 @@ import { Lazy, lazy } from '../util/lazy';
import { ResourceMap } from '../util/resourceMap';
import { MdWorkspaceContents, SkinnyTextDocument } from '../workspaceContents';
/**
* Cache of information for markdown files in the workspace.
*/
export class MdWorkspaceCache<T> extends Disposable {
class LazyResourceMap<T> {
private readonly _map = new ResourceMap<Lazy<Promise<T>>>();
private readonly _cache = new ResourceMap<Lazy<Promise<T>>>();
public has(resource: vscode.Uri): boolean {
return this._map.has(resource);
}
public get(resource: vscode.Uri): Promise<T> | undefined {
return this._map.get(resource)?.value;
}
public set(resource: vscode.Uri, value: Lazy<Promise<T>>) {
this._map.set(resource, value);
}
public delete(resource: vscode.Uri) {
this._map.delete(resource);
}
public entries(): Promise<Array<[vscode.Uri, T]>> {
return Promise.all(Array.from(this._map.entries(), async ([key, entry]) => {
return [key, await entry.value];
}));
}
}
/**
* Cache of information per-document in the workspace.
*
* The values are computed lazily and invalidated when the document changes.
*/
export class MdDocumentInfoCache<T> extends Disposable {
private readonly _cache = new LazyResourceMap<T>();
public constructor(
private readonly workspaceContents: MdWorkspaceContents,
private readonly getValue: (document: SkinnyTextDocument) => Promise<T>,
) {
super();
this._register(this.workspaceContents.onDidChangeMarkdownDocument(doc => this.onDidChangeDocument(doc)));
this._register(this.workspaceContents.onDidCreateMarkdownDocument(doc => this.onDidChangeDocument(doc)));
this._register(this.workspaceContents.onDidDeleteMarkdownDocument(this.onDidDeleteDocument, this));
}
public async get(resource: vscode.Uri): Promise<T | undefined> {
const existing = this._cache.get(resource);
if (existing) {
return existing;
}
const doc = await this.workspaceContents.getMarkdownDocument(resource);
return doc && this.onDidChangeDocument(doc, true)?.value;
}
public async entries(): Promise<Array<[vscode.Uri, T]>> {
return this._cache.entries();
}
private onDidChangeDocument(document: SkinnyTextDocument, forceAdd = false): Lazy<Promise<T>> | undefined {
if (forceAdd || this._cache.has(document.uri)) {
const value = lazy(() => this.getValue(document));
this._cache.set(document.uri, value);
return value;
}
return undefined;
}
private onDidDeleteDocument(resource: vscode.Uri) {
this._cache.delete(resource);
}
}
/**
* Cache of information across all markdown files in the workspace.
*
* Unlike {@link MdDocumentInfoCache}, the entries here are computed eagerly for every file in the workspace.
* However the computation of the values is still lazy.
*/
export class MdWorkspaceInfoCache<T> extends Disposable {
private readonly _cache = new LazyResourceMap<T>();
private _init?: Promise<void>;
public constructor(
@@ -26,14 +103,12 @@ export class MdWorkspaceCache<T> extends Disposable {
public async entries(): Promise<Array<[vscode.Uri, T]>> {
await this.ensureInit();
return Promise.all(Array.from(this._cache.entries(), async ([key, entry]) => {
return [key, await entry.value];
}));
return this._cache.entries();
}
public async values(): Promise<Array<T>> {
await this.ensureInit();
return Promise.all(Array.from(this._cache.values(), x => x.value));
return Array.from(await this._cache.entries(), x => x[1]);
}
private async ensureInit(): Promise<void> {

View File

@@ -7,11 +7,11 @@ import * as vscode from 'vscode';
import { Disposable } from '../util/dispose';
import { MdWorkspaceContents } from '../workspaceContents';
import { MdDocumentSymbolProvider } from './documentSymbolProvider';
import { MdWorkspaceCache } from './workspaceCache';
import { MdWorkspaceInfoCache } from './workspaceCache';
export class MdWorkspaceSymbolProvider extends Disposable implements vscode.WorkspaceSymbolProvider {
private readonly _cache: MdWorkspaceCache<vscode.SymbolInformation[]>;
private readonly _cache: MdWorkspaceInfoCache<vscode.SymbolInformation[]>;
public constructor(
symbolProvider: MdDocumentSymbolProvider,
@@ -19,7 +19,7 @@ export class MdWorkspaceSymbolProvider extends Disposable implements vscode.Work
) {
super();
this._cache = this._register(new MdWorkspaceCache(workspaceContents, doc => symbolProvider.provideDocumentSymbolInformation(doc)));
this._cache = this._register(new MdWorkspaceInfoCache(workspaceContents, doc => symbolProvider.provideDocumentSymbolInformation(doc)));
}
public async provideWorkspaceSymbols(query: string): Promise<vscode.SymbolInformation[]> {
@@ -27,3 +27,10 @@ export class MdWorkspaceSymbolProvider extends Disposable implements vscode.Work
return allSymbols.filter(symbolInformation => symbolInformation.name.toLowerCase().indexOf(query.toLowerCase()) !== -1);
}
}
export function registerWorkspaceSymbolSupport(
workspaceContents: MdWorkspaceContents,
symbolProvider: MdDocumentSymbolProvider,
): vscode.Disposable {
return vscode.languages.registerWorkspaceSymbolProvider(new MdWorkspaceSymbolProvider(symbolProvider, workspaceContents));
}