Skip to content

Commit 623f55f

Browse files
authored
Refactor markdown language features (microsoft#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
1 parent 54f5758 commit 623f55f

24 files changed

+331
-179
lines changed

extensions/markdown-language-features/src/extension.ts

Lines changed: 35 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,19 @@
66
import * as vscode from 'vscode';
77
import { CommandManager } from './commandManager';
88
import * as commands from './commands/index';
9-
import { registerPasteProvider } from './languageFeatures/copyPaste';
10-
import { MdDefinitionProvider } from './languageFeatures/definitionProvider';
11-
import { register as registerDiagnostics } from './languageFeatures/diagnostics';
12-
import { MdLinkComputer, registerDocumentLinkProvider } from './languageFeatures/documentLinkProvider';
13-
import { MdDocumentSymbolProvider } from './languageFeatures/documentSymbolProvider';
14-
import { registerDropIntoEditor } from './languageFeatures/dropIntoEditor';
15-
import { registerFindFileReferences } from './languageFeatures/fileReferences';
16-
import { MdFoldingProvider } from './languageFeatures/foldingProvider';
17-
import { MdPathCompletionProvider } from './languageFeatures/pathCompletions';
18-
import { MdReferencesComputer, registerReferencesProvider } from './languageFeatures/references';
19-
import { MdRenameProvider } from './languageFeatures/rename';
20-
import { MdSmartSelect } from './languageFeatures/smartSelect';
21-
import { MdWorkspaceSymbolProvider } from './languageFeatures/workspaceSymbolProvider';
9+
import { registerPasteSupport } from './languageFeatures/copyPaste';
10+
import { registerDefinitionSupport } from './languageFeatures/definitionProvider';
11+
import { registerDiagnosticSupport } from './languageFeatures/diagnostics';
12+
import { MdLinkProvider, registerDocumentLinkSupport } from './languageFeatures/documentLinkProvider';
13+
import { MdDocumentSymbolProvider, registerDocumentSymbolSupport } from './languageFeatures/documentSymbolProvider';
14+
import { registerDropIntoEditorSupport } from './languageFeatures/dropIntoEditor';
15+
import { registerFindFileReferenceSupport } from './languageFeatures/fileReferences';
16+
import { registerFoldingSupport } from './languageFeatures/foldingProvider';
17+
import { registerPathCompletionSupport } from './languageFeatures/pathCompletions';
18+
import { MdReferencesProvider, registerReferencesSupport } from './languageFeatures/references';
19+
import { registerRenameSupport } from './languageFeatures/rename';
20+
import { registerSmartSelectSupport } from './languageFeatures/smartSelect';
21+
import { registerWorkspaceSymbolSupport } from './languageFeatures/workspaceSymbolProvider';
2222
import { Logger } from './logger';
2323
import { MarkdownEngine } from './markdownEngine';
2424
import { getMarkdownExtensionContributions } from './markdownExtensions';
@@ -43,11 +43,10 @@ export function activate(context: vscode.ExtensionContext) {
4343
const commandManager = new CommandManager();
4444

4545
const contentProvider = new MarkdownContentProvider(engine, context, cspArbiter, contributions, logger);
46-
const symbolProvider = new MdDocumentSymbolProvider(engine);
4746
const previewManager = new MarkdownPreviewManager(contentProvider, logger, contributions, engine);
4847
context.subscriptions.push(previewManager);
4948

50-
context.subscriptions.push(registerMarkdownLanguageFeatures(commandManager, symbolProvider, engine));
49+
context.subscriptions.push(registerMarkdownLanguageFeatures(commandManager, engine));
5150
context.subscriptions.push(registerMarkdownCommands(commandManager, previewManager, telemetryReporter, cspArbiter, engine));
5251

5352
context.subscriptions.push(vscode.workspace.onDidChangeConfiguration(() => {
@@ -58,30 +57,35 @@ export function activate(context: vscode.ExtensionContext) {
5857

5958
function registerMarkdownLanguageFeatures(
6059
commandManager: CommandManager,
61-
symbolProvider: MdDocumentSymbolProvider,
6260
engine: MarkdownEngine
6361
): vscode.Disposable {
6462
const selector: vscode.DocumentSelector = { language: 'markdown', scheme: '*' };
6563

66-
const linkComputer = new MdLinkComputer(engine);
6764
const workspaceContents = new VsCodeMdWorkspaceContents();
6865

69-
const referencesComputer = new MdReferencesComputer(linkComputer, workspaceContents, engine, githubSlugifier);
66+
const linkProvider = new MdLinkProvider(engine, workspaceContents);
67+
const referencesProvider = new MdReferencesProvider(engine, workspaceContents);
68+
const symbolProvider = new MdDocumentSymbolProvider(engine);
69+
7070
return vscode.Disposable.from(
7171
workspaceContents,
72-
vscode.languages.registerDocumentSymbolProvider(selector, symbolProvider),
73-
vscode.languages.registerFoldingRangeProvider(selector, new MdFoldingProvider(engine)),
74-
vscode.languages.registerSelectionRangeProvider(selector, new MdSmartSelect(engine)),
75-
vscode.languages.registerWorkspaceSymbolProvider(new MdWorkspaceSymbolProvider(symbolProvider, workspaceContents)),
76-
vscode.languages.registerRenameProvider(selector, new MdRenameProvider(referencesComputer, workspaceContents, githubSlugifier)),
77-
vscode.languages.registerDefinitionProvider(selector, new MdDefinitionProvider(referencesComputer)),
78-
MdPathCompletionProvider.register(selector, engine, linkComputer),
79-
registerDocumentLinkProvider(selector, linkComputer),
80-
registerDiagnostics(selector, engine, workspaceContents, linkComputer, commandManager, referencesComputer),
81-
registerDropIntoEditor(selector),
82-
registerReferencesProvider(selector, referencesComputer),
83-
registerPasteProvider(selector),
84-
registerFindFileReferences(commandManager, referencesComputer),
72+
linkProvider,
73+
referencesProvider,
74+
75+
// Language features
76+
registerDefinitionSupport(selector, referencesProvider),
77+
registerDiagnosticSupport(selector, engine, workspaceContents, linkProvider, commandManager, referencesProvider),
78+
registerDocumentLinkSupport(selector, linkProvider),
79+
registerDocumentSymbolSupport(selector, engine),
80+
registerDropIntoEditorSupport(selector),
81+
registerFindFileReferenceSupport(commandManager, referencesProvider),
82+
registerFoldingSupport(selector, engine),
83+
registerPasteSupport(selector),
84+
registerPathCompletionSupport(selector, engine, linkProvider),
85+
registerReferencesSupport(selector, referencesProvider),
86+
registerRenameSupport(selector, workspaceContents, referencesProvider, engine.slugifier),
87+
registerSmartSelectSupport(selector, engine),
88+
registerWorkspaceSymbolSupport(workspaceContents, symbolProvider),
8589
);
8690
}
8791

extensions/markdown-language-features/src/languageFeatures/copyPaste.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import * as vscode from 'vscode';
77
import { tryGetUriListSnippet } from './dropIntoEditor';
88

9-
export function registerPasteProvider(selector: vscode.DocumentSelector) {
9+
export function registerPasteSupport(selector: vscode.DocumentSelector) {
1010
return vscode.languages.registerDocumentPasteEditProvider(selector, new class implements vscode.DocumentPasteEditProvider {
1111

1212
async provideDocumentPasteEdits(

extensions/markdown-language-features/src/languageFeatures/definitionProvider.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,25 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55
import * as vscode from 'vscode';
6-
import { Disposable } from '../util/dispose';
76
import { SkinnyTextDocument } from '../workspaceContents';
8-
import { MdReferencesComputer } from './references';
7+
import { MdReferencesProvider } from './references';
98

10-
export class MdDefinitionProvider extends Disposable implements vscode.DefinitionProvider {
9+
export class MdDefinitionProvider implements vscode.DefinitionProvider {
1110

1211
constructor(
13-
private readonly referencesComputer: MdReferencesComputer
14-
) {
15-
super();
16-
}
12+
private readonly referencesProvider: MdReferencesProvider
13+
) { }
1714

1815
async provideDefinition(document: SkinnyTextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise<vscode.Definition | undefined> {
19-
const allRefs = await this.referencesComputer.getReferencesAtPosition(document, position, token);
16+
const allRefs = await this.referencesProvider.getReferencesAtPosition(document, position, token);
2017

2118
return allRefs.find(ref => ref.kind === 'link' && ref.isDefinition)?.location;
2219
}
2320
}
21+
22+
export function registerDefinitionSupport(
23+
selector: vscode.DocumentSelector,
24+
referencesProvider: MdReferencesProvider,
25+
): vscode.Disposable {
26+
return vscode.languages.registerDefinitionProvider(selector, new MdDefinitionProvider(referencesProvider));
27+
}

extensions/markdown-language-features/src/languageFeatures/diagnostics.ts

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ import { Limiter } from '../util/limiter';
1717
import { ResourceMap } from '../util/resourceMap';
1818
import { MdTableOfContentsWatcher } from '../test/tableOfContentsWatcher';
1919
import { MdWorkspaceContents, SkinnyTextDocument } from '../workspaceContents';
20-
import { InternalHref, LinkDefinitionSet, MdLink, MdLinkComputer, MdLinkSource } from './documentLinkProvider';
21-
import { MdReferencesComputer, tryFindMdDocumentForLink } from './references';
20+
import { InternalHref, MdLink, MdLinkSource, MdLinkProvider, LinkDefinitionSet } from './documentLinkProvider';
21+
import { MdReferencesProvider, tryFindMdDocumentForLink } from './references';
2222

2323
const localize = nls.loadMessageBundle();
2424

@@ -305,7 +305,7 @@ export class DiagnosticManager extends Disposable {
305305
private readonly computer: DiagnosticComputer,
306306
private readonly configuration: DiagnosticConfiguration,
307307
private readonly reporter: DiagnosticReporter,
308-
private readonly referencesComputer: MdReferencesComputer,
308+
private readonly referencesProvider: MdReferencesProvider,
309309
delay = 300,
310310
) {
311311
super();
@@ -344,7 +344,7 @@ export class DiagnosticManager extends Disposable {
344344
this._register(this.tableOfContentsWatcher.onTocChanged(async e => {
345345
// When the toc of a document changes, revalidate every file that linked to it too
346346
const triggered = new ResourceMap<void>();
347-
for (const ref of await this.referencesComputer.getAllReferencesToFile(e.uri, noopToken)) {
347+
for (const ref of await this.referencesProvider.getAllReferencesToFile(e.uri, noopToken)) {
348348
const file = ref.location.uri;
349349
if (!triggered.has(file)) {
350350
this.triggerDiagnostics(file);
@@ -450,11 +450,11 @@ export class DiagnosticComputer {
450450
constructor(
451451
private readonly engine: MarkdownEngine,
452452
private readonly workspaceContents: MdWorkspaceContents,
453-
private readonly linkComputer: MdLinkComputer,
453+
private readonly linkProvider: MdLinkProvider,
454454
) { }
455455

456-
public async getDiagnostics(doc: SkinnyTextDocument, options: DiagnosticOptions, token: vscode.CancellationToken): Promise<{ readonly diagnostics: vscode.Diagnostic[]; readonly links: MdLink[] }> {
457-
const links = await this.linkComputer.getAllLinks(doc, token);
456+
public async getDiagnostics(doc: SkinnyTextDocument, options: DiagnosticOptions, token: vscode.CancellationToken): Promise<{ readonly diagnostics: vscode.Diagnostic[]; readonly links: readonly MdLink[] }> {
457+
const { links, definitions } = await this.linkProvider.getLinks(doc);
458458
if (token.isCancellationRequested || !options.enabled) {
459459
return { links, diagnostics: [] };
460460
}
@@ -463,7 +463,7 @@ export class DiagnosticComputer {
463463
links,
464464
diagnostics: (await Promise.all([
465465
this.validateFileLinks(options, links, token),
466-
Array.from(this.validateReferenceLinks(options, links)),
466+
Array.from(this.validateReferenceLinks(options, links, definitions)),
467467
this.validateFragmentLinks(doc, options, links, token),
468468
])).flat()
469469
};
@@ -501,15 +501,14 @@ export class DiagnosticComputer {
501501
return diagnostics;
502502
}
503503

504-
private *validateReferenceLinks(options: DiagnosticOptions, links: readonly MdLink[]): Iterable<vscode.Diagnostic> {
504+
private *validateReferenceLinks(options: DiagnosticOptions, links: readonly MdLink[], definitions: LinkDefinitionSet): Iterable<vscode.Diagnostic> {
505505
const severity = toSeverity(options.validateReferences);
506506
if (typeof severity === 'undefined') {
507507
return [];
508508
}
509509

510-
const definitionSet = new LinkDefinitionSet(links);
511510
for (const link of links) {
512-
if (link.href.kind === 'reference' && !definitionSet.lookup(link.href.ref)) {
511+
if (link.href.kind === 'reference' && !definitions.lookup(link.href.ref)) {
513512
yield new vscode.Diagnostic(
514513
link.source.hrefRange,
515514
localize('invalidReferenceLink', 'No link definition found: \'{0}\'', link.href.ref),
@@ -620,19 +619,19 @@ class AddToIgnoreLinksQuickFixProvider implements vscode.CodeActionProvider {
620619
}
621620
}
622621

623-
export function register(
622+
export function registerDiagnosticSupport(
624623
selector: vscode.DocumentSelector,
625624
engine: MarkdownEngine,
626625
workspaceContents: MdWorkspaceContents,
627-
linkComputer: MdLinkComputer,
626+
linkProvider: MdLinkProvider,
628627
commandManager: CommandManager,
629-
referenceComputer: MdReferencesComputer,
628+
referenceComputer: MdReferencesProvider,
630629
): vscode.Disposable {
631630
const configuration = new VSCodeDiagnosticConfiguration();
632631
const manager = new DiagnosticManager(
633632
engine,
634633
workspaceContents,
635-
new DiagnosticComputer(engine, workspaceContents, linkComputer),
634+
new DiagnosticComputer(engine, workspaceContents, linkProvider),
636635
configuration,
637636
new DiagnosticCollectionReporter(),
638637
referenceComputer);

extensions/markdown-language-features/src/languageFeatures/documentLinkProvider.ts

Lines changed: 51 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,11 @@ import * as uri from 'vscode-uri';
99
import { OpenDocumentLinkCommand } from '../commands/openDocumentLink';
1010
import { MarkdownEngine } from '../markdownEngine';
1111
import { coalesce } from '../util/arrays';
12+
import { noopToken } from '../util/cancellation';
13+
import { Disposable } from '../util/dispose';
1214
import { getUriForLinkWithKnownExternalScheme, isOfScheme, Schemes } from '../util/schemes';
13-
import { SkinnyTextDocument } from '../workspaceContents';
15+
import { MdWorkspaceContents, SkinnyTextDocument } from '../workspaceContents';
16+
import { MdDocumentInfoCache } from './workspaceCache';
1417

1518
const localize = nls.loadMessageBundle();
1619

@@ -242,6 +245,9 @@ class NoLinkRanges {
242245
}
243246
}
244247

248+
/**
249+
* Stateless object that extracts link information from markdown files.
250+
*/
245251
export class MdLinkComputer {
246252

247253
constructor(
@@ -257,7 +263,7 @@ export class MdLinkComputer {
257263
return Array.from([
258264
...this.getInlineLinks(document, noLinkRanges),
259265
...this.getReferenceLinks(document, noLinkRanges),
260-
...this.getLinkDefinitions2(document, noLinkRanges),
266+
...this.getLinkDefinitions(document, noLinkRanges),
261267
...this.getAutoLinks(document, noLinkRanges),
262268
]);
263269
}
@@ -369,12 +375,7 @@ export class MdLinkComputer {
369375
}
370376
}
371377

372-
public async getLinkDefinitions(document: SkinnyTextDocument): Promise<Iterable<MdLinkDefinition>> {
373-
const noLinkRanges = await NoLinkRanges.compute(document, this.engine);
374-
return this.getLinkDefinitions2(document, noLinkRanges);
375-
}
376-
377-
private *getLinkDefinitions2(document: SkinnyTextDocument, noLinkRanges: NoLinkRanges): Iterable<MdLinkDefinition> {
378+
private *getLinkDefinitions(document: SkinnyTextDocument, noLinkRanges: NoLinkRanges): Iterable<MdLinkDefinition> {
378379
const text = document.getText();
379380
for (const match of text.matchAll(definitionPattern)) {
380381
const pre = match[1];
@@ -419,7 +420,37 @@ export class MdLinkComputer {
419420
}
420421
}
421422

422-
export class LinkDefinitionSet {
423+
/**
424+
* Stateful object which provides links for markdown files the workspace.
425+
*/
426+
export class MdLinkProvider extends Disposable {
427+
428+
private readonly _linkCache: MdDocumentInfoCache<readonly MdLink[]>;
429+
430+
private readonly linkComputer: MdLinkComputer;
431+
432+
constructor(
433+
engine: MarkdownEngine,
434+
workspaceContents: MdWorkspaceContents,
435+
) {
436+
super();
437+
this.linkComputer = new MdLinkComputer(engine);
438+
this._linkCache = this._register(new MdDocumentInfoCache(workspaceContents, doc => this.linkComputer.getAllLinks(doc, noopToken)));
439+
}
440+
441+
public async getLinks(document: SkinnyTextDocument): Promise<{
442+
readonly links: readonly MdLink[];
443+
readonly definitions: LinkDefinitionSet;
444+
}> {
445+
const links = (await this._linkCache.get(document.uri)) ?? [];
446+
return {
447+
links,
448+
definitions: new LinkDefinitionSet(links),
449+
};
450+
}
451+
}
452+
453+
export class LinkDefinitionSet implements Iterable<[string, MdLinkDefinition]> {
423454
private readonly _map = new Map<string, MdLinkDefinition>();
424455

425456
constructor(links: Iterable<MdLink>) {
@@ -430,29 +461,31 @@ export class LinkDefinitionSet {
430461
}
431462
}
432463

464+
public [Symbol.iterator](): Iterator<[string, MdLinkDefinition]> {
465+
return this._map.entries();
466+
}
467+
433468
public lookup(ref: string): MdLinkDefinition | undefined {
434469
return this._map.get(ref);
435470
}
436471
}
437472

438-
export class MdLinkProvider implements vscode.DocumentLinkProvider {
473+
export class MdVsCodeLinkProvider implements vscode.DocumentLinkProvider {
439474

440475
constructor(
441-
private readonly _linkComputer: MdLinkComputer,
476+
private readonly _linkProvider: MdLinkProvider,
442477
) { }
443478

444479
public async provideDocumentLinks(
445480
document: SkinnyTextDocument,
446481
token: vscode.CancellationToken
447482
): Promise<vscode.DocumentLink[]> {
448-
const allLinks = (await this._linkComputer.getAllLinks(document, token)) ?? [];
483+
const { links, definitions } = await this._linkProvider.getLinks(document);
449484
if (token.isCancellationRequested) {
450485
return [];
451486
}
452487

453-
const definitionSet = new LinkDefinitionSet(allLinks);
454-
return coalesce(allLinks
455-
.map(data => this.toValidDocumentLink(data, definitionSet)));
488+
return coalesce(links.map(data => this.toValidDocumentLink(data, definitions)));
456489
}
457490

458491
private toValidDocumentLink(link: MdLink, definitionSet: LinkDefinitionSet): vscode.DocumentLink | undefined {
@@ -482,9 +515,9 @@ export class MdLinkProvider implements vscode.DocumentLinkProvider {
482515
}
483516
}
484517

485-
export function registerDocumentLinkProvider(
518+
export function registerDocumentLinkSupport(
486519
selector: vscode.DocumentSelector,
487-
linkComputer: MdLinkComputer,
520+
linkProvider: MdLinkProvider,
488521
): vscode.Disposable {
489-
return vscode.languages.registerDocumentLinkProvider(selector, new MdLinkProvider(linkComputer));
522+
return vscode.languages.registerDocumentLinkProvider(selector, new MdVsCodeLinkProvider(linkProvider));
490523
}

extensions/markdown-language-features/src/languageFeatures/documentSymbolProvider.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,3 +74,10 @@ export class MdDocumentSymbolProvider implements vscode.DocumentSymbolProvider {
7474
return '#'.repeat(entry.level) + ' ' + entry.text;
7575
}
7676
}
77+
78+
export function registerDocumentSymbolSupport(
79+
selector: vscode.DocumentSelector,
80+
engine: MarkdownEngine,
81+
): vscode.Disposable {
82+
return vscode.languages.registerDocumentSymbolProvider(selector, new MdDocumentSymbolProvider(engine));
83+
}

extensions/markdown-language-features/src/languageFeatures/dropIntoEditor.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ const imageFileExtensions = new Set<string>([
2323
'.webp',
2424
]);
2525

26-
export function registerDropIntoEditor(selector: vscode.DocumentSelector) {
26+
export function registerDropIntoEditorSupport(selector: vscode.DocumentSelector) {
2727
return vscode.languages.registerDocumentOnDropEditProvider(selector, new class implements vscode.DocumentOnDropEditProvider {
2828
async provideDocumentOnDropEdits(document: vscode.TextDocument, _position: vscode.Position, dataTransfer: vscode.DataTransfer, token: vscode.CancellationToken): Promise<vscode.DocumentDropEdit | undefined> {
2929
const enabled = vscode.workspace.getConfiguration('markdown', document).get('editor.drop.enabled', true);

0 commit comments

Comments
 (0)