Skip to content

Commit c84655d

Browse files
authored
Reduce recomputation of state in markdown extension (microsoft#152804)
* Reduce recomputation of state in markdown extension - Use `getForDocument` more often to avoid refetching documents - Debounce `MdTableOfContentsWatcher`. We don't want this to trigger on every keystroke :) * Cache LinkDefinitionSet * Add test file change * Fix toc watcher for tests
1 parent f9d332c commit c84655d

File tree

9 files changed

+64
-31
lines changed

9 files changed

+64
-31
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export function activate(context: vscode.ExtensionContext) {
4444
const cspArbiter = new ExtensionContentSecurityPolicyArbiter(context.globalState, context.workspaceState);
4545
const commandManager = new CommandManager();
4646

47-
const engine = new MarkdownItEngine(contributions, githubSlugifier);
47+
const engine = new MarkdownItEngine(contributions, githubSlugifier, logger);
4848
const workspaceContents = new VsCodeMdWorkspaceContents();
4949
const parser = new MdParsingProvider(engine, workspaceContents);
5050
const tocProvider = new MdTableOfContentsProvider(parser, workspaceContents, logger);

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,13 @@ import * as nls from 'vscode-nls';
99
import { CommandManager } from '../commandManager';
1010
import { ILogger } from '../logging';
1111
import { MdTableOfContentsProvider } from '../tableOfContents';
12-
import { MdTableOfContentsWatcher } from '../test/tableOfContentsWatcher';
1312
import { Delayer } from '../util/async';
1413
import { noopToken } from '../util/cancellation';
1514
import { Disposable } from '../util/dispose';
1615
import { isMarkdownFile, looksLikeMarkdownPath } from '../util/file';
1716
import { Limiter } from '../util/limiter';
1817
import { ResourceMap } from '../util/resourceMap';
18+
import { MdTableOfContentsWatcher } from '../util/tableOfContentsWatcher';
1919
import { MdWorkspaceContents, SkinnyTextDocument } from '../workspaceContents';
2020
import { InternalHref, LinkDefinitionSet, MdLink, MdLinkProvider, MdLinkSource } from './documentLinks';
2121
import { MdReferencesProvider, tryResolveLinkPath } from './references';
@@ -347,7 +347,7 @@ export class DiagnosticManager extends Disposable {
347347
}
348348
}));
349349

