diff --git a/extensions/markdown-language-features/src/languageFeatures/diagnostics.ts b/extensions/markdown-language-features/src/languageFeatures/diagnostics.ts index d7a82d2aac3..bb54aabc101 100644 --- a/extensions/markdown-language-features/src/languageFeatures/diagnostics.ts +++ b/extensions/markdown-language-features/src/languageFeatures/diagnostics.ts @@ -242,7 +242,9 @@ export abstract class DiagnosticReporter extends Disposable { public abstract delete(uri: vscode.Uri): void; - public abstract areDiagnosticsEnabled(uri: vscode.Uri): boolean; + public abstract isOpen(uri: vscode.Uri): boolean; + + public abstract getOpenDocuments(): ITextDocument[]; public addWorkItem(promise: Promise): Promise { this.pending.add(promise); @@ -256,6 +258,7 @@ export abstract class DiagnosticReporter extends Disposable { } export class DiagnosticCollectionReporter extends DiagnosticReporter { + private readonly collection: vscode.DiagnosticCollection; constructor() { @@ -269,11 +272,11 @@ export class DiagnosticCollectionReporter extends DiagnosticReporter { } public set(uri: vscode.Uri, diagnostics: readonly vscode.Diagnostic[]): void { - this.collection.set(uri, this.areDiagnosticsEnabled(uri) ? diagnostics : []); + this.collection.set(uri, this.isOpen(uri) ? diagnostics : []); } - public areDiagnosticsEnabled(uri: vscode.Uri): boolean { - const tabs = this.getAllTabResources(); + public isOpen(uri: vscode.Uri): boolean { + const tabs = this.getTabResources(); return tabs.has(uri); } @@ -281,7 +284,12 @@ export class DiagnosticCollectionReporter extends DiagnosticReporter { this.collection.delete(uri); } - private getAllTabResources(): ResourceMap { + public getOpenDocuments(): ITextDocument[] { + const tabs = this.getTabResources(); + return vscode.workspace.textDocuments.filter(doc => tabs.has(doc.uri)); + } + + private getTabResources(): ResourceMap { const openedTabDocs = new ResourceMap(); for (const group of vscode.window.tabGroups.all) { for (const tab of group.tabs) { @@ -365,7 +373,7 @@ export class DiagnosticManager extends Disposable { return this.reporter.addWorkItem( (async () => { const triggered = new ResourceMap>(); - for (const ref of await this.referencesProvider.getAllReferencesToFile(uri, noopToken)) { + for (const ref of await this.referencesProvider.getReferencesToFileInDocs(uri, this.reporter.getOpenDocuments(), noopToken)) { const file = ref.location.uri; if (!triggered.has(file)) { triggered.set(file, this.triggerDiagnostics(file)); @@ -398,7 +406,7 @@ export class DiagnosticManager extends Disposable { const doc = await this.workspace.getOrLoadMarkdownDocument(resource); if (doc) { await this.inFlightDiagnostics.trigger(doc.uri, async (token) => { - if (this.reporter.areDiagnosticsEnabled(doc.uri)) { + if (this.reporter.isOpen(doc.uri)) { const state = await this.recomputeDiagnosticState(doc, token); this.linkWatcher.updateLinksForDocument(doc.uri, state.config.enabled && state.config.validateFileLinks ? state.links : []); this.reporter.set(doc.uri, state.diagnostics); @@ -417,12 +425,7 @@ export class DiagnosticManager extends Disposable { this.inFlightDiagnostics.clear(); return this.reporter.addWorkItem( - (async () => { - // TODO: This pulls in all md files in the workspace. Instead we only care about opened text documents. - // Need a new way to handle that. - const allDocs = await this.workspace.getAllMarkdownDocuments(); - await Promise.all(Array.from(allDocs, doc => this.triggerDiagnostics(doc.uri))); - })() + Promise.all(Array.from(this.reporter.getOpenDocuments(), doc => this.triggerDiagnostics(doc.uri))) ); } diff --git a/extensions/markdown-language-features/src/languageFeatures/fileReferences.ts b/extensions/markdown-language-features/src/languageFeatures/fileReferences.ts index 9a2ecb9aa8e..4eea510d812 100644 --- a/extensions/markdown-language-features/src/languageFeatures/fileReferences.ts +++ b/extensions/markdown-language-features/src/languageFeatures/fileReferences.ts @@ -33,7 +33,7 @@ export class FindFileReferencesCommand implements Command { location: vscode.ProgressLocation.Window, title: localize('progress.title', "Finding file references") }, async (_progress, token) => { - const references = await this.referencesProvider.getAllReferencesToFile(resource!, token); + const references = await this.referencesProvider.getReferencesToFileInWorkspace(resource!, token); const locations = references.map(ref => ref.location); const config = vscode.workspace.getConfiguration('references'); diff --git a/extensions/markdown-language-features/src/languageFeatures/references.ts b/extensions/markdown-language-features/src/languageFeatures/references.ts index a0e2de31915..b9fe0bef931 100644 --- a/extensions/markdown-language-features/src/languageFeatures/references.ts +++ b/extensions/markdown-language-features/src/languageFeatures/references.ts @@ -67,7 +67,6 @@ export type MdReference = MdLinkReference | MdHeaderReference; export class MdReferencesProvider extends Disposable { private readonly _linkCache: MdWorkspaceInfoCache; - private readonly _linkComputer: MdLinkComputer; public constructor( private readonly parser: IMdParser, @@ -77,8 +76,8 @@ export class MdReferencesProvider extends Disposable { ) { super(); - this._linkComputer = new MdLinkComputer(parser); - this._linkCache = this._register(new MdWorkspaceInfoCache(workspace, doc => this._linkComputer.getAllLinks(doc, noopToken))); + const linkComputer = new MdLinkComputer(parser); + this._linkCache = this._register(new MdWorkspaceInfoCache(workspace, doc => linkComputer.getAllLinks(doc, noopToken))); } public async getReferencesAtPosition(document: ITextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise { @@ -97,11 +96,26 @@ export class MdReferencesProvider extends Disposable { } } - public async getAllReferencesToFile(resource: vscode.Uri, _token: vscode.CancellationToken): Promise { - this.logger.verbose('ReferencesProvider', `getAllReferencesToFile: ${resource}`); + public async getReferencesToFileInWorkspace(resource: vscode.Uri, token: vscode.CancellationToken): Promise { + this.logger.verbose('ReferencesProvider', `getAllReferencesToFileInWorkspace: ${resource}`); const allLinksInWorkspace = (await this._linkCache.values()).flat(); - return Array.from(this.findAllLinksToFile(resource, allLinksInWorkspace, undefined)); + if (token.isCancellationRequested) { + return []; + } + + return Array.from(this.findLinksToFile(resource, allLinksInWorkspace, undefined)); + } + + public async getReferencesToFileInDocs(resource: vscode.Uri, otherDocs: readonly ITextDocument[], token: vscode.CancellationToken): Promise { + this.logger.verbose('ReferencesProvider', `getAllReferencesToFileInFiles: ${resource}`); + + const links = (await this._linkCache.getForDocs(otherDocs)).flat(); + if (token.isCancellationRequested) { + return []; + } + + return Array.from(this.findLinksToFile(resource, links, undefined)); } private async getReferencesToHeader(document: ITextDocument, header: TocEntry): Promise { @@ -137,7 +151,7 @@ export class MdReferencesProvider extends Disposable { } private async getReferencesToLinkAtPosition(document: ITextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise { - const docLinks = await this._linkComputer.getAllLinks(document, token); + const docLinks = (await this._linkCache.getForDocs([document]))[0]; for (const link of docLinks) { if (link.kind === 'definition') { @@ -223,7 +237,7 @@ export class MdReferencesProvider extends Disposable { } } } else { // Triggered on a link without a fragment so we only require matching the file and ignore fragments - references.push(...this.findAllLinksToFile(resolvedResource ?? sourceLink.href.path, allLinksInWorkspace, sourceLink)); + references.push(...this.findLinksToFile(resolvedResource ?? sourceLink.href.path, allLinksInWorkspace, sourceLink)); } return references; @@ -238,8 +252,8 @@ export class MdReferencesProvider extends Disposable { || uri.Utils.extname(href.path) === '' && href.path.with({ path: href.path.path + '.md' }).fsPath === targetDoc.fsPath; } - private *findAllLinksToFile(resource: vscode.Uri, allLinksInWorkspace: readonly MdLink[], sourceLink: MdLink | undefined): Iterable { - for (const link of allLinksInWorkspace) { + private *findLinksToFile(resource: vscode.Uri, links: readonly MdLink[], sourceLink: MdLink | undefined): Iterable { + for (const link of links) { if (link.href.kind !== 'internal' || !this.looksLikeLinkToDoc(link.href, resource)) { continue; } diff --git a/extensions/markdown-language-features/src/logging.ts b/extensions/markdown-language-features/src/logging.ts index b29758278f7..48a2f3a7709 100644 --- a/extensions/markdown-language-features/src/logging.ts +++ b/extensions/markdown-language-features/src/logging.ts @@ -58,7 +58,7 @@ export class VsCodeOutputLogger extends Disposable implements ILogger { const now = new Date(); return String(now.getUTCHours()).padStart(2, '0') + ':' + String(now.getMinutes()).padStart(2, '0') - + ':' + String(now.getUTCSeconds()).padStart(2, '0') + '.' + now.getMilliseconds(); + + ':' + String(now.getUTCSeconds()).padStart(2, '0') + '.' + String(now.getMilliseconds()).padStart(3, '0'); } private updateConfiguration(): void { diff --git a/extensions/markdown-language-features/src/test/diagnostic.test.ts b/extensions/markdown-language-features/src/test/diagnostic.test.ts index 49600ea16b5..020e745a2d3 100644 --- a/extensions/markdown-language-features/src/test/diagnostic.test.ts +++ b/extensions/markdown-language-features/src/test/diagnostic.test.ts @@ -10,6 +10,7 @@ import { DiagnosticCollectionReporter, DiagnosticComputer, DiagnosticConfigurati import { MdLinkProvider } from '../languageFeatures/documentLinks'; import { MdReferencesProvider } from '../languageFeatures/references'; import { MdTableOfContentsProvider } from '../tableOfContents'; +import { ITextDocument } from '../types/textDocument'; import { noopToken } from '../util/cancellation'; import { DisposableStore } from '../util/dispose'; import { InMemoryDocument } from '../util/inMemoryDocument'; @@ -79,6 +80,12 @@ class MemoryDiagnosticReporter extends DiagnosticReporter { private readonly diagnostics = new ResourceMap(); + constructor( + private readonly workspace: InMemoryMdWorkspace, + ) { + super(); + } + override dispose(): void { super.clear(); this.clear(); @@ -93,7 +100,7 @@ class MemoryDiagnosticReporter extends DiagnosticReporter { this.diagnostics.set(uri, diagnostics); } - areDiagnosticsEnabled(_uri: vscode.Uri): boolean { + isOpen(_uri: vscode.Uri): boolean { return true; } @@ -104,6 +111,10 @@ class MemoryDiagnosticReporter extends DiagnosticReporter { get(uri: vscode.Uri): readonly vscode.Diagnostic[] { return orderDiagnosticsByRange(this.diagnostics.get(uri) ?? []); } + + getOpenDocuments(): ITextDocument[] { + return this.workspace.values(); + } } suite('markdown: Diagnostic Computer', () => { @@ -454,7 +465,7 @@ suite('Markdown: Diagnostics manager', () => { )) ])); - const reporter = store.add(new MemoryDiagnosticReporter()); + const reporter = store.add(new MemoryDiagnosticReporter(workspace)); const config = new MemoryDiagnosticConfiguration({ enabled: true }); const manager = createDiagnosticsManager(store, workspace, config, reporter); @@ -499,7 +510,7 @@ suite('Markdown: Diagnostics manager', () => { `[text](#no-such-2)`, )); const workspace = store.add(new InMemoryMdWorkspace([doc1, doc2])); - const reporter = store.add(new MemoryDiagnosticReporter()); + const reporter = store.add(new MemoryDiagnosticReporter(workspace)); const manager = createDiagnosticsManager(store, workspace, new MemoryDiagnosticConfiguration({}), reporter); await manager.ready; @@ -554,7 +565,7 @@ suite('Markdown: Diagnostics manager', () => { `# Header` )); const workspace = store.add(new InMemoryMdWorkspace([doc1, doc2])); - const reporter = store.add(new MemoryDiagnosticReporter()); + const reporter = store.add(new MemoryDiagnosticReporter(workspace)); const manager = createDiagnosticsManager(store, workspace, new MemoryDiagnosticConfiguration({}), reporter); await manager.ready; diff --git a/extensions/markdown-language-features/src/test/fileReferences.test.ts b/extensions/markdown-language-features/src/test/fileReferences.test.ts index d277e8aedbb..3b49e7790ea 100644 --- a/extensions/markdown-language-features/src/test/fileReferences.test.ts +++ b/extensions/markdown-language-features/src/test/fileReferences.test.ts @@ -22,7 +22,7 @@ function getFileReferences(store: DisposableStore, resource: vscode.Uri, workspa const engine = createNewMarkdownEngine(); const tocProvider = store.add(new MdTableOfContentsProvider(engine, workspace, nulLogger)); const computer = store.add(new MdReferencesProvider(engine, workspace, tocProvider, nulLogger)); - return computer.getAllReferencesToFile(resource, noopToken); + return computer.getReferencesToFileInWorkspace(resource, noopToken); } function assertReferencesEqual(actualRefs: readonly MdReference[], ...expectedRefs: { uri: vscode.Uri; line: number }[]) { diff --git a/extensions/markdown-language-features/src/test/inMemoryWorkspace.ts b/extensions/markdown-language-features/src/test/inMemoryWorkspace.ts index e614b3551a1..a383a73335a 100644 --- a/extensions/markdown-language-features/src/test/inMemoryWorkspace.ts +++ b/extensions/markdown-language-features/src/test/inMemoryWorkspace.ts @@ -22,10 +22,14 @@ export class InMemoryMdWorkspace extends Disposable implements IMdWorkspace { } } - public async getAllMarkdownDocuments() { + public values() { return Array.from(this._documents.values()); } + public async getAllMarkdownDocuments() { + return this.values(); + } + public async getOrLoadMarkdownDocument(resource: vscode.Uri): Promise { return this._documents.get(resource); } diff --git a/extensions/markdown-language-features/src/test/references.test.ts b/extensions/markdown-language-features/src/test/references.test.ts index ca33207a0b6..087ede35239 100644 --- a/extensions/markdown-language-features/src/test/references.test.ts +++ b/extensions/markdown-language-features/src/test/references.test.ts @@ -18,12 +18,19 @@ import { nulLogger } from './nulLogging'; import { joinLines, withStore, workspacePath } from './util'; -function getReferences(store: DisposableStore, doc: InMemoryDocument, pos: vscode.Position, workspace: IMdWorkspace) { +async function getReferences(store: DisposableStore, doc: InMemoryDocument, pos: vscode.Position, workspace: IMdWorkspace) { const engine = createNewMarkdownEngine(); const tocProvider = store.add(new MdTableOfContentsProvider(engine, workspace, nulLogger)); const computer = store.add(new MdReferencesProvider(engine, workspace, tocProvider, nulLogger)); const provider = new MdVsCodeReferencesProvider(computer); - return provider.provideReferences(doc, pos, { includeDeclaration: true }, noopToken); + const refs = await provider.provideReferences(doc, pos, { includeDeclaration: true }, noopToken); + return refs.sort((a, b) => { + const pathCompare = a.uri.toString().localeCompare(b.uri.toString()); + if (pathCompare !== 0) { + return pathCompare; + } + return a.range.start.compareTo(b.range.start); + }); } function assertReferencesEqual(actualRefs: readonly vscode.Location[], ...expectedRefs: { uri: vscode.Uri; line: number; startCharacter?: number; endCharacter?: number }[]) { @@ -130,7 +137,7 @@ suite('Markdown: Find all references', () => { test('Should find references from header across files', withStore(async (store) => { const docUri = workspacePath('doc.md'); const other1Uri = workspacePath('sub', 'other.md'); - const other2Uri = workspacePath('other2.md'); + const other2Uri = workspacePath('zOther2.md'); const doc = new InMemoryDocument(docUri, joinLines( `# abc`, @@ -216,7 +223,7 @@ suite('Markdown: Find all references', () => { test('Should find references from link across files', withStore(async (store) => { const docUri = workspacePath('doc.md'); const other1Uri = workspacePath('sub', 'other.md'); - const other2Uri = workspacePath('other2.md'); + const other2Uri = workspacePath('zOther2.md'); const doc = new InMemoryDocument(docUri, joinLines( `# abc`, @@ -300,9 +307,9 @@ suite('Markdown: Find all references', () => { const refs = await getReferences(store, doc, new vscode.Position(0, 23), workspace); assertReferencesEqual(refs!, - { uri: other1Uri, line: 1 }, // Header definition { uri: docUri, line: 0 }, { uri: docUri, line: 1 }, + { uri: other1Uri, line: 1 }, // Header definition ); })); @@ -467,7 +474,7 @@ suite('Markdown: Find all references', () => { { // Check refs to header fragment const headerRefs = await getReferences(store, otherDoc, new vscode.Position(0, 16), workspace); - assertReferencesEqual(headerRefs!, + assertReferencesEqual(headerRefs, { uri: docUri, line: 0 }, // Header definition { uri: docUri, line: 2 }, { uri: other1Uri, line: 0 }, @@ -477,7 +484,7 @@ suite('Markdown: Find all references', () => { { // Check refs to file itself from link with ext const fileRefs = await getReferences(store, otherDoc, new vscode.Position(0, 9), workspace); - assertReferencesEqual(fileRefs!, + assertReferencesEqual(fileRefs, { uri: other1Uri, line: 0, endCharacter: 14 }, { uri: other1Uri, line: 1, endCharacter: 19 }, ); @@ -485,7 +492,7 @@ suite('Markdown: Find all references', () => { { // Check refs to file itself from link without ext const fileRefs = await getReferences(store, otherDoc, new vscode.Position(1, 17), workspace); - assertReferencesEqual(fileRefs!, + assertReferencesEqual(fileRefs, { uri: other1Uri, line: 0 }, { uri: other1Uri, line: 1 }, ); diff --git a/extensions/markdown-language-features/src/util/workspaceCache.ts b/extensions/markdown-language-features/src/util/workspaceCache.ts index 5e9e4010976..8432675c87a 100644 --- a/extensions/markdown-language-features/src/util/workspaceCache.ts +++ b/extensions/markdown-language-features/src/util/workspaceCache.ts @@ -143,6 +143,16 @@ export class MdWorkspaceInfoCache extends Disposable { return Array.from(await this._cache.entries(), x => x[1]); } + public async getForDocs(docs: readonly ITextDocument[]): Promise { + for (const doc of docs) { + if (!this._cache.has(doc.uri)) { + this.update(doc); + } + } + + return Promise.all(docs.map(doc => this._cache.get(doc.uri) as Promise)); + } + private async ensureInit(): Promise { if (!this._init) { this._init = this.populateCache(); @@ -157,7 +167,9 @@ export class MdWorkspaceInfoCache extends Disposable { private async populateCache(): Promise { const markdownDocumentUris = await this.workspace.getAllMarkdownDocuments(); for (const document of markdownDocumentUris) { - this.update(document); + if (!this._cache.has(document.uri)) { + this.update(document); + } } }