mirror of
https://github.com/microsoft/vscode.git
synced 2025-12-20 10:19:02 +00:00
Reduce recomputation of state in markdown extension (#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
This commit is contained in:
@@ -44,7 +44,7 @@ export function activate(context: vscode.ExtensionContext) {
|
|||||||
const cspArbiter = new ExtensionContentSecurityPolicyArbiter(context.globalState, context.workspaceState);
|
const cspArbiter = new ExtensionContentSecurityPolicyArbiter(context.globalState, context.workspaceState);
|
||||||
const commandManager = new CommandManager();
|
const commandManager = new CommandManager();
|
||||||
|
|
||||||
const engine = new MarkdownItEngine(contributions, githubSlugifier);
|
const engine = new MarkdownItEngine(contributions, githubSlugifier, logger);
|
||||||
const workspaceContents = new VsCodeMdWorkspaceContents();
|
const workspaceContents = new VsCodeMdWorkspaceContents();
|
||||||
const parser = new MdParsingProvider(engine, workspaceContents);
|
const parser = new MdParsingProvider(engine, workspaceContents);
|
||||||
const tocProvider = new MdTableOfContentsProvider(parser, workspaceContents, logger);
|
const tocProvider = new MdTableOfContentsProvider(parser, workspaceContents, logger);
|
||||||
|
|||||||
@@ -9,13 +9,13 @@ import * as nls from 'vscode-nls';
|
|||||||
import { CommandManager } from '../commandManager';
|
import { CommandManager } from '../commandManager';
|
||||||
import { ILogger } from '../logging';
|
import { ILogger } from '../logging';
|
||||||
import { MdTableOfContentsProvider } from '../tableOfContents';
|
import { MdTableOfContentsProvider } from '../tableOfContents';
|
||||||
import { MdTableOfContentsWatcher } from '../test/tableOfContentsWatcher';
|
|
||||||
import { Delayer } from '../util/async';
|
import { Delayer } from '../util/async';
|
||||||
import { noopToken } from '../util/cancellation';
|
import { noopToken } from '../util/cancellation';
|
||||||
import { Disposable } from '../util/dispose';
|
import { Disposable } from '../util/dispose';
|
||||||
import { isMarkdownFile, looksLikeMarkdownPath } from '../util/file';
|
import { isMarkdownFile, looksLikeMarkdownPath } from '../util/file';
|
||||||
import { Limiter } from '../util/limiter';
|
import { Limiter } from '../util/limiter';
|
||||||
import { ResourceMap } from '../util/resourceMap';
|
import { ResourceMap } from '../util/resourceMap';
|
||||||
|
import { MdTableOfContentsWatcher } from '../util/tableOfContentsWatcher';
|
||||||
import { MdWorkspaceContents, SkinnyTextDocument } from '../workspaceContents';
|
import { MdWorkspaceContents, SkinnyTextDocument } from '../workspaceContents';
|
||||||
import { InternalHref, LinkDefinitionSet, MdLink, MdLinkProvider, MdLinkSource } from './documentLinks';
|
import { InternalHref, LinkDefinitionSet, MdLink, MdLinkProvider, MdLinkSource } from './documentLinks';
|
||||||
import { MdReferencesProvider, tryResolveLinkPath } from './references';
|
import { MdReferencesProvider, tryResolveLinkPath } from './references';
|
||||||
@@ -347,7 +347,7 @@ export class DiagnosticManager extends Disposable {
|
|||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
this.tableOfContentsWatcher = this._register(new MdTableOfContentsWatcher(workspaceContents, tocProvider));
|
this.tableOfContentsWatcher = this._register(new MdTableOfContentsWatcher(workspaceContents, tocProvider, delay));
|
||||||
this._register(this.tableOfContentsWatcher.onTocChanged(async e => {
|
this._register(this.tableOfContentsWatcher.onTocChanged(async e => {
|
||||||
// When the toc of a document changes, revalidate every file that linked to it too
|
// When the toc of a document changes, revalidate every file that linked to it too
|
||||||
const triggered = new ResourceMap<void>();
|
const triggered = new ResourceMap<void>();
|
||||||
@@ -491,7 +491,7 @@ export class DiagnosticComputer {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const toc = await this.tocProvider.get(doc.uri);
|
const toc = await this.tocProvider.getForDocument(doc);
|
||||||
if (token.isCancellationRequested) {
|
if (token.isCancellationRequested) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -427,12 +427,17 @@ export class MdLinkComputer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface MdDocumentLinks {
|
||||||
|
readonly links: readonly MdLink[];
|
||||||
|
readonly definitions: LinkDefinitionSet;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stateful object which provides links for markdown files the workspace.
|
* Stateful object which provides links for markdown files the workspace.
|
||||||
*/
|
*/
|
||||||
export class MdLinkProvider extends Disposable {
|
export class MdLinkProvider extends Disposable {
|
||||||
|
|
||||||
private readonly _linkCache: MdDocumentInfoCache<readonly MdLink[]>;
|
private readonly _linkCache: MdDocumentInfoCache<MdDocumentLinks>;
|
||||||
|
|
||||||
private readonly linkComputer: MdLinkComputer;
|
private readonly linkComputer: MdLinkComputer;
|
||||||
|
|
||||||
@@ -443,21 +448,19 @@ export class MdLinkProvider extends Disposable {
|
|||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
this.linkComputer = new MdLinkComputer(tokenizer);
|
this.linkComputer = new MdLinkComputer(tokenizer);
|
||||||
this._linkCache = this._register(new MdDocumentInfoCache(workspaceContents, doc => {
|
this._linkCache = this._register(new MdDocumentInfoCache(workspaceContents, async doc => {
|
||||||
logger.verbose('LinkProvider', `compute - ${doc.uri}`);
|
logger.verbose('LinkProvider', `compute - ${doc.uri}`);
|
||||||
return this.linkComputer.getAllLinks(doc, noopToken);
|
|
||||||
|
const links = await this.linkComputer.getAllLinks(doc, noopToken);
|
||||||
|
return {
|
||||||
|
links,
|
||||||
|
definitions: new LinkDefinitionSet(links),
|
||||||
|
};
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getLinks(document: SkinnyTextDocument): Promise<{
|
public async getLinks(document: SkinnyTextDocument): Promise<MdDocumentLinks> {
|
||||||
readonly links: readonly MdLink[];
|
return this._linkCache.getForDocument(document);
|
||||||
readonly definitions: LinkDefinitionSet;
|
|
||||||
}> {
|
|
||||||
const links = (await this._linkCache.get(document.uri)) ?? [];
|
|
||||||
return {
|
|
||||||
links,
|
|
||||||
definitions: new LinkDefinitionSet(links),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export class MdFoldingProvider implements vscode.FoldingRangeProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async getHeaderFoldingRanges(document: SkinnyTextDocument): Promise<vscode.FoldingRange[]> {
|
private async getHeaderFoldingRanges(document: SkinnyTextDocument): Promise<vscode.FoldingRange[]> {
|
||||||
const toc = await this.tocProvide.get(document.uri);
|
const toc = await this.tocProvide.getForDocument(document);
|
||||||
return toc.entries.map(entry => {
|
return toc.entries.map(entry => {
|
||||||
let endLine = entry.sectionLocation.range.end.line;
|
let endLine = entry.sectionLocation.range.end.line;
|
||||||
if (document.lineAt(endLine).isEmptyOrWhitespace && endLine >= entry.line + 1) {
|
if (document.lineAt(endLine).isEmptyOrWhitespace && endLine >= entry.line + 1) {
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ export class MdReferencesProvider extends Disposable {
|
|||||||
public async getReferencesAtPosition(document: SkinnyTextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise<MdReference[]> {
|
public async getReferencesAtPosition(document: SkinnyTextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise<MdReference[]> {
|
||||||
this.logger.verbose('ReferencesProvider', `getReferencesAtPosition: ${document.uri}`);
|
this.logger.verbose('ReferencesProvider', `getReferencesAtPosition: ${document.uri}`);
|
||||||
|
|
||||||
const toc = await this.tocProvider.get(document.uri);
|
const toc = await this.tocProvider.getForDocument(document);
|
||||||
if (token.isCancellationRequested) {
|
if (token.isCancellationRequested) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export class MdSmartSelect implements vscode.SelectionRangeProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async getHeaderSelectionRange(document: SkinnyTextDocument, position: vscode.Position): Promise<vscode.SelectionRange | undefined> {
|
private async getHeaderSelectionRange(document: SkinnyTextDocument, position: vscode.Position): Promise<vscode.SelectionRange | undefined> {
|
||||||
const toc = await this.tocProvider.get(document.uri);
|
const toc = await this.tocProvider.getForDocument(document);
|
||||||
|
|
||||||
const headerInfo = getHeadersForPosition(toc.entries, position);
|
const headerInfo = getHeadersForPosition(toc.entries, position);
|
||||||
|
|
||||||
|
|||||||
@@ -6,13 +6,14 @@
|
|||||||
import type MarkdownIt = require('markdown-it');
|
import type MarkdownIt = require('markdown-it');
|
||||||
import type Token = require('markdown-it/lib/token');
|
import type Token = require('markdown-it/lib/token');
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
import { MdDocumentInfoCache } from './util/workspaceCache';
|
import { ILogger } from './logging';
|
||||||
import { MarkdownContributionProvider } from './markdownExtensions';
|
import { MarkdownContributionProvider } from './markdownExtensions';
|
||||||
import { Slugifier } from './slugify';
|
import { Slugifier } from './slugify';
|
||||||
import { Disposable } from './util/dispose';
|
import { Disposable } from './util/dispose';
|
||||||
import { stringHash } from './util/hash';
|
import { stringHash } from './util/hash';
|
||||||
import { WebviewResourceProvider } from './util/resources';
|
import { WebviewResourceProvider } from './util/resources';
|
||||||
import { isOfScheme, Schemes } from './util/schemes';
|
import { isOfScheme, Schemes } from './util/schemes';
|
||||||
|
import { MdDocumentInfoCache } from './util/workspaceCache';
|
||||||
import { MdWorkspaceContents, SkinnyTextDocument } from './workspaceContents';
|
import { MdWorkspaceContents, SkinnyTextDocument } from './workspaceContents';
|
||||||
|
|
||||||
const UNICODE_NEWLINE_REGEX = /\u2028|\u2029/g;
|
const UNICODE_NEWLINE_REGEX = /\u2028|\u2029/g;
|
||||||
@@ -95,6 +96,7 @@ interface RenderEnv {
|
|||||||
|
|
||||||
export interface IMdParser {
|
export interface IMdParser {
|
||||||
readonly slugifier: Slugifier;
|
readonly slugifier: Slugifier;
|
||||||
|
|
||||||
tokenize(document: SkinnyTextDocument): Promise<Token[]>;
|
tokenize(document: SkinnyTextDocument): Promise<Token[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,6 +112,7 @@ export class MarkdownItEngine implements IMdParser {
|
|||||||
public constructor(
|
public constructor(
|
||||||
private readonly contributionProvider: MarkdownContributionProvider,
|
private readonly contributionProvider: MarkdownContributionProvider,
|
||||||
slugifier: Slugifier,
|
slugifier: Slugifier,
|
||||||
|
private readonly logger: ILogger,
|
||||||
) {
|
) {
|
||||||
this.slugifier = slugifier;
|
this.slugifier = slugifier;
|
||||||
|
|
||||||
@@ -180,6 +183,7 @@ export class MarkdownItEngine implements IMdParser {
|
|||||||
return cached;
|
return cached;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.logger.verbose('MarkdownItEngine', `tokenizeDocument - ${document.uri}`);
|
||||||
const tokens = this.tokenizeString(document.getText(), engine);
|
const tokens = this.tokenizeString(document.getText(), engine);
|
||||||
this._tokenCache.update(document, config, tokens);
|
this._tokenCache.update(document, config, tokens);
|
||||||
return tokens;
|
return tokens;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { MarkdownItEngine } from '../markdownEngine';
|
|||||||
import { MarkdownContributionProvider, MarkdownContributions } from '../markdownExtensions';
|
import { MarkdownContributionProvider, MarkdownContributions } from '../markdownExtensions';
|
||||||
import { githubSlugifier } from '../slugify';
|
import { githubSlugifier } from '../slugify';
|
||||||
import { Disposable } from '../util/dispose';
|
import { Disposable } from '../util/dispose';
|
||||||
|
import { nulLogger } from './nulLogging';
|
||||||
|
|
||||||
const emptyContributions = new class extends Disposable implements MarkdownContributionProvider {
|
const emptyContributions = new class extends Disposable implements MarkdownContributionProvider {
|
||||||
readonly extensionUri = vscode.Uri.file('/');
|
readonly extensionUri = vscode.Uri.file('/');
|
||||||
@@ -16,5 +17,5 @@ const emptyContributions = new class extends Disposable implements MarkdownContr
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function createNewMarkdownEngine(): MarkdownItEngine {
|
export function createNewMarkdownEngine(): MarkdownItEngine {
|
||||||
return new MarkdownItEngine(emptyContributions, githubSlugifier);
|
return new MarkdownItEngine(emptyContributions, githubSlugifier, nulLogger);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,10 +5,11 @@
|
|||||||
|
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
import { MdTableOfContentsProvider, TableOfContents } from '../tableOfContents';
|
import { MdTableOfContentsProvider, TableOfContents } from '../tableOfContents';
|
||||||
import { equals } from '../util/arrays';
|
import { equals } from './arrays';
|
||||||
import { Disposable } from '../util/dispose';
|
import { Disposable } from './dispose';
|
||||||
import { ResourceMap } from '../util/resourceMap';
|
import { ResourceMap } from './resourceMap';
|
||||||
import { MdWorkspaceContents, SkinnyTextDocument } from '../workspaceContents';
|
import { MdWorkspaceContents, SkinnyTextDocument } from '../workspaceContents';
|
||||||
|
import { Delayer } from './async';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if the items in a table of contents have changed.
|
* Check if the items in a table of contents have changed.
|
||||||
@@ -27,15 +28,22 @@ export class MdTableOfContentsWatcher extends Disposable {
|
|||||||
readonly toc: TableOfContents;
|
readonly toc: TableOfContents;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
private readonly _pending = new ResourceMap<void>();
|
||||||
|
|
||||||
private readonly _onTocChanged = this._register(new vscode.EventEmitter<{ readonly uri: vscode.Uri }>);
|
private readonly _onTocChanged = this._register(new vscode.EventEmitter<{ readonly uri: vscode.Uri }>);
|
||||||
public readonly onTocChanged = this._onTocChanged.event;
|
public readonly onTocChanged = this._onTocChanged.event;
|
||||||
|
|
||||||
|
private readonly delayer: Delayer<void>;
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
private readonly workspaceContents: MdWorkspaceContents,
|
private readonly workspaceContents: MdWorkspaceContents,
|
||||||
private readonly tocProvider: MdTableOfContentsProvider,
|
private readonly tocProvider: MdTableOfContentsProvider,
|
||||||
|
private readonly delay: number,
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
|
this.delayer = this._register(new Delayer<void>(delay));
|
||||||
|
|
||||||
this._register(this.workspaceContents.onDidChangeMarkdownDocument(this.onDidChangeDocument, this));
|
this._register(this.workspaceContents.onDidChangeMarkdownDocument(this.onDidChangeDocument, this));
|
||||||
this._register(this.workspaceContents.onDidCreateMarkdownDocument(this.onDidCreateDocument, this));
|
this._register(this.workspaceContents.onDidCreateMarkdownDocument(this.onDidCreateDocument, this));
|
||||||
this._register(this.workspaceContents.onDidDeleteMarkdownDocument(this.onDidDeleteDocument, this));
|
this._register(this.workspaceContents.onDidDeleteMarkdownDocument(this.onDidDeleteDocument, this));
|
||||||
@@ -47,17 +55,34 @@ export class MdTableOfContentsWatcher extends Disposable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async onDidChangeDocument(document: SkinnyTextDocument) {
|
private async onDidChangeDocument(document: SkinnyTextDocument) {
|
||||||
const existing = this._files.get(document.uri);
|
if (this.delay > 0) {
|
||||||
const newToc = await this.tocProvider.getForDocument(document);
|
this._pending.set(document.uri);
|
||||||
|
this.delayer.trigger(() => this.flushPending());
|
||||||
if (!existing || hasTableOfContentsChanged(existing.toc, newToc)) {
|
} else {
|
||||||
this._onTocChanged.fire({ uri: document.uri });
|
this.updateForResource(document.uri);
|
||||||
}
|
}
|
||||||
|
|
||||||
this._files.set(document.uri, { toc: newToc });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private onDidDeleteDocument(resource: vscode.Uri) {
|
private onDidDeleteDocument(resource: vscode.Uri) {
|
||||||
this._files.delete(resource);
|
this._files.delete(resource);
|
||||||
|
this._pending.delete(resource);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async flushPending() {
|
||||||
|
const pending = [...this._pending.keys()];
|
||||||
|
this._pending.clear();
|
||||||
|
|
||||||
|
return Promise.all(pending.map(resource => this.updateForResource(resource)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async updateForResource(resource: vscode.Uri) {
|
||||||
|
const existing = this._files.get(resource);
|
||||||
|
const newToc = await this.tocProvider.get(resource);
|
||||||
|
|
||||||
|
if (!existing || hasTableOfContentsChanged(existing.toc, newToc)) {
|
||||||
|
this._onTocChanged.fire({ uri: resource });
|
||||||
|
}
|
||||||
|
|
||||||
|
this._files.set(resource, { toc: newToc });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user