Revalidate linked files on header change (#152366)

Fixes #150945

With this change, when the headers in a file change, we should also try to revalidate all files that link to it
This commit is contained in:
Matt Bierner
2022-06-16 11:14:01 -07:00
committed by GitHub
parent cb18a4870c
commit 6c252851f2
4 changed files with 286 additions and 50 deletions

View File

@@ -10,13 +10,15 @@ import { CommandManager } from '../commandManager';
import { MarkdownEngine } from '../markdownEngine';
import { TableOfContents } from '../tableOfContents';
import { Delayer } from '../util/async';
import { noopToken } from '../util/cancellation';
import { Disposable } from '../util/dispose';
import { isMarkdownFile } from '../util/file';
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 { tryFindMdDocumentForLink } from './references';
import { MdReferencesProvider, tryFindMdDocumentForLink } from './references';
const localize = nls.loadMessageBundle();
@@ -93,19 +95,21 @@ class InflightDiagnosticRequests {
private readonly inFlightRequests = new ResourceMap<{ readonly cts: vscode.CancellationTokenSource }>();
public trigger(resource: vscode.Uri, compute: (token: vscode.CancellationToken) => Promise<void>) {
public async trigger(resource: vscode.Uri, compute: (token: vscode.CancellationToken) => Promise<void>): Promise<void> {
this.cancel(resource);
const cts = new vscode.CancellationTokenSource();
const entry = { cts };
this.inFlightRequests.set(resource, entry);
compute(cts.token).finally(() => {
try {
return await compute(cts.token);
} finally {
if (this.inFlightRequests.get(resource) === entry) {
this.inFlightRequests.delete(resource);
}
cts.dispose();
});
}
}
public cancel(resource: vscode.Uri) {
@@ -226,55 +230,136 @@ class LinkDoesNotExistDiagnostic extends vscode.Diagnostic {
}
}
export class DiagnosticManager extends Disposable {
export abstract class DiagnosticReporter extends Disposable {
private readonly pending = new ResourceMap<Promise<any>>();
public clear(): void {
this.pending.clear();
}
public abstract set(uri: vscode.Uri, diagnostics: readonly vscode.Diagnostic[]): void;
public delete(uri: vscode.Uri): void {
this.pending.delete(uri);
}
public signalTriggered(uri: vscode.Uri, recompute: Promise<any>): void {
this.pending.set(uri, recompute);
recompute.finally(() => {
if (this.pending.get(uri) === recompute) {
this.pending.delete(uri);
}
});
}
public async waitAllPending(): Promise<void> {
await Promise.all([...this.pending.values()]);
}
}
export class DiagnosticCollectionReporter extends DiagnosticReporter {
private readonly collection: vscode.DiagnosticCollection;
constructor() {
super();
this.collection = this._register(vscode.languages.createDiagnosticCollection('markdown'));
}
public override clear(): void {
super.clear();
this.collection.clear();
}
public set(uri: vscode.Uri, diagnostics: readonly vscode.Diagnostic[]): void {
const tabs = this.getAllTabResources();
this.collection.set(uri, tabs.has(uri) ? diagnostics : []);
}
public override delete(uri: vscode.Uri): void {
super.delete(uri);
this.collection.delete(uri);
}
private getAllTabResources(): ResourceMap<void> {
const openedTabDocs = new ResourceMap<void>();
for (const group of vscode.window.tabGroups.all) {
for (const tab of group.tabs) {
if (tab.input instanceof vscode.TabInputText) {
openedTabDocs.set(tab.input.uri);
}
}
}
return openedTabDocs;
}
}
export class DiagnosticManager extends Disposable {
private readonly diagnosticDelayer: Delayer<void>;
private readonly pendingDiagnostics = new Set<vscode.Uri>();
private readonly inFlightDiagnostics = this._register(new InflightDiagnosticRequests());
private readonly linkWatcher = this._register(new LinkWatcher());
private readonly tableOfContentsWatcher: MdTableOfContentsWatcher;
public readonly ready: Promise<void>;
constructor(
engine: MarkdownEngine,
private readonly workspaceContents: MdWorkspaceContents,
private readonly computer: DiagnosticComputer,
private readonly configuration: DiagnosticConfiguration,
private readonly reporter: DiagnosticReporter,
private readonly referencesProvider: MdReferencesProvider,
delay = 300,
) {
super();
this.diagnosticDelayer = this._register(new Delayer(300));
this.collection = this._register(vscode.languages.createDiagnosticCollection('markdown'));
this.diagnosticDelayer = this._register(new Delayer(delay));
this._register(this.configuration.onDidChange(() => {
this.rebuild();
}));
this._register(vscode.workspace.onDidOpenTextDocument(doc => {
this.triggerDiagnostics(doc);
this._register(workspaceContents.onDidCreateMarkdownDocument(doc => {
this.triggerDiagnostics(doc.uri);
}));
this._register(vscode.workspace.onDidChangeTextDocument(e => {
this.triggerDiagnostics(e.document);
this._register(workspaceContents.onDidChangeMarkdownDocument(doc => {
this.triggerDiagnostics(doc.uri);
}));
this._register(vscode.workspace.onDidCloseTextDocument(({ uri }) => {
this.pendingDiagnostics.delete(uri);
this.inFlightDiagnostics.cancel(uri);
this.linkWatcher.deleteDocument(uri);
this.collection.delete(uri);
this.reporter.delete(uri);
}));
this._register(this.linkWatcher.onDidChangeLinkedToFile(changedDocuments => {
for (const resource of changedDocuments) {
const doc = vscode.workspace.textDocuments.find(doc => doc.uri.toString() === resource.toString());
if (doc) {
this.triggerDiagnostics(doc);
if (doc && isMarkdownFile(doc)) {
this.triggerDiagnostics(doc.uri);
}
}
}));
this.rebuild();
this.tableOfContentsWatcher = this._register(new MdTableOfContentsWatcher(engine, workspaceContents));
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.referencesProvider.getAllReferencesToFile(e.uri, noopToken)) {
const file = ref.location.uri;
if (!triggered.has(file)) {
this.triggerDiagnostics(file);
triggered.set(file);
}
}
}));
this.ready = this.rebuild();
}
public override dispose() {
@@ -294,49 +379,33 @@ export class DiagnosticManager extends Disposable {
const pending = [...this.pendingDiagnostics];
this.pendingDiagnostics.clear();
for (const resource of pending) {
const doc = vscode.workspace.textDocuments.find(doc => doc.uri.fsPath === resource.fsPath);
await Promise.all(pending.map(async resource => {
const doc = await this.workspaceContents.getMarkdownDocument(resource);
if (doc) {
this.inFlightDiagnostics.trigger(doc.uri, async (token) => {
await this.inFlightDiagnostics.trigger(doc.uri, async (token) => {
const state = await this.recomputeDiagnosticState(doc, token);
this.linkWatcher.updateLinksForDocument(doc.uri, state.config.enabled && state.config.validateFileLinks ? state.links : []);
this.collection.set(doc.uri, state.diagnostics);
this.reporter.set(doc.uri, state.diagnostics);
});
}
}
}));
}
private async rebuild() {
this.collection.clear();
this.reporter.clear();
this.pendingDiagnostics.clear();
this.inFlightDiagnostics.clear();
const allOpenedTabResources = this.getAllTabResources();
await Promise.all(
vscode.workspace.textDocuments
.filter(doc => allOpenedTabResources.has(doc.uri) && isMarkdownFile(doc))
.map(doc => this.triggerDiagnostics(doc)));
for (const doc of await this.workspaceContents.getAllMarkdownDocuments()) {
this.triggerDiagnostics(doc.uri);
}
}
private getAllTabResources(): ResourceMap<void> {
const openedTabDocs = new ResourceMap<void>();
for (const group of vscode.window.tabGroups.all) {
for (const tab of group.tabs) {
if (tab.input instanceof vscode.TabInputText) {
openedTabDocs.set(tab.input.uri);
}
}
}
return openedTabDocs;
}
private triggerDiagnostics(uri: vscode.Uri) {
this.inFlightDiagnostics.cancel(uri);
private triggerDiagnostics(doc: vscode.TextDocument) {
this.inFlightDiagnostics.cancel(doc.uri);
if (isMarkdownFile(doc)) {
this.pendingDiagnostics.add(doc.uri);
this.diagnosticDelayer.trigger(() => this.recomputePendingDiagnostics());
}
this.pendingDiagnostics.add(uri);
this.reporter.signalTriggered(uri, this.diagnosticDelayer.trigger(() => this.recomputePendingDiagnostics()));
}
}
@@ -558,9 +627,16 @@ export function register(
workspaceContents: MdWorkspaceContents,
linkComputer: MdLinkComputer,
commandManager: CommandManager,
referenceProvider: MdReferencesProvider,
): vscode.Disposable {
const configuration = new VSCodeDiagnosticConfiguration();
const manager = new DiagnosticManager(new DiagnosticComputer(engine, workspaceContents, linkComputer), configuration);
const manager = new DiagnosticManager(
engine,
workspaceContents,
new DiagnosticComputer(engine, workspaceContents, linkComputer),
configuration,
new DiagnosticCollectionReporter(),
referenceProvider);
return vscode.Disposable.from(
configuration,
manager,