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

@@ -6,10 +6,13 @@
import * as assert from 'assert';
import 'mocha';
import * as vscode from 'vscode';
import { DiagnosticComputer, DiagnosticConfiguration, DiagnosticLevel, DiagnosticManager, DiagnosticOptions } from '../languageFeatures/diagnostics';
import { DiagnosticCollectionReporter, DiagnosticComputer, DiagnosticConfiguration, DiagnosticLevel, DiagnosticManager, DiagnosticOptions, DiagnosticReporter } from '../languageFeatures/diagnostics';
import { MdLinkComputer } from '../languageFeatures/documentLinkProvider';
import { MdReferencesProvider } from '../languageFeatures/references';
import { githubSlugifier } from '../slugify';
import { noopToken } from '../util/cancellation';
import { InMemoryDocument } from '../util/inMemoryDocument';
import { ResourceMap } from '../util/resourceMap';
import { MdWorkspaceContents } from '../workspaceContents';
import { createNewMarkdownEngine } from './engine';
import { InMemoryWorkspaceMarkdownDocuments } from './inMemoryWorkspace';
@@ -32,10 +35,22 @@ async function getComputedDiagnostics(doc: InMemoryDocument, workspaceContents:
).diagnostics;
}
function createDiagnosticsManager(workspaceContents: MdWorkspaceContents, configuration = new MemoryDiagnosticConfiguration({})) {
function createDiagnosticsManager(
workspaceContents: MdWorkspaceContents,
configuration = new MemoryDiagnosticConfiguration({}),
reporter: DiagnosticReporter = new DiagnosticCollectionReporter(),
) {
const engine = createNewMarkdownEngine();
const linkComputer = new MdLinkComputer(engine);
return new DiagnosticManager(new DiagnosticComputer(engine, workspaceContents, linkComputer), configuration);
const referencesProvider = new MdReferencesProvider(linkComputer, workspaceContents, engine, githubSlugifier);
return new DiagnosticManager(
engine,
workspaceContents,
new DiagnosticComputer(engine, workspaceContents, linkComputer),
configuration,
reporter,
referencesProvider,
0);
}
function assertDiagnosticsEqual(actual: readonly vscode.Diagnostic[], expectedRanges: readonly vscode.Range[]) {
@@ -72,6 +87,29 @@ class MemoryDiagnosticConfiguration implements DiagnosticConfiguration {
}
}
class MemoryDiagnosticReporter extends DiagnosticReporter {
public readonly diagnostics = new ResourceMap<readonly vscode.Diagnostic[]>();
override dispose(): void {
super.clear();
this.clear();
}
override clear(): void {
super.clear();
this.diagnostics.clear();
}
set(uri: vscode.Uri, diagnostics: readonly vscode.Diagnostic[]): void {
this.diagnostics.set(uri, diagnostics);
}
override delete(uri: vscode.Uri): void {
super.delete(uri);
this.diagnostics.delete(uri);
}
}
suite('markdown: Diagnostics', () => {
test('Should not return any diagnostics for empty document', async () => {
@@ -387,6 +425,64 @@ suite('markdown: Diagnostics', () => {
new vscode.Range(3, 14, 3, 22),
]);
});
test('Should revalidate linked files when header changes', async () => {
const doc1Uri = workspacePath('doc1.md');
const doc1 = new InMemoryDocument(doc1Uri, joinLines(
`[text](#no-such)`,
`[text](/doc2.md#header)`,
));
const doc2Uri = workspacePath('doc2.md');
const doc2 = new InMemoryDocument(doc2Uri, joinLines(
`# Header`,
`[text](#header)`,
`[text](#no-such-2)`,
));
const contents = new InMemoryWorkspaceMarkdownDocuments([doc1, doc2]);
const reporter = new MemoryDiagnosticReporter();
const manager = createDiagnosticsManager(contents, new MemoryDiagnosticConfiguration({}), reporter);
await manager.ready;
// Check initial state
await reporter.waitAllPending();
assertDiagnosticsEqual(reporter.diagnostics.get(doc1Uri)!, [
new vscode.Range(0, 7, 0, 15),
]);
assertDiagnosticsEqual(reporter.diagnostics.get(doc2Uri)!, [
new vscode.Range(2, 7, 2, 17),
]);
// Edit header
contents.updateDocument(new InMemoryDocument(doc2Uri, joinLines(
`# new header`,
`[text](#new-header)`,
`[text](#no-such-2)`,
)));
await reporter.waitAllPending();
assertDiagnosticsEqual(orderDiagnosticsByRange(reporter.diagnostics.get(doc1Uri)!), [
new vscode.Range(0, 7, 0, 15),
new vscode.Range(1, 15, 1, 22),
]);
assertDiagnosticsEqual(reporter.diagnostics.get(doc2Uri)!, [
new vscode.Range(2, 7, 2, 17),
]);
// Revert to original file
contents.updateDocument(new InMemoryDocument(doc2Uri, joinLines(
`# header`,
`[text](#header)`,
`[text](#no-such-2)`,
)));
await reporter.waitAllPending();
assertDiagnosticsEqual(orderDiagnosticsByRange(reporter.diagnostics.get(doc1Uri)!), [
new vscode.Range(0, 7, 0, 15)
]);
assertDiagnosticsEqual(reporter.diagnostics.get(doc2Uri)!, [
new vscode.Range(2, 7, 2, 17),
]);
});
});
function orderDiagnosticsByRange(diagnostics: Iterable<vscode.Diagnostic>): readonly vscode.Diagnostic[] {

View File

@@ -0,0 +1,64 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { MarkdownEngine } from '../markdownEngine';
import { TableOfContents } from '../tableOfContents';
import { MdWorkspaceContents, SkinnyTextDocument } from '../workspaceContents';
import { equals } from '../util/arrays';
import { Disposable } from '../util/dispose';
import { ResourceMap } from '../util/resourceMap';
/**
* Check if the items in a table of contents have changed.
*
* This only checks for changes in the entries themselves, not for any changes in their locations.
*/
function hasTableOfContentsChanged(a: TableOfContents, b: TableOfContents): boolean {
const aSlugs = a.entries.map(entry => entry.slug.value).sort();
const bSlugs = b.entries.map(entry => entry.slug.value).sort();
return !equals(aSlugs, bSlugs);
}
export class MdTableOfContentsWatcher extends Disposable {
private readonly _files = new ResourceMap<{
readonly toc: TableOfContents;
}>();
private readonly _onTocChanged = this._register(new vscode.EventEmitter<{ readonly uri: vscode.Uri }>);
public readonly onTocChanged = this._onTocChanged.event;
public constructor(
private readonly engine: MarkdownEngine,
private readonly workspaceContents: MdWorkspaceContents,
) {
super();
this._register(this.workspaceContents.onDidChangeMarkdownDocument(this.onDidChangeDocument, this));
this._register(this.workspaceContents.onDidCreateMarkdownDocument(this.onDidCreateDocument, this));
this._register(this.workspaceContents.onDidDeleteMarkdownDocument(this.onDidDeleteDocument, this));
}
private async onDidCreateDocument(document: SkinnyTextDocument) {
const toc = await TableOfContents.create(this.engine, document);
this._files.set(document.uri, { toc });
}
private async onDidChangeDocument(document: SkinnyTextDocument) {
const existing = this._files.get(document.uri);
const newToc = await TableOfContents.create(this.engine, document);
if (!existing || hasTableOfContentsChanged(existing.toc, newToc)) {
this._onTocChanged.fire({ uri: document.uri });
}
this._files.set(document.uri, { toc: newToc });
}
private onDidDeleteDocument(resource: vscode.Uri) {
this._files.delete(resource);
}
}