From 3083626214de1a74db4c5bf3e5e77dbe7d47a421 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Thu, 2 Apr 2026 17:21:34 -0700 Subject: [PATCH 01/11] Implement multi-file doc highlights --- _extension/package.json | 7 +- _extension/src/client.ts | 2 + .../src/languageFeatures/documentHighlight.ts | 78 +++++++ ...oposed.multiDocumentHighlightProvider.d.ts | 58 +++++ .../fourslash/_scripts/convertFourslash.mts | 54 ++++- internal/fourslash/_scripts/unparsedTests.txt | 4 - internal/fourslash/fourslash.go | 89 ++++++-- .../tests/gen/documentHighlights02_test.go | 33 +++ .../gen/documentHighlights_33722_test.go | 2 +- ...mport_filesToSearchWithInvalidFile_test.go | 30 +++ ...hlights_moduleImport_filesToSearch_test.go | 30 +++ .../documentHighlights_windowsPath_test.go | 22 ++ .../gen/exportInLabeledStatement_test.go | 2 +- .../tests/gen/exportInObjectLiteral_test.go | 2 +- .../tests/gen/findAllRefsForModule_test.go | 2 +- internal/ls/documenthighlights.go | 94 ++++++++ internal/lsp/lsproto/_generate/generate.mts | 61 +++++ internal/lsp/lsproto/lsp_generated.go | 212 ++++++++++++++++++ internal/lsp/server.go | 6 + package-lock.json | 10 +- .../documentHighlights02.baseline.jsonc | 34 +++ .../documentHighlights_33722.baseline.jsonc | 3 + ..._moduleImport_filesToSearch.baseline.jsonc | 25 +++ ...ilesToSearchWithInvalidFile.baseline.jsonc | 27 +++ ...umentHighlights_windowsPath.baseline.jsonc | 6 + .../exportInLabeledStatement.baseline.jsonc | 3 + .../exportInObjectLiteral.baseline.jsonc | 3 + .../findAllRefsForModule.baseline.jsonc | 33 +++ 28 files changed, 896 insertions(+), 36 deletions(-) create mode 100644 _extension/src/languageFeatures/documentHighlight.ts create mode 100644 _extension/src/vscode.proposed.multiDocumentHighlightProvider.d.ts create mode 100644 internal/fourslash/tests/gen/documentHighlights02_test.go create mode 100644 internal/fourslash/tests/gen/documentHighlights_moduleImport_filesToSearchWithInvalidFile_test.go create mode 100644 internal/fourslash/tests/gen/documentHighlights_moduleImport_filesToSearch_test.go create mode 100644 internal/fourslash/tests/gen/documentHighlights_windowsPath_test.go create mode 100644 testdata/baselines/reference/fourslash/documentHighlights/documentHighlights02.baseline.jsonc create mode 100644 testdata/baselines/reference/fourslash/documentHighlights/documentHighlights_moduleImport_filesToSearch.baseline.jsonc create mode 100644 testdata/baselines/reference/fourslash/documentHighlights/documentHighlights_moduleImport_filesToSearchWithInvalidFile.baseline.jsonc create mode 100644 testdata/baselines/reference/fourslash/documentHighlights/documentHighlights_windowsPath.baseline.jsonc diff --git a/_extension/package.json b/_extension/package.json index 3a654bc4648..d54dfe48808 100644 --- a/_extension/package.json +++ b/_extension/package.json @@ -12,8 +12,11 @@ "type": "git", "url": "https://github.com/microsoft/typescript-go" }, + "enabledApiProposals": [ + "multiDocumentHighlightProvider" + ], "engines": { - "vscode": "^1.106.0" + "vscode": "^1.110.0" }, "capabilities": { "untrustedWorkspaces": { @@ -176,7 +179,7 @@ "vscode-languageclient": "^10.0.0-next.21" }, "devDependencies": { - "@types/vscode": "~1.106.1", + "@types/vscode": "~1.110.0", "@vscode/vsce": "^3.7.1", "esbuild": "^0.27.4" } diff --git a/_extension/src/client.ts b/_extension/src/client.ts index 9ca8e361e09..ff7659aec5e 100644 --- a/_extension/src/client.ts +++ b/_extension/src/client.ts @@ -20,6 +20,7 @@ import { configurationMiddleware, sendNotificationMiddleware, } from "./configurationMiddleware"; +import { registerMultiDocumentHighlightFeature } from "./languageFeatures/documentHighlight"; import { registerTagClosingFeature } from "./languageFeatures/tagClosing"; import * as tr from "./telemetryReporting"; import { @@ -205,6 +206,7 @@ export class Client implements vscode.Disposable { this.disposables.push( serverTelemetryListener, + registerMultiDocumentHighlightFeature(this.documentSelector, this.client), registerTagClosingFeature("typescript", this.documentSelector, this.client), registerTagClosingFeature("javascript", this.documentSelector, this.client), ); diff --git a/_extension/src/languageFeatures/documentHighlight.ts b/_extension/src/languageFeatures/documentHighlight.ts new file mode 100644 index 00000000000..578c9d06858 --- /dev/null +++ b/_extension/src/languageFeatures/documentHighlight.ts @@ -0,0 +1,78 @@ +import * as vscode from "vscode"; +import { LanguageClient } from "vscode-languageclient/node"; + +const multiDocumentHighlightMethod = "custom/textDocument/multiDocumentHighlight"; + +interface MultiDocumentHighlightParams { + textDocument: { uri: string; }; + position: { line: number; character: number; }; + filesToSearch: string[]; +} + +interface MultiDocumentHighlightItem { + uri: string; + highlights: { range: { start: { line: number; character: number; }; end: { line: number; character: number; }; }; kind?: number; }[]; +} + +class MultiDocumentHighlightProvider implements vscode.MultiDocumentHighlightProvider { + constructor(private readonly client: LanguageClient) {} + + async provideMultiDocumentHighlights( + document: vscode.TextDocument, + position: vscode.Position, + otherDocuments: vscode.TextDocument[], + token: vscode.CancellationToken, + ): Promise { + const allFiles = [document, ...otherDocuments] + .map(doc => this.client.code2ProtocolConverter.asUri(doc.uri)) + .filter(file => !!file); + + if (allFiles.length === 0) { + return []; + } + + const params: MultiDocumentHighlightParams = { + textDocument: this.client.code2ProtocolConverter.asTextDocumentIdentifier(document), + position: this.client.code2ProtocolConverter.asPosition(position), + filesToSearch: allFiles, + }; + + let response: MultiDocumentHighlightItem[] | null; + try { + response = await this.client.sendRequest(multiDocumentHighlightMethod, params, token); + } + catch (error) { + return []; + } + + if (!response || token.isCancellationRequested) { + return []; + } + + return response.map(item => + new vscode.MultiDocumentHighlight( + vscode.Uri.parse(item.uri), + item.highlights.map(h => + new vscode.DocumentHighlight( + new vscode.Range( + new vscode.Position(h.range.start.line, h.range.start.character), + new vscode.Position(h.range.end.line, h.range.end.character), + ), + h.kind === 3 ? vscode.DocumentHighlightKind.Write : vscode.DocumentHighlightKind.Read, + ) + ), + ) + ); + } +} + +export function registerMultiDocumentHighlightFeature( + selector: vscode.DocumentSelector, + client: LanguageClient, +): vscode.Disposable { + const capabilities = client.initializeResult?.capabilities as { customMultiDocumentHighlightProvider?: boolean; } | undefined; + if (!capabilities?.customMultiDocumentHighlightProvider) { + return { dispose() {} }; + } + return vscode.languages.registerMultiDocumentHighlightProvider(selector, new MultiDocumentHighlightProvider(client)); +} diff --git a/_extension/src/vscode.proposed.multiDocumentHighlightProvider.d.ts b/_extension/src/vscode.proposed.multiDocumentHighlightProvider.d.ts new file mode 100644 index 00000000000..1a95e49b4a9 --- /dev/null +++ b/_extension/src/vscode.proposed.multiDocumentHighlightProvider.d.ts @@ -0,0 +1,58 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module "vscode" { + /** + * Represents a collection of document highlights from multiple documents. + */ + export class MultiDocumentHighlight { + /** + * The URI of the document containing the highlights. + */ + uri: Uri; + + /** + * The highlights for the document. + */ + highlights: DocumentHighlight[]; + + /** + * Creates a new instance of MultiDocumentHighlight. + * @param uri The URI of the document containing the highlights. + * @param highlights The highlights for the document. + */ + constructor(uri: Uri, highlights: DocumentHighlight[]); + } + + export interface MultiDocumentHighlightProvider { + /** + * Provide a set of document highlights, like all occurrences of a variable or + * all exit-points of a function. + * + * @param document The document in which the command was invoked. + * @param position The position at which the command was invoked. + * @param otherDocuments An array of additional valid documents for which highlights should be provided. + * @param token A cancellation token. + * @returns A Map containing a mapping of the Uri of a document to the document highlights or a thenable that resolves to such. The lack of a result can be + * signaled by returning `undefined`, `null`, or an empty map. + */ + provideMultiDocumentHighlights(document: TextDocument, position: Position, otherDocuments: TextDocument[], token: CancellationToken): ProviderResult; + } + + namespace languages { + /** + * Register a multi document highlight provider. + * + * Multiple providers can be registered for a language. In that case providers are sorted + * by their {@link languages.match score} and groups sequentially asked for document highlights. + * The process stops when a provider returns a `non-falsy` or `non-failure` result. + * + * @param selector A selector that defines the documents this provider is applicable to. + * @param provider A multi-document highlight provider. + * @returns A {@link Disposable} that unregisters this provider when being disposed. + */ + export function registerMultiDocumentHighlightProvider(selector: DocumentSelector, provider: MultiDocumentHighlightProvider): Disposable; + } +} diff --git a/internal/fourslash/_scripts/convertFourslash.mts b/internal/fourslash/_scripts/convertFourslash.mts index 9b40f4862a8..57fbd80ffa3 100755 --- a/internal/fourslash/_scripts/convertFourslash.mts +++ b/internal/fourslash/_scripts/convertFourslash.mts @@ -1261,6 +1261,7 @@ function parseBaselineFindAllReferencesArgs(args: readonly ts.Expression[]): [Ve function parseBaselineDocumentHighlightsArgs(args: readonly ts.Expression[]): [VerifyBaselineDocumentHighlightsCmd] { const newArgs: string[] = []; let preferences: string | undefined; + let filesToSearch: string[] | undefined; for (const arg of args) { let strArg; if (strArg = getArrayLiteralExpression(arg)) { @@ -1269,8 +1270,47 @@ function parseBaselineDocumentHighlightsArgs(args: readonly ts.Expression[]): [V newArgs.push(newArg); } } + else if (ts.isCallExpression(arg) && arg.getText().includes("test.ranges()")) { + newArgs.push("ToAny(f.Ranges())..."); + } else if (ts.isObjectLiteralExpression(arg)) { - // !!! todo when multiple files supported in lsp + for (const prop of arg.properties) { + if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name) && prop.name.text === "filesToSearch" && ts.isArrayLiteralExpression(prop.initializer)) { + filesToSearch = []; + for (const e of prop.initializer.elements) { + if (ts.isStringLiteral(e)) { + filesToSearch.push(JSON.stringify(e.text)); + } + else if (ts.isPropertyAccessExpression(e) && e.name.text === "fileName") { + // e.g. test.ranges()[0].fileName -> f.Ranges()[0].FileName() + const obj = e.expression; + if (ts.isElementAccessExpression(obj) && ts.isCallExpression(obj.expression) && obj.expression.getText().includes("ranges")) { + const index = obj.argumentExpression?.getText(); + if (index !== undefined) { + filesToSearch.push(`f.Ranges()[${index}].FileName()`); + continue; + } + } + // e.g. range.fileName where `const range = test.ranges()[0]` + if (ts.isIdentifier(obj)) { + const resolved = parseRangeVariable(obj); + if (resolved) { + filesToSearch.push(`${resolved}.FileName()`); + continue; + } + } + // Fallback: skip filesToSearch entirely + filesToSearch = undefined; + break; + } + else { + // Unsupported expression; skip filesToSearch + filesToSearch = undefined; + break; + } + } + } + } } else { newArgs.push(parseBaselineMarkerOrRangeArg(arg)); @@ -1285,6 +1325,7 @@ function parseBaselineDocumentHighlightsArgs(args: readonly ts.Expression[]): [V kind: "verifyBaselineDocumentHighlights", args: newArgs, preferences: preferences ? preferences : "nil /*preferences*/", + filesToSearch, }]; } @@ -1837,6 +1878,10 @@ function parseRangeVariable(arg: ts.Identifier | ts.ElementAccessExpression): st if (ts.isElementAccessExpression(arg)) { return `f.Ranges()[${arg.argumentExpression!.getText()}]`; } + // `const range = test.ranges()[0]` used directly as `range` + if (ts.isIdentifier(arg) && ts.isElementAccessExpression(decl.initializer) && ts.isCallExpression(decl.initializer.expression) && decl.initializer.argumentExpression) { + return `f.Ranges()[${decl.initializer.argumentExpression.getText()}]`; + } } // `const cRanges = ranges.get("C")` or `const cRanges = test.rangesByText().get("C")` if (ts.isIdentifier(decl.name) && decl.name.text === argName && decl.initializer && ts.isCallExpression(decl.initializer)) { @@ -3018,6 +3063,7 @@ interface VerifyBaselineDocumentHighlightsCmd { kind: "verifyBaselineDocumentHighlights"; args: string[]; preferences: string; + filesToSearch?: string[]; } interface VerifyBaselineInlayHintsCmd { @@ -3288,7 +3334,11 @@ function generateBaselineFindAllReferences({ markers, ranges }: VerifyBaselineFi return `f.VerifyBaselineFindAllReferences(t, ${markers.join(", ")})`; } -function generateBaselineDocumentHighlights({ args, preferences }: VerifyBaselineDocumentHighlightsCmd): string { +function generateBaselineDocumentHighlights({ args, preferences, filesToSearch }: VerifyBaselineDocumentHighlightsCmd): string { + if (filesToSearch) { + const filesGo = `[]string{${filesToSearch.join(", ")}}`; + return `f.VerifyBaselineDocumentHighlightsWithOptions(t, ${preferences}, ${filesGo}, ${args.join(", ")})`; + } return `f.VerifyBaselineDocumentHighlights(t, ${preferences}, ${args.join(", ")})`; } diff --git a/internal/fourslash/_scripts/unparsedTests.txt b/internal/fourslash/_scripts/unparsedTests.txt index 3d7ba11aa41..5f346e52a40 100644 --- a/internal/fourslash/_scripts/unparsedTests.txt +++ b/internal/fourslash/_scripts/unparsedTests.txt @@ -1460,10 +1460,6 @@ docCommentTemplateWithMultipleJSDoc1.ts parse error: "Unrecognized fourslash sta docCommentTemplateWithMultipleJSDoc2.ts parse error: "Unrecognized fourslash statement: verify.docCommentTemplateAt(...)" docCommentTemplateWithMultipleJSDoc3.ts parse error: "Unrecognized fourslash statement: verify.docCommentTemplateAt(...)" docCommentTemplateWithMultipleJSDocAndParameters.ts parse error: "Unrecognized fourslash statement: verify.docCommentTemplateAt(...)" -documentHighlights_moduleImport_filesToSearch.ts parse error: "Unrecognized marker or range argument: test.ranges()" -documentHighlights_moduleImport_filesToSearchWithInvalidFile.ts parse error: "Unrecognized marker or range argument: test.ranges()" -documentHighlights_windowsPath.ts parse error: "Unrecognized marker or range argument: range" -documentHighlights02.ts parse error: "Unrecognized marker or range argument: test.ranges()" duplicateClassModuleError0.ts parse error: "Unrecognized edit function: disableFormatting" editClearsJsDocCache.ts parse error: "Unrecognized edit function: replace" eval.ts parse error: "Unrecognized fourslash statement: verify.eval(...)" diff --git a/internal/fourslash/fourslash.go b/internal/fourslash/fourslash.go index 0df43d4daad..b4814a20c55 100644 --- a/internal/fourslash/fourslash.go +++ b/internal/fourslash/fourslash.go @@ -2651,6 +2651,15 @@ func (f *FourslashTest) VerifyBaselineDocumentHighlights( t *testing.T, preferences *lsutil.UserPreferences, markerOrRangeOrNames ...MarkerOrRangeOrName, +) { + f.VerifyBaselineDocumentHighlightsWithOptions(t, preferences, nil /*filesToSearch*/, markerOrRangeOrNames...) +} + +func (f *FourslashTest) VerifyBaselineDocumentHighlightsWithOptions( + t *testing.T, + preferences *lsutil.UserPreferences, + filesToSearch []string, + markerOrRangeOrNames ...MarkerOrRangeOrName, ) { var markerOrRanges []MarkerOrRange for _, markerOrRangeOrName := range markerOrRangeOrNames { @@ -2670,39 +2679,81 @@ func (f *FourslashTest) VerifyBaselineDocumentHighlights( } } - f.verifyBaselineDocumentHighlights(t, preferences, markerOrRanges) + f.verifyBaselineDocumentHighlights(t, preferences, filesToSearch, markerOrRanges) } func (f *FourslashTest) verifyBaselineDocumentHighlights( t *testing.T, preferences *lsutil.UserPreferences, + filesToSearch []string, markerOrRanges []MarkerOrRange, ) { for _, markerOrRange := range markerOrRanges { f.goToMarker(t, markerOrRange) - params := &lsproto.DocumentHighlightParams{ - TextDocument: lsproto.TextDocumentIdentifier{ - Uri: lsconv.FileNameToDocumentURI(f.activeFilename), - }, - Position: f.currentCaretPosition, - } - result := sendRequest(t, f, lsproto.TextDocumentDocumentHighlightInfo, params) - highlights := result.DocumentHighlights - if highlights == nil { - highlights = &[]*lsproto.DocumentHighlight{} - } - var spans []lsproto.Location - for _, h := range *highlights { - spans = append(spans, lsproto.Location{ - Uri: lsconv.FileNameToDocumentURI(f.activeFilename), - Range: h.Range, - }) + var header string + + if len(filesToSearch) > 0 { + // Multi-file: use the custom method. + var searchURIs []lsproto.DocumentUri + for _, file := range filesToSearch { + searchURIs = append(searchURIs, lsconv.FileNameToDocumentURI(file)) + } + + params := &lsproto.MultiDocumentHighlightParams{ + TextDocument: lsproto.TextDocumentIdentifier{ + Uri: lsconv.FileNameToDocumentURI(f.activeFilename), + }, + Position: f.currentCaretPosition, + FilesToSearch: searchURIs, + } + result := sendRequest(t, f, lsproto.CustomTextDocumentMultiDocumentHighlightInfo, params) + multiHighlights := result.MultiDocumentHighlights + if multiHighlights == nil { + multiHighlights = &[]*lsproto.MultiDocumentHighlight{} + } + + for _, mh := range *multiHighlights { + for _, h := range mh.Highlights { + spans = append(spans, lsproto.Location{ + Uri: mh.Uri, + Range: h.Range, + }) + } + } + + var sb strings.Builder + sb.WriteString("// filesToSearch:\n") + for _, file := range filesToSearch { + fmt.Fprintf(&sb, "// %s\n", file) + } + sb.WriteString("\n") + header = sb.String() + } else { + // Single-file: use the standard LSP method. + params := &lsproto.DocumentHighlightParams{ + TextDocument: lsproto.TextDocumentIdentifier{ + Uri: lsconv.FileNameToDocumentURI(f.activeFilename), + }, + Position: f.currentCaretPosition, + } + result := sendRequest(t, f, lsproto.TextDocumentDocumentHighlightInfo, params) + highlights := result.DocumentHighlights + if highlights == nil { + highlights = &[]*lsproto.DocumentHighlight{} + } + + for _, h := range *highlights { + spans = append(spans, lsproto.Location{ + Uri: lsconv.FileNameToDocumentURI(f.activeFilename), + Range: h.Range, + }) + } } // Add result to baseline - f.addResultToBaseline(t, documentHighlightsCmd, f.getBaselineForLocationsWithFileContents(spans, baselineFourslashLocationsOptions{ + f.addResultToBaseline(t, documentHighlightsCmd, header+f.getBaselineForLocationsWithFileContents(spans, baselineFourslashLocationsOptions{ marker: markerOrRange, markerName: "/*HIGHLIGHTS*/", })) diff --git a/internal/fourslash/tests/gen/documentHighlights02_test.go b/internal/fourslash/tests/gen/documentHighlights02_test.go new file mode 100644 index 00000000000..460c753a5f7 --- /dev/null +++ b/internal/fourslash/tests/gen/documentHighlights02_test.go @@ -0,0 +1,33 @@ +// Code generated by convertFourslash; DO NOT EDIT. +// To modify this test, run "npm run makemanual documentHighlights02" + +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/fourslash" + . "github.com/microsoft/typescript-go/internal/fourslash/tests/util" + "github.com/microsoft/typescript-go/internal/testutil" +) + +func TestDocumentHighlights02(t *testing.T) { + fourslash.SkipIfFailing(t) + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `// @lib: es5 +// @Filename: a.ts +function [|foo|] () { + return 1; +} +[|foo|](); +// @Filename: b.ts +/// +[|foo|]();` + f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) + defer done() + f.MarkTestAsStradaServer() + f.GoToFile(t, "a.ts") + f.GoToFile(t, "b.ts") + f.VerifyBaselineDocumentHighlightsWithOptions(t, nil /*preferences*/, []string{"a.ts", "b.ts"}, ToAny(f.Ranges())...) +} diff --git a/internal/fourslash/tests/gen/documentHighlights_33722_test.go b/internal/fourslash/tests/gen/documentHighlights_33722_test.go index cec87f5e4f6..bb5fa4220cd 100644 --- a/internal/fourslash/tests/gen/documentHighlights_33722_test.go +++ b/internal/fourslash/tests/gen/documentHighlights_33722_test.go @@ -27,5 +27,5 @@ import y from "./y"; y().[|foo|]();` f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) defer done() - f.VerifyBaselineDocumentHighlights(t, nil /*preferences*/, f.Ranges()[0]) + f.VerifyBaselineDocumentHighlightsWithOptions(t, nil /*preferences*/, []string{"/x.ts"}, f.Ranges()[0]) } diff --git a/internal/fourslash/tests/gen/documentHighlights_moduleImport_filesToSearchWithInvalidFile_test.go b/internal/fourslash/tests/gen/documentHighlights_moduleImport_filesToSearchWithInvalidFile_test.go new file mode 100644 index 00000000000..0f555dce6e8 --- /dev/null +++ b/internal/fourslash/tests/gen/documentHighlights_moduleImport_filesToSearchWithInvalidFile_test.go @@ -0,0 +1,30 @@ +// Code generated by convertFourslash; DO NOT EDIT. +// To modify this test, run "npm run makemanual documentHighlights_moduleImport_filesToSearchWithInvalidFile" + +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/fourslash" + . "github.com/microsoft/typescript-go/internal/fourslash/tests/util" + "github.com/microsoft/typescript-go/internal/testutil" +) + +func TestDocumentHighlights_moduleImport_filesToSearchWithInvalidFile(t *testing.T) { + fourslash.SkipIfFailing(t) + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `// @Filename: /node_modules/@types/foo/index.d.ts +export const x: number; +// @Filename: /a.ts +import * as foo from "foo"; +foo.[|x|]; +// @Filename: /b.ts +import { [|x|] } from "foo"; +// @Filename: /c.ts +import { x } from "foo";` + f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) + defer done() + f.VerifyBaselineDocumentHighlightsWithOptions(t, nil /*preferences*/, []string{"/a.ts", "/b.ts", "/unknown.ts"}, ToAny(f.Ranges())...) +} diff --git a/internal/fourslash/tests/gen/documentHighlights_moduleImport_filesToSearch_test.go b/internal/fourslash/tests/gen/documentHighlights_moduleImport_filesToSearch_test.go new file mode 100644 index 00000000000..53fac38c8b5 --- /dev/null +++ b/internal/fourslash/tests/gen/documentHighlights_moduleImport_filesToSearch_test.go @@ -0,0 +1,30 @@ +// Code generated by convertFourslash; DO NOT EDIT. +// To modify this test, run "npm run makemanual documentHighlights_moduleImport_filesToSearch" + +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/fourslash" + . "github.com/microsoft/typescript-go/internal/fourslash/tests/util" + "github.com/microsoft/typescript-go/internal/testutil" +) + +func TestDocumentHighlights_moduleImport_filesToSearch(t *testing.T) { + fourslash.SkipIfFailing(t) + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `// @Filename: /node_modules/@types/foo/index.d.ts +export const x: number; +// @Filename: /a.ts +import * as foo from "foo"; +foo.[|x|]; +// @Filename: /b.ts +import { [|x|] } from "foo"; +// @Filename: /c.ts +import { x } from "foo";` + f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) + defer done() + f.VerifyBaselineDocumentHighlightsWithOptions(t, nil /*preferences*/, []string{"/a.ts", "/b.ts"}, ToAny(f.Ranges())...) +} diff --git a/internal/fourslash/tests/gen/documentHighlights_windowsPath_test.go b/internal/fourslash/tests/gen/documentHighlights_windowsPath_test.go new file mode 100644 index 00000000000..f8b1cc793a7 --- /dev/null +++ b/internal/fourslash/tests/gen/documentHighlights_windowsPath_test.go @@ -0,0 +1,22 @@ +// Code generated by convertFourslash; DO NOT EDIT. +// To modify this test, run "npm run makemanual documentHighlights_windowsPath" + +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/fourslash" + "github.com/microsoft/typescript-go/internal/testutil" +) + +func TestDocumentHighlights_windowsPath(t *testing.T) { + fourslash.SkipIfFailing(t) + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `//@Filename: C:\a\b\c.ts +var /*1*/[|x|] = 1;` + f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) + defer done() + f.VerifyBaselineDocumentHighlightsWithOptions(t, nil /*preferences*/, []string{f.Ranges()[0].FileName()}, f.Ranges()[0]) +} diff --git a/internal/fourslash/tests/gen/exportInLabeledStatement_test.go b/internal/fourslash/tests/gen/exportInLabeledStatement_test.go index c2cbed5afc9..da57a252984 100644 --- a/internal/fourslash/tests/gen/exportInLabeledStatement_test.go +++ b/internal/fourslash/tests/gen/exportInLabeledStatement_test.go @@ -19,5 +19,5 @@ subTitle: [|export|] const title: string` f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) defer done() - f.VerifyBaselineDocumentHighlights(t, nil /*preferences*/, f.Ranges()[0]) + f.VerifyBaselineDocumentHighlightsWithOptions(t, nil /*preferences*/, []string{f.Ranges()[0].FileName()}, f.Ranges()[0]) } diff --git a/internal/fourslash/tests/gen/exportInObjectLiteral_test.go b/internal/fourslash/tests/gen/exportInObjectLiteral_test.go index 320b51a8776..e77ed18c737 100644 --- a/internal/fourslash/tests/gen/exportInObjectLiteral_test.go +++ b/internal/fourslash/tests/gen/exportInObjectLiteral_test.go @@ -20,5 +20,5 @@ const k = { }` f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) defer done() - f.VerifyBaselineDocumentHighlights(t, nil /*preferences*/, f.Ranges()[0]) + f.VerifyBaselineDocumentHighlightsWithOptions(t, nil /*preferences*/, []string{f.Ranges()[0].FileName()}, f.Ranges()[0]) } diff --git a/internal/fourslash/tests/gen/findAllRefsForModule_test.go b/internal/fourslash/tests/gen/findAllRefsForModule_test.go index ee2e9fcab3d..1df0989f145 100644 --- a/internal/fourslash/tests/gen/findAllRefsForModule_test.go +++ b/internal/fourslash/tests/gen/findAllRefsForModule_test.go @@ -26,5 +26,5 @@ export const x = 0; f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) defer done() f.VerifyBaselineFindAllReferences(t, "0", "1", "2") - f.VerifyBaselineDocumentHighlights(t, nil /*preferences*/, f.Ranges()[1], f.Ranges()[3], f.Ranges()[4]) + f.VerifyBaselineDocumentHighlightsWithOptions(t, nil /*preferences*/, []string{"/b.ts", "/c/sub.js", "/d.ts"}, f.Ranges()[1], f.Ranges()[3], f.Ranges()[4]) } diff --git a/internal/ls/documenthighlights.go b/internal/ls/documenthighlights.go index 9d0ac40558c..70334545c48 100644 --- a/internal/ls/documenthighlights.go +++ b/internal/ls/documenthighlights.go @@ -6,6 +6,7 @@ import ( "github.com/microsoft/typescript-go/internal/ast" "github.com/microsoft/typescript-go/internal/astnav" "github.com/microsoft/typescript-go/internal/compiler" + "github.com/microsoft/typescript-go/internal/ls/lsconv" "github.com/microsoft/typescript-go/internal/ls/lsutil" "github.com/microsoft/typescript-go/internal/scanner" "github.com/microsoft/typescript-go/internal/stringutil" @@ -49,6 +50,63 @@ func (l *LanguageService) ProvideDocumentHighlights(ctx context.Context, documen return lsproto.DocumentHighlightsOrNull{DocumentHighlights: &documentHighlights}, nil } +func (l *LanguageService) ProvideMultiDocumentHighlights(ctx context.Context, documentUri lsproto.DocumentUri, documentPosition lsproto.Position, filesToSearch []lsproto.DocumentUri) (lsproto.CustomMultiDocumentHighlightResponse, error) { + program, sourceFile := l.getProgramAndFile(documentUri) + position := int(l.converters.LineAndCharacterToPosition(sourceFile, documentPosition)) + node := astnav.GetTouchingPropertyName(sourceFile, position) + + // Resolve the source files to search + var sourceFiles []*ast.SourceFile + for _, uri := range filesToSearch { + if sf := program.GetSourceFile(uri.FileName()); sf != nil { + sourceFiles = append(sourceFiles, sf) + } + } + if len(sourceFiles) == 0 { + sourceFiles = []*ast.SourceFile{sourceFile} + } + + if node.Parent != nil && (node.Parent.Kind == ast.KindJsxClosingElement || (node.Parent.Kind == ast.KindJsxOpeningElement && node.Parent.TagName() == node)) { + var openingElement, closingElement *ast.Node + if ast.IsJsxElement(node.Parent.Parent) { + openingElement = node.Parent.Parent.AsJsxElement().OpeningElement + closingElement = node.Parent.Parent.AsJsxElement().ClosingElement + } + var highlights []*lsproto.DocumentHighlight + kind := lsproto.DocumentHighlightKindRead + if openingElement != nil { + highlights = append(highlights, &lsproto.DocumentHighlight{ + Range: l.createLspRangeFromNode(openingElement, sourceFile), + Kind: &kind, + }) + } + if closingElement != nil { + highlights = append(highlights, &lsproto.DocumentHighlight{ + Range: l.createLspRangeFromNode(closingElement, sourceFile), + Kind: &kind, + }) + } + multiHighlights := []*lsproto.MultiDocumentHighlight{ + {Uri: documentUri, Highlights: highlights}, + } + return lsproto.MultiDocumentHighlightsOrNull{ + MultiDocumentHighlights: &multiHighlights, + }, nil + } + + multiHighlights := l.getMultiFileSemanticDocumentHighlights(ctx, position, node, program, sourceFiles) + if len(multiHighlights) == 0 { + // Fall back to syntactic highlights for the current file only. + syntacticHighlights := l.getSyntacticDocumentHighlights(node, sourceFile) + if len(syntacticHighlights) > 0 { + multiHighlights = []*lsproto.MultiDocumentHighlight{ + {Uri: documentUri, Highlights: syntacticHighlights}, + } + } + } + return lsproto.MultiDocumentHighlightsOrNull{MultiDocumentHighlights: &multiHighlights}, nil +} + func (l *LanguageService) getSemanticDocumentHighlights(ctx context.Context, position int, node *ast.Node, program *compiler.Program, sourceFile *ast.SourceFile) []*lsproto.DocumentHighlight { options := refOptions{use: referenceUseNone} referenceEntries := l.getReferencedSymbolsForNode(ctx, position, node, program, []*ast.SourceFile{sourceFile}, options) @@ -68,6 +126,42 @@ func (l *LanguageService) getSemanticDocumentHighlights(ctx context.Context, pos return highlights } +func (l *LanguageService) getMultiFileSemanticDocumentHighlights(ctx context.Context, position int, node *ast.Node, program *compiler.Program, sourceFiles []*ast.SourceFile) []*lsproto.MultiDocumentHighlight { + options := refOptions{use: referenceUseNone} + referenceEntries := l.getReferencedSymbolsForNode(ctx, position, node, program, sourceFiles, options) + if referenceEntries == nil { + return nil + } + + // Build a set of allowed file names for quick lookup + allowedFiles := make(map[string]bool, len(sourceFiles)) + for _, sf := range sourceFiles { + allowedFiles[sf.FileName()] = true + } + + // Group highlights by file + fileHighlights := make(map[string][]*lsproto.DocumentHighlight) + for _, entry := range referenceEntries { + for _, ref := range entry.references { + fileName, highlight := l.toDocumentHighlight(ref) + if allowedFiles[fileName] { + fileHighlights[fileName] = append(fileHighlights[fileName], highlight) + } + } + } + + var result []*lsproto.MultiDocumentHighlight + for _, sf := range sourceFiles { + if highlights, ok := fileHighlights[sf.FileName()]; ok { + result = append(result, &lsproto.MultiDocumentHighlight{ + Uri: lsconv.FileNameToDocumentURI(sf.FileName()), + Highlights: highlights, + }) + } + } + return result +} + func (l *LanguageService) toDocumentHighlight(entry *ReferenceEntry) (string, *lsproto.DocumentHighlight) { entry = l.resolveEntry(entry) diff --git a/internal/lsp/lsproto/_generate/generate.mts b/internal/lsp/lsproto/_generate/generate.mts index d6623fdb169..89b44599fb6 100755 --- a/internal/lsp/lsproto/_generate/generate.mts +++ b/internal/lsp/lsproto/_generate/generate.mts @@ -286,6 +286,43 @@ const customStructures: Structure[] = [ ], documentation: "Result for the custom/projectInfo request.", }, + { + name: "MultiDocumentHighlight", + properties: [ + { + name: "uri", + type: { kind: "base", name: "DocumentUri" }, + documentation: "The URI of the document containing the highlights.", + }, + { + name: "highlights", + type: { kind: "array", element: { kind: "reference", name: "DocumentHighlight" } }, + documentation: "The highlights for the document.", + }, + ], + documentation: "Represents a collection of document highlights from a single document, used in multi-document highlight responses.", + }, + { + name: "MultiDocumentHighlightParams", + properties: [ + { + name: "textDocument", + type: { kind: "reference", name: "TextDocumentIdentifier" }, + documentation: "The text document.", + }, + { + name: "position", + type: { kind: "reference", name: "Position" }, + documentation: "The position inside the text document.", + }, + { + name: "filesToSearch", + type: { kind: "array", element: { kind: "base", name: "DocumentUri" } }, + documentation: "The list of file URIs to search for highlights across.", + }, + ], + documentation: "Parameters for the custom/textDocument/multiDocumentHighlight request.", + }, ]; const customEnumerations: Enumeration[] = [ @@ -408,6 +445,20 @@ const customRequests: Request[] = [ messageDirection: "clientToServer", documentation: "Returns project information (e.g. the tsconfig.json path) for a given text document.", }, + { + method: "custom/textDocument/multiDocumentHighlight", + typeName: "CustomMultiDocumentHighlightRequest", + params: { kind: "reference", name: "MultiDocumentHighlightParams" }, + result: { + kind: "or", + items: [ + { kind: "array", element: { kind: "reference", name: "MultiDocumentHighlight" } }, + { kind: "base", name: "null" }, + ], + }, + messageDirection: "clientToServer", + documentation: "Request to get document highlights across multiple files.", + }, ]; const customTypeAliases: TypeAlias[] = [ @@ -492,6 +543,16 @@ function patchAndPreprocessModel() { } for (const structure of model.structures) { + // Patch ServerCapabilities to add custom tsgo capability flags + if (structure.name === "ServerCapabilities") { + structure.properties.push({ + name: "customMultiDocumentHighlightProvider", + type: { kind: "base", name: "boolean" }, + optional: true, + documentation: "The server provides multi-document highlight support via custom/textDocument/multiDocumentHighlight.", + }); + } + for (const prop of structure.properties) { // Replace initializationOptions type with custom InitializationOptions if (prop.name === "initializationOptions" && prop.type.kind === "reference" && prop.type.name === "LSPAny") { diff --git a/internal/lsp/lsproto/lsp_generated.go b/internal/lsp/lsproto/lsp_generated.go index 176c8cf292d..5c8bbf831d8 100644 --- a/internal/lsp/lsproto/lsp_generated.go +++ b/internal/lsp/lsproto/lsp_generated.go @@ -17140,6 +17140,9 @@ type ServerCapabilities struct { // Workspace specific server capabilities. Workspace *WorkspaceOptions `json:"workspace,omitzero"` + + // The server provides multi-document highlight support via custom/textDocument/multiDocumentHighlight. + CustomMultiDocumentHighlightProvider *bool `json:"customMultiDocumentHighlightProvider,omitzero"` } var _ json.UnmarshalerFrom = (*ServerCapabilities)(nil) @@ -17396,6 +17399,13 @@ func (s *ServerCapabilities) UnmarshalJSONFrom(dec *json.Decoder) error { if err := json.UnmarshalDecode(dec, &s.Workspace); err != nil { return err } + case `"customMultiDocumentHighlightProvider"`: + if dec.PeekKind() == 'n' { + return errNull("customMultiDocumentHighlightProvider") + } + if err := json.UnmarshalDecode(dec, &s.CustomMultiDocumentHighlightProvider); err != nil { + return err + } default: if err := dec.SkipValue(); err != nil { return err @@ -28819,6 +28829,166 @@ func (s *ProjectInfoResult) UnmarshalJSONFrom(dec *json.Decoder) error { return nil } +// Represents a collection of document highlights from a single document, used in multi-document highlight responses. +type MultiDocumentHighlight struct { + // The URI of the document containing the highlights. + Uri DocumentUri `json:"uri"` + + // The highlights for the document. + Highlights []*DocumentHighlight `json:"highlights"` +} + +var _ json.UnmarshalerFrom = (*MultiDocumentHighlight)(nil) + +func (s *MultiDocumentHighlight) UnmarshalJSONFrom(dec *json.Decoder) error { + const ( + missingUri uint = 1 << iota + missingHighlights + _missingLast + ) + missing := _missingLast - 1 + + if k := dec.PeekKind(); k != '{' { + return errNotObject(k) + } + if _, err := dec.ReadToken(); err != nil { + return err + } + + for dec.PeekKind() != '}' { + name, err := dec.ReadValue() + if err != nil { + return err + } + switch string(name) { + case `"uri"`: + missing &^= missingUri + if err := json.UnmarshalDecode(dec, &s.Uri); err != nil { + return err + } + case `"highlights"`: + missing &^= missingHighlights + if dec.PeekKind() == 'n' { + return errNull("highlights") + } + if err := json.UnmarshalDecode(dec, &s.Highlights); err != nil { + return err + } + default: + if err := dec.SkipValue(); err != nil { + return err + } + } + } + + if _, err := dec.ReadToken(); err != nil { + return err + } + + if missing != 0 { + var missingProps []string + if missing&missingUri != 0 { + missingProps = append(missingProps, "uri") + } + if missing&missingHighlights != 0 { + missingProps = append(missingProps, "highlights") + } + return errMissing(missingProps) + } + + return nil +} + +// Parameters for the custom/textDocument/multiDocumentHighlight request. +type MultiDocumentHighlightParams struct { + // The text document. + TextDocument TextDocumentIdentifier `json:"textDocument"` + + // The position inside the text document. + Position Position `json:"position"` + + // The list of file URIs to search for highlights across. + FilesToSearch []DocumentUri `json:"filesToSearch"` +} + +func (s *MultiDocumentHighlightParams) TextDocumentURI() DocumentUri { + return s.TextDocument.Uri +} + +func (s *MultiDocumentHighlightParams) TextDocumentPosition() Position { + return s.Position +} + +var _ json.UnmarshalerFrom = (*MultiDocumentHighlightParams)(nil) + +func (s *MultiDocumentHighlightParams) UnmarshalJSONFrom(dec *json.Decoder) error { + const ( + missingTextDocument uint = 1 << iota + missingPosition + missingFilesToSearch + _missingLast + ) + missing := _missingLast - 1 + + if k := dec.PeekKind(); k != '{' { + return errNotObject(k) + } + if _, err := dec.ReadToken(); err != nil { + return err + } + + for dec.PeekKind() != '}' { + name, err := dec.ReadValue() + if err != nil { + return err + } + switch string(name) { + case `"textDocument"`: + missing &^= missingTextDocument + if err := json.UnmarshalDecode(dec, &s.TextDocument); err != nil { + return err + } + case `"position"`: + missing &^= missingPosition + if err := json.UnmarshalDecode(dec, &s.Position); err != nil { + return err + } + case `"filesToSearch"`: + missing &^= missingFilesToSearch + if dec.PeekKind() == 'n' { + return errNull("filesToSearch") + } + if err := json.UnmarshalDecode(dec, &s.FilesToSearch); err != nil { + return err + } + default: + if err := dec.SkipValue(); err != nil { + return err + } + } + } + + if _, err := dec.ReadToken(); err != nil { + return err + } + + if missing != 0 { + var missingProps []string + if missing&missingTextDocument != 0 { + missingProps = append(missingProps, "textDocument") + } + if missing&missingPosition != 0 { + missingProps = append(missingProps, "position") + } + if missing&missingFilesToSearch != 0 { + missingProps = append(missingProps, "filesToSearch") + } + return errMissing(missingProps) + } + + return nil +} + // CallHierarchyItemData is a placeholder for custom data preserved on a CallHierarchyItem. type CallHierarchyItemData struct{} @@ -30203,6 +30373,8 @@ func unmarshalParams(method Method, data []byte) (any, error) { return unmarshalPtrTo[InitializeAPISessionParams](data) case MethodCustomProjectInfo: return unmarshalPtrTo[ProjectInfoParams](data) + case MethodCustomTextDocumentMultiDocumentHighlight: + return unmarshalPtrTo[MultiDocumentHighlightParams](data) case MethodWorkspaceDidChangeWorkspaceFolders: return unmarshalPtrTo[DidChangeWorkspaceFoldersParams](data) case MethodWindowWorkDoneProgressCancel: @@ -30408,6 +30580,8 @@ func unmarshalResult(method Method, data []byte) (any, error) { return unmarshalValue[CustomInitializeAPISessionResponse](data) case MethodCustomProjectInfo: return unmarshalValue[CustomProjectInfoResponse](data) + case MethodCustomTextDocumentMultiDocumentHighlight: + return unmarshalValue[CustomMultiDocumentHighlightResponse](data) default: return unmarshalAny(data) } @@ -30728,6 +30902,8 @@ const ( MethodCustomInitializeAPISession Method = "custom/initializeAPISession" // Returns project information (e.g. the tsconfig.json path) for a given text document. MethodCustomProjectInfo Method = "custom/projectInfo" + // Request to get document highlights across multiple files. + MethodCustomTextDocumentMultiDocumentHighlight Method = "custom/textDocument/multiDocumentHighlight" // The `workspace/didChangeWorkspaceFolders` notification is sent from the client to the server when the workspace // folder configuration changes. MethodWorkspaceDidChangeWorkspaceFolders Method = "workspace/didChangeWorkspaceFolders" @@ -31273,6 +31449,12 @@ type CustomProjectInfoResponse = *ProjectInfoResult // Type mapping info for `custom/projectInfo` var CustomProjectInfoInfo = RequestInfo[*ProjectInfoParams, CustomProjectInfoResponse]{Method: MethodCustomProjectInfo} +// Response type for `custom/textDocument/multiDocumentHighlight` +type CustomMultiDocumentHighlightResponse = MultiDocumentHighlightsOrNull + +// Type mapping info for `custom/textDocument/multiDocumentHighlight` +var CustomTextDocumentMultiDocumentHighlightInfo = RequestInfo[*MultiDocumentHighlightParams, CustomMultiDocumentHighlightResponse]{Method: MethodCustomTextDocumentMultiDocumentHighlight} + // Type mapping info for `workspace/didChangeWorkspaceFolders` var WorkspaceDidChangeWorkspaceFoldersInfo = NotificationInfo[*DidChangeWorkspaceFoldersParams]{Method: MethodWorkspaceDidChangeWorkspaceFolders} @@ -34784,6 +34966,36 @@ func (o *CustomClosingTagCompletionOrNull) UnmarshalJSONFrom(dec *json.Decoder) } } +type MultiDocumentHighlightsOrNull struct { + MultiDocumentHighlights *[]*MultiDocumentHighlight +} + +var _ json.MarshalerTo = (*MultiDocumentHighlightsOrNull)(nil) + +func (o *MultiDocumentHighlightsOrNull) MarshalJSONTo(enc *json.Encoder) error { + if o.MultiDocumentHighlights != nil { + return json.MarshalEncode(enc, o.MultiDocumentHighlights) + } + return enc.WriteToken(json.Null) +} + +var _ json.UnmarshalerFrom = (*MultiDocumentHighlightsOrNull)(nil) + +func (o *MultiDocumentHighlightsOrNull) UnmarshalJSONFrom(dec *json.Decoder) error { + *o = MultiDocumentHighlightsOrNull{} + + switch dec.PeekKind() { + case 'n': + _, err := dec.ReadToken() + return err + case '[': + o.MultiDocumentHighlights = new([]*MultiDocumentHighlight) + return json.UnmarshalDecode(dec, o.MultiDocumentHighlights) + default: + return errInvalidKind("MultiDocumentHighlightsOrNull", dec.PeekKind()) + } +} + type RequestFailureTelemetryEventOrNull struct { RequestFailureTelemetryEvent *RequestFailureTelemetryEvent } diff --git a/internal/lsp/server.go b/internal/lsp/server.go index c51b2ad6fa8..4ba2987f358 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -684,6 +684,7 @@ var handlers = sync.OnceValue(func() handlerMap { registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentOnTypeFormattingInfo, (*Server).handleDocumentOnTypeFormat) registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentDocumentSymbolInfo, (*Server).handleDocumentSymbol) registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentDocumentHighlightInfo, (*Server).handleDocumentHighlight) + registerLanguageServiceDocumentRequestHandler(handlers, lsproto.CustomTextDocumentMultiDocumentHighlightInfo, (*Server).handleMultiDocumentHighlight) registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentSelectionRangeInfo, (*Server).handleSelectionRange) registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentInlayHintInfo, (*Server).handleInlayHint) registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentCodeLensInfo, (*Server).handleCodeLens) @@ -1059,6 +1060,7 @@ func (s *Server) handleInitialize(ctx context.Context, params *lsproto.Initializ CallHierarchyProvider: &lsproto.BooleanOrCallHierarchyOptionsOrCallHierarchyRegistrationOptions{ Boolean: new(true), }, + CustomMultiDocumentHighlightProvider: new(true), }, } @@ -1329,6 +1331,10 @@ func (s *Server) handleDocumentHighlight(ctx context.Context, ls *ls.LanguageSer return ls.ProvideDocumentHighlights(ctx, params.TextDocument.Uri, params.Position) } +func (s *Server) handleMultiDocumentHighlight(ctx context.Context, ls *ls.LanguageService, params *lsproto.MultiDocumentHighlightParams) (lsproto.CustomMultiDocumentHighlightResponse, error) { + return ls.ProvideMultiDocumentHighlights(ctx, params.TextDocument.Uri, params.Position, params.FilesToSearch) +} + func (s *Server) handleSelectionRange(ctx context.Context, ls *ls.LanguageService, params *lsproto.SelectionRangeParams) (lsproto.SelectionRangeResponse, error) { return ls.ProvideSelectionRanges(ctx, params) } diff --git a/package-lock.json b/package-lock.json index 4200a9a8cde..0c8f4735f9d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41,12 +41,12 @@ "vscode-languageclient": "^10.0.0-next.21" }, "devDependencies": { - "@types/vscode": "~1.106.1", + "@types/vscode": "~1.110.0", "@vscode/vsce": "^3.7.1", "esbuild": "^0.27.4" }, "engines": { - "vscode": "^1.106.0" + "vscode": "^1.110.0" } }, "_packages/api": { @@ -1401,9 +1401,9 @@ "license": "MIT" }, "node_modules/@types/vscode": { - "version": "1.106.1", - "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.106.1.tgz", - "integrity": "sha512-R/HV8u2h8CAddSbX8cjpdd7B8/GnE4UjgjpuGuHcbp1xV6yh4OeqU4L1pKjlwujCrSFS0MOpwJAIs/NexMB1fQ==", + "version": "1.110.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.110.0.tgz", + "integrity": "sha512-AGuxUEpU4F4mfuQjxPPaQVyuOMhs+VT/xRok1jiHVBubHK7lBRvCuOMZG0LKUwxncrPorJ5qq/uil3IdZBd5lA==", "dev": true, "license": "MIT" }, diff --git a/testdata/baselines/reference/fourslash/documentHighlights/documentHighlights02.baseline.jsonc b/testdata/baselines/reference/fourslash/documentHighlights/documentHighlights02.baseline.jsonc new file mode 100644 index 00000000000..d53d228bb5a --- /dev/null +++ b/testdata/baselines/reference/fourslash/documentHighlights/documentHighlights02.baseline.jsonc @@ -0,0 +1,34 @@ +// === documentHighlights === +// filesToSearch: +// a.ts +// b.ts + +// === /a.ts === +// function /*HIGHLIGHTS*/[|foo|] () { +// return 1; +// } +// [|foo|](); + + + +// === documentHighlights === +// filesToSearch: +// a.ts +// b.ts + +// === /a.ts === +// function [|foo|] () { +// return 1; +// } +// /*HIGHLIGHTS*/[|foo|](); + + + +// === documentHighlights === +// filesToSearch: +// a.ts +// b.ts + +// === /b.ts === +// /// +// /*HIGHLIGHTS*/[|foo|](); \ No newline at end of file diff --git a/testdata/baselines/reference/fourslash/documentHighlights/documentHighlights_33722.baseline.jsonc b/testdata/baselines/reference/fourslash/documentHighlights/documentHighlights_33722.baseline.jsonc index a789840493b..c0a4b45e140 100644 --- a/testdata/baselines/reference/fourslash/documentHighlights/documentHighlights_33722.baseline.jsonc +++ b/testdata/baselines/reference/fourslash/documentHighlights/documentHighlights_33722.baseline.jsonc @@ -1,4 +1,7 @@ // === documentHighlights === +// filesToSearch: +// /x.ts + // === /x.ts === // import y from "./y"; // diff --git a/testdata/baselines/reference/fourslash/documentHighlights/documentHighlights_moduleImport_filesToSearch.baseline.jsonc b/testdata/baselines/reference/fourslash/documentHighlights/documentHighlights_moduleImport_filesToSearch.baseline.jsonc new file mode 100644 index 00000000000..9453b9f6b2c --- /dev/null +++ b/testdata/baselines/reference/fourslash/documentHighlights/documentHighlights_moduleImport_filesToSearch.baseline.jsonc @@ -0,0 +1,25 @@ +// === documentHighlights === +// filesToSearch: +// /a.ts +// /b.ts + +// === /a.ts === +// import * as foo from "foo"; +// foo./*HIGHLIGHTS*/[|x|]; + +// === /b.ts === +// import { [|x|] } from "foo"; + + + +// === documentHighlights === +// filesToSearch: +// /a.ts +// /b.ts + +// === /a.ts === +// import * as foo from "foo"; +// foo.[|x|]; + +// === /b.ts === +// import { /*HIGHLIGHTS*/[|x|] } from "foo"; \ No newline at end of file diff --git a/testdata/baselines/reference/fourslash/documentHighlights/documentHighlights_moduleImport_filesToSearchWithInvalidFile.baseline.jsonc b/testdata/baselines/reference/fourslash/documentHighlights/documentHighlights_moduleImport_filesToSearchWithInvalidFile.baseline.jsonc new file mode 100644 index 00000000000..c9fa5a6dd2d --- /dev/null +++ b/testdata/baselines/reference/fourslash/documentHighlights/documentHighlights_moduleImport_filesToSearchWithInvalidFile.baseline.jsonc @@ -0,0 +1,27 @@ +// === documentHighlights === +// filesToSearch: +// /a.ts +// /b.ts +// /unknown.ts + +// === /a.ts === +// import * as foo from "foo"; +// foo./*HIGHLIGHTS*/[|x|]; + +// === /b.ts === +// import { [|x|] } from "foo"; + + + +// === documentHighlights === +// filesToSearch: +// /a.ts +// /b.ts +// /unknown.ts + +// === /a.ts === +// import * as foo from "foo"; +// foo.[|x|]; + +// === /b.ts === +// import { /*HIGHLIGHTS*/[|x|] } from "foo"; \ No newline at end of file diff --git a/testdata/baselines/reference/fourslash/documentHighlights/documentHighlights_windowsPath.baseline.jsonc b/testdata/baselines/reference/fourslash/documentHighlights/documentHighlights_windowsPath.baseline.jsonc new file mode 100644 index 00000000000..93a220cd7d1 --- /dev/null +++ b/testdata/baselines/reference/fourslash/documentHighlights/documentHighlights_windowsPath.baseline.jsonc @@ -0,0 +1,6 @@ +// === documentHighlights === +// filesToSearch: +// C:/a/b/c.ts + +// === C:/a/b/c.ts === +// var /*HIGHLIGHTS*/x = 1; \ No newline at end of file diff --git a/testdata/baselines/reference/fourslash/documentHighlights/exportInLabeledStatement.baseline.jsonc b/testdata/baselines/reference/fourslash/documentHighlights/exportInLabeledStatement.baseline.jsonc index c36893dcbf3..c43b2bad5ed 100644 --- a/testdata/baselines/reference/fourslash/documentHighlights/exportInLabeledStatement.baseline.jsonc +++ b/testdata/baselines/reference/fourslash/documentHighlights/exportInLabeledStatement.baseline.jsonc @@ -1,4 +1,7 @@ // === documentHighlights === +// filesToSearch: +// /a.ts + // === /a.ts === // subTitle: // /*HIGHLIGHTS*/export const title: string \ No newline at end of file diff --git a/testdata/baselines/reference/fourslash/documentHighlights/exportInObjectLiteral.baseline.jsonc b/testdata/baselines/reference/fourslash/documentHighlights/exportInObjectLiteral.baseline.jsonc index da96d37cf57..32990928486 100644 --- a/testdata/baselines/reference/fourslash/documentHighlights/exportInObjectLiteral.baseline.jsonc +++ b/testdata/baselines/reference/fourslash/documentHighlights/exportInObjectLiteral.baseline.jsonc @@ -1,4 +1,7 @@ // === documentHighlights === +// filesToSearch: +// /a.ts + // === /a.ts === // const k = { // /*HIGHLIGHTS*/export f() { } diff --git a/testdata/baselines/reference/fourslash/documentHighlights/findAllRefsForModule.baseline.jsonc b/testdata/baselines/reference/fourslash/documentHighlights/findAllRefsForModule.baseline.jsonc index c3259d6a17a..282b3c9e0e9 100644 --- a/testdata/baselines/reference/fourslash/documentHighlights/findAllRefsForModule.baseline.jsonc +++ b/testdata/baselines/reference/fourslash/documentHighlights/findAllRefsForModule.baseline.jsonc @@ -1,15 +1,48 @@ // === documentHighlights === +// filesToSearch: +// /b.ts +// /c/sub.js +// /d.ts + // === /b.ts === // import { x } from "/*HIGHLIGHTS*/[|./a|]"; +// === /c/sub.js === +// const a = require("[|../a|]"); + +// === /d.ts === +// /// + // === documentHighlights === +// filesToSearch: +// /b.ts +// /c/sub.js +// /d.ts + +// === /b.ts === +// import { x } from "[|./a|]"; + // === /c/sub.js === // const a = require("/*HIGHLIGHTS*/[|../a|]"); +// === /d.ts === +// /// + // === documentHighlights === +// filesToSearch: +// /b.ts +// /c/sub.js +// /d.ts + +// === /b.ts === +// import { x } from "[|./a|]"; + +// === /c/sub.js === +// const a = require("[|../a|]"); + // === /d.ts === // /// \ No newline at end of file From 364d84c353d75d6367c00fcab411115c982fd23d Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Fri, 3 Apr 2026 09:47:55 -0700 Subject: [PATCH 02/11] PR feedback --- _extension/package.json | 3 ++- .../src/languageFeatures/documentHighlight.ts | 13 +++---------- internal/ls/documenthighlights.go | 10 ++++++++-- package-lock.json | 3 ++- 4 files changed, 15 insertions(+), 14 deletions(-) diff --git a/_extension/package.json b/_extension/package.json index d54dfe48808..d0d34a45327 100644 --- a/_extension/package.json +++ b/_extension/package.json @@ -176,7 +176,8 @@ }, "dependencies": { "@vscode/extension-telemetry": "^1.5.1", - "vscode-languageclient": "^10.0.0-next.21" + "vscode-languageclient": "10.0.0-next.21", + "vscode-languageserver-protocol": "3.17.6-next.17" }, "devDependencies": { "@types/vscode": "~1.110.0", diff --git a/_extension/src/languageFeatures/documentHighlight.ts b/_extension/src/languageFeatures/documentHighlight.ts index 578c9d06858..2dabde99d82 100644 --- a/_extension/src/languageFeatures/documentHighlight.ts +++ b/_extension/src/languageFeatures/documentHighlight.ts @@ -1,5 +1,6 @@ import * as vscode from "vscode"; import { LanguageClient } from "vscode-languageclient/node"; +import type { DocumentHighlight } from "vscode-languageserver-protocol"; const multiDocumentHighlightMethod = "custom/textDocument/multiDocumentHighlight"; @@ -11,7 +12,7 @@ interface MultiDocumentHighlightParams { interface MultiDocumentHighlightItem { uri: string; - highlights: { range: { start: { line: number; character: number; }; end: { line: number; character: number; }; }; kind?: number; }[]; + highlights: DocumentHighlight[]; } class MultiDocumentHighlightProvider implements vscode.MultiDocumentHighlightProvider { @@ -52,15 +53,7 @@ class MultiDocumentHighlightProvider implements vscode.MultiDocumentHighlightPro return response.map(item => new vscode.MultiDocumentHighlight( vscode.Uri.parse(item.uri), - item.highlights.map(h => - new vscode.DocumentHighlight( - new vscode.Range( - new vscode.Position(h.range.start.line, h.range.start.character), - new vscode.Position(h.range.end.line, h.range.end.character), - ), - h.kind === 3 ? vscode.DocumentHighlightKind.Write : vscode.DocumentHighlightKind.Read, - ) - ), + item.highlights.map(h => this.client.protocol2CodeConverter.asDocumentHighlight(h)), ) ); } diff --git a/internal/ls/documenthighlights.go b/internal/ls/documenthighlights.go index 70334545c48..1940d889713 100644 --- a/internal/ls/documenthighlights.go +++ b/internal/ls/documenthighlights.go @@ -5,6 +5,7 @@ import ( "github.com/microsoft/typescript-go/internal/ast" "github.com/microsoft/typescript-go/internal/astnav" + "github.com/microsoft/typescript-go/internal/collections" "github.com/microsoft/typescript-go/internal/compiler" "github.com/microsoft/typescript-go/internal/ls/lsconv" "github.com/microsoft/typescript-go/internal/ls/lsutil" @@ -55,10 +56,15 @@ func (l *LanguageService) ProvideMultiDocumentHighlights(ctx context.Context, do position := int(l.converters.LineAndCharacterToPosition(sourceFile, documentPosition)) node := astnav.GetTouchingPropertyName(sourceFile, position) - // Resolve the source files to search + // Resolve the source files to search, deduplicating by file name. var sourceFiles []*ast.SourceFile + seenFiles := collections.NewSetWithSizeHint[string](len(filesToSearch)) for _, uri := range filesToSearch { - if sf := program.GetSourceFile(uri.FileName()); sf != nil { + fileName := uri.FileName() + if !seenFiles.AddIfAbsent(fileName) { + continue + } + if sf := program.GetSourceFile(fileName); sf != nil { sourceFiles = append(sourceFiles, sf) } } diff --git a/package-lock.json b/package-lock.json index 0c8f4735f9d..50a854ffd98 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,7 +38,8 @@ "version": "0.0.0", "dependencies": { "@vscode/extension-telemetry": "^1.5.1", - "vscode-languageclient": "^10.0.0-next.21" + "vscode-languageclient": "10.0.0-next.21", + "vscode-languageserver-protocol": "3.17.6-next.17" }, "devDependencies": { "@types/vscode": "~1.110.0", From 327466e1a4827c5fe98054aefe4109b16fb43b07 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Mon, 6 Apr 2026 11:36:06 -0700 Subject: [PATCH 03/11] Dedupe, duh --- internal/ls/documenthighlights.go | 78 ++++++++++--------------------- 1 file changed, 25 insertions(+), 53 deletions(-) diff --git a/internal/ls/documenthighlights.go b/internal/ls/documenthighlights.go index 1940d889713..6f6e62a0e0c 100644 --- a/internal/ls/documenthighlights.go +++ b/internal/ls/documenthighlights.go @@ -16,45 +16,24 @@ import ( ) func (l *LanguageService) ProvideDocumentHighlights(ctx context.Context, documentUri lsproto.DocumentUri, documentPosition lsproto.Position) (lsproto.DocumentHighlightResponse, error) { - program, sourceFile := l.getProgramAndFile(documentUri) - position := int(l.converters.LineAndCharacterToPosition(sourceFile, documentPosition)) - node := astnav.GetTouchingPropertyName(sourceFile, position) - if node.Parent != nil && (node.Parent.Kind == ast.KindJsxClosingElement || (node.Parent.Kind == ast.KindJsxOpeningElement && node.Parent.TagName() == node)) { - var openingElement, closingElement *ast.Node - if ast.IsJsxElement(node.Parent.Parent) { - openingElement = node.Parent.Parent.AsJsxElement().OpeningElement - closingElement = node.Parent.Parent.AsJsxElement().ClosingElement - } - var documentHighlights []*lsproto.DocumentHighlight - kind := lsproto.DocumentHighlightKindRead - if openingElement != nil { - documentHighlights = append(documentHighlights, &lsproto.DocumentHighlight{ - Range: l.createLspRangeFromNode(openingElement, sourceFile), - Kind: &kind, - }) - } - if closingElement != nil { - documentHighlights = append(documentHighlights, &lsproto.DocumentHighlight{ - Range: l.createLspRangeFromNode(closingElement, sourceFile), - Kind: &kind, - }) + result, err := l.provideDocumentHighlightsWorker(ctx, documentUri, documentPosition, []*ast.SourceFile{}) + if err != nil { + return lsproto.DocumentHighlightsOrNull{}, err + } + // Extract highlights for the current file only. + var documentHighlights []*lsproto.DocumentHighlight + if result.MultiDocumentHighlights != nil { + for _, mh := range *result.MultiDocumentHighlights { + if mh.Uri == documentUri { + documentHighlights = append(documentHighlights, mh.Highlights...) + } } - return lsproto.DocumentHighlightsOrNull{ - DocumentHighlights: &documentHighlights, - }, nil - } - documentHighlights := l.getSemanticDocumentHighlights(ctx, position, node, program, sourceFile) - if len(documentHighlights) == 0 { - documentHighlights = l.getSyntacticDocumentHighlights(node, sourceFile) } - // if nil is passed here we never generate an error, just pass an empty highlight return lsproto.DocumentHighlightsOrNull{DocumentHighlights: &documentHighlights}, nil } func (l *LanguageService) ProvideMultiDocumentHighlights(ctx context.Context, documentUri lsproto.DocumentUri, documentPosition lsproto.Position, filesToSearch []lsproto.DocumentUri) (lsproto.CustomMultiDocumentHighlightResponse, error) { program, sourceFile := l.getProgramAndFile(documentUri) - position := int(l.converters.LineAndCharacterToPosition(sourceFile, documentPosition)) - node := astnav.GetTouchingPropertyName(sourceFile, position) // Resolve the source files to search, deduplicating by file name. var sourceFiles []*ast.SourceFile @@ -72,6 +51,18 @@ func (l *LanguageService) ProvideMultiDocumentHighlights(ctx context.Context, do sourceFiles = []*ast.SourceFile{sourceFile} } + return l.provideDocumentHighlightsWorker(ctx, documentUri, documentPosition, sourceFiles) +} + +func (l *LanguageService) provideDocumentHighlightsWorker(ctx context.Context, documentUri lsproto.DocumentUri, documentPosition lsproto.Position, sourceFiles []*ast.SourceFile) (lsproto.MultiDocumentHighlightsOrNull, error) { + program, sourceFile := l.getProgramAndFile(documentUri) + position := int(l.converters.LineAndCharacterToPosition(sourceFile, documentPosition)) + node := astnav.GetTouchingPropertyName(sourceFile, position) + + if len(sourceFiles) == 0 { + sourceFiles = []*ast.SourceFile{sourceFile} + } + if node.Parent != nil && (node.Parent.Kind == ast.KindJsxClosingElement || (node.Parent.Kind == ast.KindJsxOpeningElement && node.Parent.TagName() == node)) { var openingElement, closingElement *ast.Node if ast.IsJsxElement(node.Parent.Parent) { @@ -100,7 +91,7 @@ func (l *LanguageService) ProvideMultiDocumentHighlights(ctx context.Context, do }, nil } - multiHighlights := l.getMultiFileSemanticDocumentHighlights(ctx, position, node, program, sourceFiles) + multiHighlights := l.getSemanticDocumentHighlights(ctx, position, node, program, sourceFiles) if len(multiHighlights) == 0 { // Fall back to syntactic highlights for the current file only. syntacticHighlights := l.getSyntacticDocumentHighlights(node, sourceFile) @@ -113,26 +104,7 @@ func (l *LanguageService) ProvideMultiDocumentHighlights(ctx context.Context, do return lsproto.MultiDocumentHighlightsOrNull{MultiDocumentHighlights: &multiHighlights}, nil } -func (l *LanguageService) getSemanticDocumentHighlights(ctx context.Context, position int, node *ast.Node, program *compiler.Program, sourceFile *ast.SourceFile) []*lsproto.DocumentHighlight { - options := refOptions{use: referenceUseNone} - referenceEntries := l.getReferencedSymbolsForNode(ctx, position, node, program, []*ast.SourceFile{sourceFile}, options) - if referenceEntries == nil { - return nil - } - - var highlights []*lsproto.DocumentHighlight - for _, entry := range referenceEntries { - for _, ref := range entry.references { - fileName, highlight := l.toDocumentHighlight(ref) - if fileName == sourceFile.FileName() { - highlights = append(highlights, highlight) - } - } - } - return highlights -} - -func (l *LanguageService) getMultiFileSemanticDocumentHighlights(ctx context.Context, position int, node *ast.Node, program *compiler.Program, sourceFiles []*ast.SourceFile) []*lsproto.MultiDocumentHighlight { +func (l *LanguageService) getSemanticDocumentHighlights(ctx context.Context, position int, node *ast.Node, program *compiler.Program, sourceFiles []*ast.SourceFile) []*lsproto.MultiDocumentHighlight { options := refOptions{use: referenceUseNone} referenceEntries := l.getReferencedSymbolsForNode(ctx, position, node, program, sourceFiles, options) if referenceEntries == nil { From 4f1fbc458f7536e03d2d58bb01e710f750b34d2d Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Mon, 6 Apr 2026 11:38:24 -0700 Subject: [PATCH 04/11] Cheap first --- internal/ls/documenthighlights.go | 45 ++++++++++++++----------------- 1 file changed, 20 insertions(+), 25 deletions(-) diff --git a/internal/ls/documenthighlights.go b/internal/ls/documenthighlights.go index 6f6e62a0e0c..859b7ab2e53 100644 --- a/internal/ls/documenthighlights.go +++ b/internal/ls/documenthighlights.go @@ -16,7 +16,7 @@ import ( ) func (l *LanguageService) ProvideDocumentHighlights(ctx context.Context, documentUri lsproto.DocumentUri, documentPosition lsproto.Position) (lsproto.DocumentHighlightResponse, error) { - result, err := l.provideDocumentHighlightsWorker(ctx, documentUri, documentPosition, []*ast.SourceFile{}) + result, err := l.provideDocumentHighlightsWorker(ctx, documentUri, documentPosition, nil) if err != nil { return lsproto.DocumentHighlightsOrNull{}, err } @@ -33,36 +33,15 @@ func (l *LanguageService) ProvideDocumentHighlights(ctx context.Context, documen } func (l *LanguageService) ProvideMultiDocumentHighlights(ctx context.Context, documentUri lsproto.DocumentUri, documentPosition lsproto.Position, filesToSearch []lsproto.DocumentUri) (lsproto.CustomMultiDocumentHighlightResponse, error) { - program, sourceFile := l.getProgramAndFile(documentUri) - - // Resolve the source files to search, deduplicating by file name. - var sourceFiles []*ast.SourceFile - seenFiles := collections.NewSetWithSizeHint[string](len(filesToSearch)) - for _, uri := range filesToSearch { - fileName := uri.FileName() - if !seenFiles.AddIfAbsent(fileName) { - continue - } - if sf := program.GetSourceFile(fileName); sf != nil { - sourceFiles = append(sourceFiles, sf) - } - } - if len(sourceFiles) == 0 { - sourceFiles = []*ast.SourceFile{sourceFile} - } - - return l.provideDocumentHighlightsWorker(ctx, documentUri, documentPosition, sourceFiles) + return l.provideDocumentHighlightsWorker(ctx, documentUri, documentPosition, filesToSearch) } -func (l *LanguageService) provideDocumentHighlightsWorker(ctx context.Context, documentUri lsproto.DocumentUri, documentPosition lsproto.Position, sourceFiles []*ast.SourceFile) (lsproto.MultiDocumentHighlightsOrNull, error) { +func (l *LanguageService) provideDocumentHighlightsWorker(ctx context.Context, documentUri lsproto.DocumentUri, documentPosition lsproto.Position, filesToSearch []lsproto.DocumentUri) (lsproto.MultiDocumentHighlightsOrNull, error) { program, sourceFile := l.getProgramAndFile(documentUri) position := int(l.converters.LineAndCharacterToPosition(sourceFile, documentPosition)) node := astnav.GetTouchingPropertyName(sourceFile, position) - if len(sourceFiles) == 0 { - sourceFiles = []*ast.SourceFile{sourceFile} - } - + // Cheap JSX check before resolving files to search. if node.Parent != nil && (node.Parent.Kind == ast.KindJsxClosingElement || (node.Parent.Kind == ast.KindJsxOpeningElement && node.Parent.TagName() == node)) { var openingElement, closingElement *ast.Node if ast.IsJsxElement(node.Parent.Parent) { @@ -91,6 +70,22 @@ func (l *LanguageService) provideDocumentHighlightsWorker(ctx context.Context, d }, nil } + // Resolve the source files to search, deduplicating by file name. + var sourceFiles []*ast.SourceFile + seenFiles := collections.NewSetWithSizeHint[string](len(filesToSearch)) + for _, uri := range filesToSearch { + fileName := uri.FileName() + if !seenFiles.AddIfAbsent(fileName) { + continue + } + if sf := program.GetSourceFile(fileName); sf != nil { + sourceFiles = append(sourceFiles, sf) + } + } + if len(sourceFiles) == 0 { + sourceFiles = []*ast.SourceFile{sourceFile} + } + multiHighlights := l.getSemanticDocumentHighlights(ctx, position, node, program, sourceFiles) if len(multiHighlights) == 0 { // Fall back to syntactic highlights for the current file only. From 7e8a7b81e175e08f56ed706d2e62a9f2f0465ac8 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Mon, 6 Apr 2026 11:40:55 -0700 Subject: [PATCH 05/11] Collections set --- internal/ls/documenthighlights.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/ls/documenthighlights.go b/internal/ls/documenthighlights.go index 859b7ab2e53..6906431772c 100644 --- a/internal/ls/documenthighlights.go +++ b/internal/ls/documenthighlights.go @@ -107,9 +107,9 @@ func (l *LanguageService) getSemanticDocumentHighlights(ctx context.Context, pos } // Build a set of allowed file names for quick lookup - allowedFiles := make(map[string]bool, len(sourceFiles)) + allowedFiles := collections.NewSetWithSizeHint[string](len(sourceFiles)) for _, sf := range sourceFiles { - allowedFiles[sf.FileName()] = true + allowedFiles.Add(sf.FileName()) } // Group highlights by file @@ -117,7 +117,7 @@ func (l *LanguageService) getSemanticDocumentHighlights(ctx context.Context, pos for _, entry := range referenceEntries { for _, ref := range entry.references { fileName, highlight := l.toDocumentHighlight(ref) - if allowedFiles[fileName] { + if allowedFiles.Has(fileName) { fileHighlights[fileName] = append(fileHighlights[fileName], highlight) } } From 12ea635606862e52bdf563d6cc111b01a99b4e5d Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Mon, 6 Apr 2026 11:44:13 -0700 Subject: [PATCH 06/11] Remove redundant check --- internal/ls/documenthighlights.go | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/internal/ls/documenthighlights.go b/internal/ls/documenthighlights.go index 6906431772c..7494ee707ea 100644 --- a/internal/ls/documenthighlights.go +++ b/internal/ls/documenthighlights.go @@ -106,20 +106,12 @@ func (l *LanguageService) getSemanticDocumentHighlights(ctx context.Context, pos return nil } - // Build a set of allowed file names for quick lookup - allowedFiles := collections.NewSetWithSizeHint[string](len(sourceFiles)) - for _, sf := range sourceFiles { - allowedFiles.Add(sf.FileName()) - } - // Group highlights by file fileHighlights := make(map[string][]*lsproto.DocumentHighlight) for _, entry := range referenceEntries { for _, ref := range entry.references { fileName, highlight := l.toDocumentHighlight(ref) - if allowedFiles.Has(fileName) { - fileHighlights[fileName] = append(fileHighlights[fileName], highlight) - } + fileHighlights[fileName] = append(fileHighlights[fileName], highlight) } } From 4526919682245d7e89234e6138fab138b2905c5f Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Wed, 8 Apr 2026 14:33:48 -0700 Subject: [PATCH 07/11] Fmt --- internal/lsp/server.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/lsp/server.go b/internal/lsp/server.go index 82b077ab1ba..2b970f997c8 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -1081,7 +1081,7 @@ func (s *Server) handleInitialize(ctx context.Context, params *lsproto.Initializ CallHierarchyProvider: &lsproto.BooleanOrCallHierarchyOptionsOrCallHierarchyRegistrationOptions{ Boolean: new(true), }, - CustomSourceDefinitionProvider: new(true), + CustomSourceDefinitionProvider: new(true), CustomMultiDocumentHighlightProvider: new(true), }, } From 57ae256627757fd0488d8cdcdf07060322f5d345 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Wed, 8 Apr 2026 15:56:19 -0700 Subject: [PATCH 08/11] Guard against the proposal not being there --- .../src/languageFeatures/documentHighlight.ts | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/_extension/src/languageFeatures/documentHighlight.ts b/_extension/src/languageFeatures/documentHighlight.ts index 2dabde99d82..ba9c58f9fc6 100644 --- a/_extension/src/languageFeatures/documentHighlight.ts +++ b/_extension/src/languageFeatures/documentHighlight.ts @@ -50,12 +50,18 @@ class MultiDocumentHighlightProvider implements vscode.MultiDocumentHighlightPro return []; } - return response.map(item => - new vscode.MultiDocumentHighlight( - vscode.Uri.parse(item.uri), - item.highlights.map(h => this.client.protocol2CodeConverter.asDocumentHighlight(h)), - ) - ); + // MultiDocumentHighlight is proposed API; guard against missing or changed constructor. + try { + return response.map(item => + new vscode.MultiDocumentHighlight( + vscode.Uri.parse(item.uri), + item.highlights.map(h => this.client.protocol2CodeConverter.asDocumentHighlight(h)), + ) + ); + } + catch { + return []; + } } } @@ -64,7 +70,8 @@ export function registerMultiDocumentHighlightFeature( client: LanguageClient, ): vscode.Disposable { const capabilities = client.initializeResult?.capabilities as { customMultiDocumentHighlightProvider?: boolean; } | undefined; - if (!capabilities?.customMultiDocumentHighlightProvider) { + // registerMultiDocumentHighlightProvider is proposed API; guard against it not being available. + if (!capabilities?.customMultiDocumentHighlightProvider || typeof vscode.languages.registerMultiDocumentHighlightProvider !== "function") { return { dispose() {} }; } return vscode.languages.registerMultiDocumentHighlightProvider(selector, new MultiDocumentHighlightProvider(client)); From 922cb1b95ac268817b85cb87a0c26f7f1df1ff87 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Mon, 13 Apr 2026 15:24:54 -0700 Subject: [PATCH 09/11] Drop --- _extension/package.json | 3 +-- _extension/src/languageFeatures/documentHighlight.ts | 6 ++++-- package-lock.json | 3 +-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/_extension/package.json b/_extension/package.json index 2e692b6c18e..552cadcf185 100644 --- a/_extension/package.json +++ b/_extension/package.json @@ -198,8 +198,7 @@ }, "dependencies": { "@vscode/extension-telemetry": "^1.5.1", - "vscode-languageclient": "10.0.0-next.21", - "vscode-languageserver-protocol": "3.17.6-next.17" + "vscode-languageclient": "10.0.0-next.21" }, "devDependencies": { "@types/vscode": "~1.110.0", diff --git a/_extension/src/languageFeatures/documentHighlight.ts b/_extension/src/languageFeatures/documentHighlight.ts index ba9c58f9fc6..8c1a014adc9 100644 --- a/_extension/src/languageFeatures/documentHighlight.ts +++ b/_extension/src/languageFeatures/documentHighlight.ts @@ -1,6 +1,8 @@ import * as vscode from "vscode"; -import { LanguageClient } from "vscode-languageclient/node"; -import type { DocumentHighlight } from "vscode-languageserver-protocol"; +import { + DocumentHighlight, + LanguageClient, +} from "vscode-languageclient/node"; const multiDocumentHighlightMethod = "custom/textDocument/multiDocumentHighlight"; diff --git a/package-lock.json b/package-lock.json index 50a854ffd98..e181b7070b8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,8 +38,7 @@ "version": "0.0.0", "dependencies": { "@vscode/extension-telemetry": "^1.5.1", - "vscode-languageclient": "10.0.0-next.21", - "vscode-languageserver-protocol": "3.17.6-next.17" + "vscode-languageclient": "10.0.0-next.21" }, "devDependencies": { "@types/vscode": "~1.110.0", From cc69911fc627ace935f8ab99177b0220fa7b5c11 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Mon, 13 Apr 2026 15:25:17 -0700 Subject: [PATCH 10/11] Drop --- _extension/package.json | 2 +- package-lock.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/_extension/package.json b/_extension/package.json index 552cadcf185..28f94d83819 100644 --- a/_extension/package.json +++ b/_extension/package.json @@ -198,7 +198,7 @@ }, "dependencies": { "@vscode/extension-telemetry": "^1.5.1", - "vscode-languageclient": "10.0.0-next.21" + "vscode-languageclient": "^10.0.0-next.21" }, "devDependencies": { "@types/vscode": "~1.110.0", diff --git a/package-lock.json b/package-lock.json index e181b7070b8..0c8f4735f9d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,7 +38,7 @@ "version": "0.0.0", "dependencies": { "@vscode/extension-telemetry": "^1.5.1", - "vscode-languageclient": "10.0.0-next.21" + "vscode-languageclient": "^10.0.0-next.21" }, "devDependencies": { "@types/vscode": "~1.110.0", From e018476fe6f83ca911b19fe8d515c6cbba461dee Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Mon, 13 Apr 2026 15:57:43 -0700 Subject: [PATCH 11/11] fmt --- _extension/src/client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_extension/src/client.ts b/_extension/src/client.ts index 7b8f55e3c1c..cd62ae7b237 100644 --- a/_extension/src/client.ts +++ b/_extension/src/client.ts @@ -22,8 +22,8 @@ import { configurationMiddleware, sendNotificationMiddleware, } from "./configurationMiddleware"; -import { registerHoverFeature } from "./languageFeatures/hover"; import { registerMultiDocumentHighlightFeature } from "./languageFeatures/documentHighlight"; +import { registerHoverFeature } from "./languageFeatures/hover"; import { registerSourceDefinitionFeature } from "./languageFeatures/sourceDefinition"; import { registerTagClosingFeature } from "./languageFeatures/tagClosing"; import * as tr from "./telemetryReporting";