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

View file

@ -6,19 +6,19 @@
import * as vscode from 'vscode';
import { CommandManager } from './commandManager';
import * as commands from './commands/index';
import { registerPasteProvider } from './languageFeatures/copyPaste';
import { MdDefinitionProvider } from './languageFeatures/definitionProvider';
import { register as registerDiagnostics } from './languageFeatures/diagnostics';
import { MdLinkComputer, registerDocumentLinkProvider } from './languageFeatures/documentLinkProvider';
import { MdDocumentSymbolProvider } from './languageFeatures/documentSymbolProvider';
import { registerDropIntoEditor } from './languageFeatures/dropIntoEditor';
import { registerFindFileReferences } from './languageFeatures/fileReferences';
import { MdFoldingProvider } from './languageFeatures/foldingProvider';
import { MdPathCompletionProvider } from './languageFeatures/pathCompletions';
import { MdReferencesComputer, registerReferencesProvider } from './languageFeatures/references';
import { MdRenameProvider } from './languageFeatures/rename';
import { MdSmartSelect } from './languageFeatures/smartSelect';
import { MdWorkspaceSymbolProvider } from './languageFeatures/workspaceSymbolProvider';
import { registerPasteSupport } from './languageFeatures/copyPaste';
import { registerDefinitionSupport } from './languageFeatures/definitionProvider';
import { registerDiagnosticSupport } from './languageFeatures/diagnostics';
import { MdLinkProvider, registerDocumentLinkSupport } from './languageFeatures/documentLinkProvider';
import { MdDocumentSymbolProvider, registerDocumentSymbolSupport } from './languageFeatures/documentSymbolProvider';
import { registerDropIntoEditorSupport } from './languageFeatures/dropIntoEditor';
import { registerFindFileReferenceSupport } from './languageFeatures/fileReferences';
import { registerFoldingSupport } from './languageFeatures/foldingProvider';
import { registerPathCompletionSupport } from './languageFeatures/pathCompletions';
import { MdReferencesProvider, registerReferencesSupport } from './languageFeatures/references';
import { registerRenameSupport } from './languageFeatures/rename';
import { registerSmartSelectSupport } from './languageFeatures/smartSelect';
import { registerWorkspaceSymbolSupport } from './languageFeatures/workspaceSymbolProvider';
import { Logger } from './logger';
import { MarkdownEngine } from './markdownEngine';
import { getMarkdownExtensionContributions } from './markdownExtensions';
@ -43,11 +43,10 @@ export function activate(context: vscode.ExtensionContext) {
const commandManager = new CommandManager();
const contentProvider = new MarkdownContentProvider(engine, context, cspArbiter, contributions, logger);
const symbolProvider = new MdDocumentSymbolProvider(engine);
const previewManager = new MarkdownPreviewManager(contentProvider, logger, contributions, engine);
context.subscriptions.push(previewManager);
context.subscriptions.push(registerMarkdownLanguageFeatures(commandManager, symbolProvider, engine));
context.subscriptions.push(registerMarkdownLanguageFeatures(commandManager, engine));
context.subscriptions.push(registerMarkdownCommands(commandManager, previewManager, telemetryReporter, cspArbiter, engine));
context.subscriptions.push(vscode.workspace.onDidChangeConfiguration(() => {
@ -58,30 +57,35 @@ export function activate(context: vscode.ExtensionContext) {
function registerMarkdownLanguageFeatures(
commandManager: CommandManager,
symbolProvider: MdDocumentSymbolProvider,
engine: MarkdownEngine
): vscode.Disposable {
const selector: vscode.DocumentSelector = { language: 'markdown', scheme: '*' };
const linkComputer = new MdLinkComputer(engine);
const workspaceContents = new VsCodeMdWorkspaceContents();
const referencesComputer = new MdReferencesComputer(linkComputer, workspaceContents, engine, githubSlugifier);
const linkProvider = new MdLinkProvider(engine, workspaceContents);
const referencesProvider = new MdReferencesProvider(engine, workspaceContents);
const symbolProvider = new MdDocumentSymbolProvider(engine);
return vscode.Disposable.from(
workspaceContents,
vscode.languages.registerDocumentSymbolProvider(selector, symbolProvider),
vscode.languages.registerFoldingRangeProvider(selector, new MdFoldingProvider(engine)),
vscode.languages.registerSelectionRangeProvider(selector, new MdSmartSelect(engine)),
vscode.languages.registerWorkspaceSymbolProvider(new MdWorkspaceSymbolProvider(symbolProvider, workspaceContents)),
vscode.languages.registerRenameProvider(selector, new MdRenameProvider(referencesComputer, workspaceContents, githubSlugifier)),
vscode.languages.registerDefinitionProvider(selector, new MdDefinitionProvider(referencesComputer)),
MdPathCompletionProvider.register(selector, engine, linkComputer),
registerDocumentLinkProvider(selector, linkComputer),
registerDiagnostics(selector, engine, workspaceContents, linkComputer, commandManager, referencesComputer),
registerDropIntoEditor(selector),
registerReferencesProvider(selector, referencesComputer),
registerPasteProvider(selector),
registerFindFileReferences(commandManager, referencesComputer),
linkProvider,
referencesProvider,
// Language features
registerDefinitionSupport(selector, referencesProvider),
registerDiagnosticSupport(selector, engine, workspaceContents, linkProvider, commandManager, referencesProvider),
registerDocumentLinkSupport(selector, linkProvider),
registerDocumentSymbolSupport(selector, engine),
registerDropIntoEditorSupport(selector),
registerFindFileReferenceSupport(commandManager, referencesProvider),
registerFoldingSupport(selector, engine),
registerPasteSupport(selector),
registerPathCompletionSupport(selector, engine, linkProvider),
registerReferencesSupport(selector, referencesProvider),
registerRenameSupport(selector, workspaceContents, referencesProvider, engine.slugifier),
registerSmartSelectSupport(selector, engine),
registerWorkspaceSymbolSupport(workspaceContents, symbolProvider),
);
}

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));
}

View file

@ -98,10 +98,14 @@ export class MarkdownEngine {
private _slugCount = new Map<string, number>();
private _tokenCache = new TokenCache();
public readonly slugifier: Slugifier;
public constructor(
private readonly contributionProvider: MarkdownContributionProvider,
private readonly slugifier: Slugifier,
slugifier: Slugifier,
) {
this.slugifier = slugifier;
contributionProvider.onContributionsChanged(() => {
// Markdown plugin contributions may have changed
this.md = undefined;

View file

@ -7,9 +7,7 @@ import * as assert from 'assert';
import 'mocha';
import * as vscode from 'vscode';
import { MdDefinitionProvider } from '../languageFeatures/definitionProvider';
import { MdLinkComputer } from '../languageFeatures/documentLinkProvider';
import { MdReferencesComputer } from '../languageFeatures/references';
import { githubSlugifier } from '../slugify';
import { MdReferencesProvider } from '../languageFeatures/references';
import { noopToken } from '../util/cancellation';
import { InMemoryDocument } from '../util/inMemoryDocument';
import { MdWorkspaceContents } from '../workspaceContents';
@ -18,11 +16,10 @@ import { InMemoryWorkspaceMarkdownDocuments } from './inMemoryWorkspace';
import { joinLines, workspacePath } from './util';
function getDefinition(doc: InMemoryDocument, pos: vscode.Position, workspaceContents: MdWorkspaceContents) {
function getDefinition(doc: InMemoryDocument, pos: vscode.Position, workspace: MdWorkspaceContents) {
const engine = createNewMarkdownEngine();
const linkComputer = new MdLinkComputer(engine);
const referencesComputer = new MdReferencesComputer(linkComputer, workspaceContents, engine, githubSlugifier);
const provider = new MdDefinitionProvider(referencesComputer);
const referencesProvider = new MdReferencesProvider(engine, workspace);
const provider = new MdDefinitionProvider(referencesProvider);
return provider.provideDefinition(doc, pos, noopToken);
}

View file

@ -7,9 +7,8 @@ import * as assert from 'assert';
import 'mocha';
import * as vscode from 'vscode';
import { DiagnosticCollectionReporter, DiagnosticComputer, DiagnosticConfiguration, DiagnosticLevel, DiagnosticManager, DiagnosticOptions, DiagnosticReporter } from '../languageFeatures/diagnostics';
import { MdLinkComputer } from '../languageFeatures/documentLinkProvider';
import { MdReferencesComputer } from '../languageFeatures/references';
import { githubSlugifier } from '../slugify';
import { MdLinkProvider } from '../languageFeatures/documentLinkProvider';
import { MdReferencesProvider } from '../languageFeatures/references';
import { noopToken } from '../util/cancellation';
import { disposeAll } from '../util/dispose';
import { InMemoryDocument } from '../util/inMemoryDocument';
@ -28,10 +27,10 @@ const defaultDiagnosticsOptions = Object.freeze<DiagnosticOptions>({
ignoreLinks: [],
});
async function getComputedDiagnostics(doc: InMemoryDocument, workspaceContents: MdWorkspaceContents, options: Partial<DiagnosticOptions> = {}): Promise<vscode.Diagnostic[]> {
async function getComputedDiagnostics(doc: InMemoryDocument, workspace: MdWorkspaceContents, options: Partial<DiagnosticOptions> = {}): Promise<vscode.Diagnostic[]> {
const engine = createNewMarkdownEngine();
const linkComputer = new MdLinkComputer(engine);
const computer = new DiagnosticComputer(engine, workspaceContents, linkComputer);
const linkProvider = new MdLinkProvider(engine, workspace);
const computer = new DiagnosticComputer(engine, workspace, linkProvider);
return (
await computer.getDiagnostics(doc, { ...defaultDiagnosticsOptions, ...options, }, noopToken)
).diagnostics;
@ -430,17 +429,17 @@ suite('Markdown: Diagnostics manager', () => {
reporter: DiagnosticReporter = new DiagnosticCollectionReporter(),
) {
const engine = createNewMarkdownEngine();
const linkComputer = new MdLinkComputer(engine);
const referencesComputer = new MdReferencesComputer(linkComputer, workspace, engine, githubSlugifier);
const linkProvider = new MdLinkProvider(engine, workspace);
const referencesProvider = new MdReferencesProvider(engine, workspace);
const manager = new DiagnosticManager(
engine,
workspace,
new DiagnosticComputer(engine, workspace, linkComputer),
new DiagnosticComputer(engine, workspace, linkProvider),
configuration,
reporter,
referencesComputer,
referencesProvider,
0);
_disposables.push(manager, referencesComputer);
_disposables.push(manager, referencesProvider);
return manager;
}

View file

@ -6,19 +6,21 @@
import * as assert from 'assert';
import 'mocha';
import * as vscode from 'vscode';
import { MdLinkComputer, MdLinkProvider } from '../languageFeatures/documentLinkProvider';
import { MdLinkProvider, MdVsCodeLinkProvider } from '../languageFeatures/documentLinkProvider';
import { noopToken } from '../util/cancellation';
import { InMemoryDocument } from '../util/inMemoryDocument';
import { createNewMarkdownEngine } from './engine';
import { assertRangeEqual, joinLines } from './util';
import { InMemoryWorkspaceMarkdownDocuments } from './inMemoryWorkspace';
import { assertRangeEqual, joinLines, workspacePath } from './util';
const testFile = vscode.Uri.joinPath(vscode.workspace.workspaceFolders![0].uri, 'x.md');
function getLinksForFile(fileContents: string) {
const doc = new InMemoryDocument(testFile, fileContents);
const linkComputer = new MdLinkComputer(createNewMarkdownEngine());
const provider = new MdLinkProvider(linkComputer);
const doc = new InMemoryDocument(workspacePath('x.md'), fileContents);
const workspace = new InMemoryWorkspaceMarkdownDocuments([doc]);
const engine = createNewMarkdownEngine();
const linkProvider = new MdLinkProvider(engine, workspace);
const provider = new MdVsCodeLinkProvider(linkProvider);
return provider.provideDocumentLinks(doc, noopToken);
}

View file

@ -5,22 +5,18 @@
import * as assert from 'assert';
import 'mocha';
import * as vscode from 'vscode';
import { MdDocumentSymbolProvider } from '../languageFeatures/documentSymbolProvider';
import { createNewMarkdownEngine } from './engine';
import { InMemoryDocument } from '../util/inMemoryDocument';
const testFileName = vscode.Uri.file('test.md');
import { workspacePath } from './util';
function getSymbolsForFile(fileContents: string) {
const doc = new InMemoryDocument(testFileName, fileContents);
const doc = new InMemoryDocument(workspacePath('test.md'), fileContents);
const provider = new MdDocumentSymbolProvider(createNewMarkdownEngine());
return provider.provideDocumentSymbols(doc);
}
suite('markdown.DocumentSymbolProvider', () => {
test('Should not return anything for empty document', async () => {
const symbols = await getSymbolsForFile('');

View file

@ -6,9 +6,7 @@
import * as assert from 'assert';
import 'mocha';
import * as vscode from 'vscode';
import { MdLinkComputer } from '../languageFeatures/documentLinkProvider';
import { MdReference, MdReferencesComputer } from '../languageFeatures/references';
import { githubSlugifier } from '../slugify';
import { MdReference, MdReferencesProvider } from '../languageFeatures/references';
import { noopToken } from '../util/cancellation';
import { InMemoryDocument } from '../util/inMemoryDocument';
import { MdWorkspaceContents } from '../workspaceContents';
@ -19,8 +17,7 @@ import { joinLines, workspacePath } from './util';
function getFileReferences(resource: vscode.Uri, workspaceContents: MdWorkspaceContents) {
const engine = createNewMarkdownEngine();
const linkComputer = new MdLinkComputer(engine);
const computer = new MdReferencesComputer(linkComputer, workspaceContents, engine, githubSlugifier);
const computer = new MdReferencesProvider(engine, workspaceContents);
return computer.getAllReferencesToFile(resource, noopToken);
}

View file

@ -6,19 +6,22 @@
import * as assert from 'assert';
import 'mocha';
import * as vscode from 'vscode';
import { MdLinkComputer } from '../languageFeatures/documentLinkProvider';
import { MdPathCompletionProvider } from '../languageFeatures/pathCompletions';
import { MdLinkProvider } from '../languageFeatures/documentLinkProvider';
import { MdVsCodePathCompletionProvider } from '../languageFeatures/pathCompletions';
import { noopToken } from '../util/cancellation';
import { InMemoryDocument } from '../util/inMemoryDocument';
import { createNewMarkdownEngine } from './engine';
import { InMemoryWorkspaceMarkdownDocuments } from './inMemoryWorkspace';
import { CURSOR, getCursorPositions, joinLines, workspacePath } from './util';
function getCompletionsAtCursor(resource: vscode.Uri, fileContents: string) {
const doc = new InMemoryDocument(resource, fileContents);
const workspace = new InMemoryWorkspaceMarkdownDocuments([doc]);
const engine = createNewMarkdownEngine();
const linkComputer = new MdLinkComputer(engine);
const provider = new MdPathCompletionProvider(engine, linkComputer);
const linkProvider = new MdLinkProvider(engine, workspace);
const provider = new MdVsCodePathCompletionProvider(engine, linkProvider);
const cursorPositions = getCursorPositions(fileContents, doc);
return provider.provideCompletionItems(doc, cursorPositions[0], noopToken, {
triggerCharacter: undefined,

View file

@ -6,9 +6,7 @@
import * as assert from 'assert';
import 'mocha';
import * as vscode from 'vscode';
import { MdLinkComputer } from '../languageFeatures/documentLinkProvider';
import { MdReferencesComputer, MdVsCodeReferencesProvider } from '../languageFeatures/references';
import { githubSlugifier } from '../slugify';
import { MdReferencesProvider, MdVsCodeReferencesProvider } from '../languageFeatures/references';
import { noopToken } from '../util/cancellation';
import { InMemoryDocument } from '../util/inMemoryDocument';
import { MdWorkspaceContents } from '../workspaceContents';
@ -19,8 +17,7 @@ import { joinLines, workspacePath } from './util';
function getReferences(doc: InMemoryDocument, pos: vscode.Position, workspaceContents: MdWorkspaceContents) {
const engine = createNewMarkdownEngine();
const linkComputer = new MdLinkComputer(engine);
const computer = new MdReferencesComputer(linkComputer, workspaceContents, engine, githubSlugifier);
const computer = new MdReferencesProvider(engine, workspaceContents);
const provider = new MdVsCodeReferencesProvider(computer);
return provider.provideReferences(doc, pos, { includeDeclaration: true }, noopToken);
}

View file

@ -6,9 +6,8 @@
import * as assert from 'assert';
import 'mocha';
import * as vscode from 'vscode';
import { MdLinkComputer } from '../languageFeatures/documentLinkProvider';
import { MdReferencesComputer } from '../languageFeatures/references';
import { MdRenameProvider, MdWorkspaceEdit } from '../languageFeatures/rename';
import { MdReferencesProvider } from '../languageFeatures/references';
import { MdVsCodeRenameProvider, MdWorkspaceEdit } from '../languageFeatures/rename';
import { githubSlugifier } from '../slugify';
import { noopToken } from '../util/cancellation';
import { InMemoryDocument } from '../util/inMemoryDocument';
@ -21,22 +20,20 @@ import { assertRangeEqual, joinLines, workspacePath } from './util';
/**
* Get prepare rename info.
*/
function prepareRename(doc: InMemoryDocument, pos: vscode.Position, workspaceContents: MdWorkspaceContents): Promise<undefined | { readonly range: vscode.Range; readonly placeholder: string }> {
function prepareRename(doc: InMemoryDocument, pos: vscode.Position, workspace: MdWorkspaceContents): Promise<undefined | { readonly range: vscode.Range; readonly placeholder: string }> {
const engine = createNewMarkdownEngine();
const linkComputer = new MdLinkComputer(engine);
const referenceComputer = new MdReferencesComputer(linkComputer, workspaceContents, engine, githubSlugifier);
const renameProvider = new MdRenameProvider(referenceComputer, workspaceContents, githubSlugifier);
const referenceComputer = new MdReferencesProvider(engine, workspace);
const renameProvider = new MdVsCodeRenameProvider(workspace, referenceComputer, githubSlugifier);
return renameProvider.prepareRename(doc, pos, noopToken);
}
/**
* Get all the edits for the rename.
*/
function getRenameEdits(doc: InMemoryDocument, pos: vscode.Position, newName: string, workspaceContents: MdWorkspaceContents): Promise<MdWorkspaceEdit | undefined> {
function getRenameEdits(doc: InMemoryDocument, pos: vscode.Position, newName: string, workspace: MdWorkspaceContents): Promise<MdWorkspaceEdit | undefined> {
const engine = createNewMarkdownEngine();
const linkComputer = new MdLinkComputer(engine);
const referencesProvider = new MdReferencesComputer(linkComputer, workspaceContents, engine, githubSlugifier);
const renameProvider = new MdRenameProvider(referencesProvider, workspaceContents, githubSlugifier);
const referencesProvider = new MdReferencesProvider(engine, workspace);
const renameProvider = new MdVsCodeRenameProvider(workspace, referencesProvider, githubSlugifier);
return renameProvider.provideRenameEditsImpl(doc, pos, newName, noopToken);
}