350-
this.tableOfContentsWatcher = this._register(new MdTableOfContentsWatcher(workspaceContents, tocProvider));
350+
this.tableOfContentsWatcher = this._register(new MdTableOfContentsWatcher(workspaceContents, tocProvider, delay));
351351
this._register(this.tableOfContentsWatcher.onTocChanged(async e => {
352352
// When the toc of a document changes, revalidate every file that linked to it too
353353
const triggered = new ResourceMap<void>();
@@ -491,7 +491,7 @@ export class DiagnosticComputer {
491491
return [];
492492
}
493493

494-
const toc = await this.tocProvider.get(doc.uri);
494+
const toc = await this.tocProvider.getForDocument(doc);
495495
if (token.isCancellationRequested) {
496496
return [];
497497
}

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

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -427,12 +427,17 @@ export class MdLinkComputer {
427427
}
428428
}
429429

430+
interface MdDocumentLinks {
431+
readonly links: readonly MdLink[];
432+
readonly definitions: LinkDefinitionSet;
433+
}
434+
430435
/**
431436
* Stateful object which provides links for markdown files the workspace.
432437
*/
433438
export class MdLinkProvider extends Disposable {
434439

435-
private readonly _linkCache: MdDocumentInfoCache<readonly MdLink[]>;
440+
private readonly _linkCache: MdDocumentInfoCache<MdDocumentLinks>;
436441

437442
private readonly linkComputer: MdLinkComputer;
438443

@@ -443,21 +448,19 @@ export class MdLinkProvider extends Disposable {
443448
) {
444449
super();
445450
this.linkComputer = new MdLinkComputer(tokenizer);
446-
this._linkCache = this._register(new MdDocumentInfoCache(workspaceContents, doc => {
451+
this._linkCache = this._register(new MdDocumentInfoCache(workspaceContents, async doc => {
447452
logger.verbose('LinkProvider', `compute - ${doc.uri}`);
448-
return this.linkComputer.getAllLinks(doc, noopToken);
453+
454+
const links = await this.linkComputer.getAllLinks(doc, noopToken);
455+
return {
456+
links,
457+
definitions: new LinkDefinitionSet(links),
458+
};
449459
}));
450460
}
451461

452-
public async getLinks(document: SkinnyTextDocument): Promise<{
453-
readonly links: readonly MdLink[];
454-
readonly definitions: LinkDefinitionSet;
455-
}> {
456-
const links = (await this._linkCache.get(document.uri)) ?? [];
457-
return {
458-
links,
459-
definitions: new LinkDefinitionSet(links),
460-
};
462+
public async getLinks(document: SkinnyTextDocument): Promise<MdDocumentLinks> {
463+
return this._linkCache.getForDocument(document);
461464
}
462465
}
463466

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ export class MdFoldingProvider implements vscode.FoldingRangeProvider {
5656
}
5757

5858
private async getHeaderFoldingRanges(document: SkinnyTextDocument): Promise<vscode.FoldingRange[]> {
59-
const toc = await this.tocProvide.get(document.uri);
59+
const toc = await this.tocProvide.getForDocument(document);
6060
return toc.entries.map(entry => {
6161
let endLine = entry.sectionLocation.range.end.line;
6262
if (document.lineAt(endLine).isEmptyOrWhitespace && endLine >= entry.line + 1) {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ export class MdReferencesProvider extends Disposable {
8383
public async getReferencesAtPosition(document: SkinnyTextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise<MdReference[]> {
8484
this.logger.verbose('ReferencesProvider', `getReferencesAtPosition: ${document.uri}`);
8585

86-
const toc = await this.tocProvider.get(document.uri);
86+
const toc = await this.tocProvider.getForDocument(document);
8787
if (token.isCancellationRequested) {
8888
return [];
8989
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ export class MdSmartSelect implements vscode.SelectionRangeProvider {
5353
}
5454

5555
private async getHeaderSelectionRange(document: SkinnyTextDocument, position: vscode.Position): Promise<vscode.SelectionRange | undefined> {
56-
const toc = await this.tocProvider.get(document.uri);
56+
const toc = await this.tocProvider.getForDocument(document);
5757

5858
const headerInfo = getHeadersForPosition(toc.entries, position);
5959

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,14 @@
66
import type MarkdownIt = require('markdown-it');
77
import type Token = require('markdown-it/lib/token');
88
import * as vscode from 'vscode';
9-
import { MdDocumentInfoCache } from './util/workspaceCache';
9+
import { ILogger } from './logging';
1010
import { MarkdownContributionProvider } from './markdownExtensions';
1111
import { Slugifier } from './slugify';
1212
import { Disposable } from './util/dispose';
1313
import { stringHash } from './util/hash';
1414
import { WebviewResourceProvider } from './util/resources';
1515
import { isOfScheme, Schemes } from './util/schemes';
16+
import { MdDocumentInfoCache } from './util/workspaceCache';
1617
import { MdWorkspaceContents, SkinnyTextDocument } from './workspaceContents';
1718

1819
const UNICODE_NEWLINE_REGEX = /\u2028|\u2029/g;
@@ -95,6 +96,7 @@ interface RenderEnv {
9596

9697
export interface IMdParser {
9798
readonly slugifier: Slugifier;
99+
98100
tokenize(document: SkinnyTextDocument): Promise<Token[]>;
99101
}
100102

@@ -110,6 +112,7 @@ export class MarkdownItEngine implements IMdParser {
110112
public constructor(
111113
private readonly contributionProvider: MarkdownContributionProvider,
112114
slugifier: Slugifier,
115+
private readonly logger: ILogger,
113116
) {
114117
this.slugifier = slugifier;
115118

@@ -180,6 +183,7 @@ export class MarkdownItEngine implements IMdParser {
180183
return cached;
181184
}
182185

186+
this.logger.verbose('MarkdownItEngine', `tokenizeDocument - ${document.uri}`);
183187
const tokens = this.tokenizeString(document.getText(), engine);
184188
this._tokenCache.update(document, config, tokens);
185189
return tokens;

extensions/markdown-language-features/src/test/engine.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { MarkdownItEngine } from '../markdownEngine';
88
import { MarkdownContributionProvider, MarkdownContributions } from '../markdownExtensions';
99
import { githubSlugifier } from '../slugify';
1010
import { Disposable } from '../util/dispose';
11+
import { nulLogger } from './nulLogging';
1112

1213
const emptyContributions = new class extends Disposable implements MarkdownContributionProvider {
1314
readonly extensionUri = vscode.Uri.file('/');
@@ -16,5 +17,5 @@ const emptyContributions = new class extends Disposable implements MarkdownContr
1617
};
1718

1819
export function createNewMarkdownEngine(): MarkdownItEngine {
19-
return new MarkdownItEngine(emptyContributions, githubSlugifier);
20+
return new MarkdownItEngine(emptyContributions, githubSlugifier, nulLogger);
2021
}

extensions/markdown-language-features/src/test/tableOfContentsWatcher.ts renamed to extensions/markdown-language-features/src/util/tableOfContentsWatcher.ts

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@
55

66
import * as vscode from 'vscode';
77
import { MdTableOfContentsProvider, TableOfContents } from '../tableOfContents';
8-
import { equals } from '../util/arrays';
9-
import { Disposable } from '../util/dispose';
10-
import { ResourceMap } from '../util/resourceMap';
8+
import { equals } from './arrays';
9+
import { Disposable } from './dispose';
10+
import { ResourceMap } from './resourceMap';
1111
import { MdWorkspaceContents, SkinnyTextDocument } from '../workspaceContents';
12+
import { Delayer } from './async';
1213

1314
/**
1415
* Check if the items in a table of contents have changed.
@@ -27,15 +28,22 @@ export class MdTableOfContentsWatcher extends Disposable {
2728
readonly toc: TableOfContents;
2829
}>();
2930

31+
private readonly _pending = new ResourceMap<void>();
32+
3033
private readonly _onTocChanged = this._register(new vscode.EventEmitter<{ readonly uri: vscode.Uri }>);
3134
public readonly onTocChanged = this._onTocChanged.event;
3235

36+
private readonly delayer: Delayer<void>;
37+
3338
public constructor(
3439
private readonly workspaceContents: MdWorkspaceContents,
3540
private readonly tocProvider: MdTableOfContentsProvider,
41+
private readonly delay: number,
3642
) {
3743
super();
3844

45+
this.delayer = this._register(new Delayer<void>(delay));
46+
3947
this._register(this.workspaceContents.onDidChangeMarkdownDocument(this.onDidChangeDocument, this));
4048
this._register(this.workspaceContents.onDidCreateMarkdownDocument(this.onDidCreateDocument, this));
4149
this._register(this.workspaceContents.onDidDeleteMarkdownDocument(this.onDidDeleteDocument, this));
@@ -47,17 +55,34 @@ export class MdTableOfContentsWatcher extends Disposable {
4755
}
4856

4957
private async onDidChangeDocument(document: SkinnyTextDocument) {
50-
const existing = this._files.get(document.uri);
51-
const newToc = await this.tocProvider.getForDocument(document);
52-
53-
if (!existing || hasTableOfContentsChanged(existing.toc, newToc)) {
54-
this._onTocChanged.fire({ uri: document.uri });
58+
if (this.delay > 0) {
59+
this._pending.set(document.uri);
60+
this.delayer.trigger(() => this.flushPending());
61+
} else {
62+
this.updateForResource(document.uri);
5563
}
56-
57-
this._files.set(document.uri, { toc: newToc });
5864
}
5965

6066
private onDidDeleteDocument(resource: vscode.Uri) {
6167
this._files.delete(resource);
68+
this._pending.delete(resource);
69+
}
70+
71+
private async flushPending() {
72+
const pending = [...this._pending.keys()];
73+
this._pending.clear();
74+
75+
return Promise.all(pending.map(resource => this.updateForResource(resource)));
76+
}
77+
78+
private async updateForResource(resource: vscode.Uri) {
79+
const existing = this._files.get(resource);
80+
const newToc = await this.tocProvider.get(resource);
81+
82+
if (!existing || hasTableOfContentsChanged(existing.toc, newToc)) {
83+
this._onTocChanged.fire({ uri: resource });
84+
}
85+
86+
this._files.set(resource, { toc: newToc });
6287
}
6388
}

0 commit comments

Comments
 (0)