From 763640592befc81a4b251379f313a751ab136876 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Thu, 4 Dec 2025 15:02:56 -0800 Subject: [PATCH 01/16] Test --- .../tests/stateimplementations_test.go | 121 ++++++++ ...ationsAncestorProjectRefMangement.baseline | 273 ++++++++++++++++++ 2 files changed, 394 insertions(+) create mode 100644 internal/fourslash/tests/stateimplementations_test.go create mode 100644 testdata/baselines/reference/fourslash/state/implementationsAncestorProjectRefMangement.baseline diff --git a/internal/fourslash/tests/stateimplementations_test.go b/internal/fourslash/tests/stateimplementations_test.go new file mode 100644 index 0000000000..7332e48abc --- /dev/null +++ b/internal/fourslash/tests/stateimplementations_test.go @@ -0,0 +1,121 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/fourslash" + "github.com/microsoft/typescript-go/internal/testutil" +) + +func TestImplementationsAncestorProjectRefMangement(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + content := ` +// @stateBaseline: true +// @Filename: /projects/temp/temp.ts +/*temp*/let x = 10 +// @Filename: /projects/temp/tsconfig.json +{} +// @Filename: /projects/container/lib/tsconfig.json +{ + "compilerOptions": { + "composite": true, + }, + references: [], + files: [ + "index.ts", + "bar.ts" + ], +} +// @Filename: /projects/container/lib/index.ts +export interface /*impl*/Foo { + func(); +} +export const val = 42; +// @Filename: /projects/container/lib/bar.ts +import {Foo} from './index' +class A implements Foo { + func() {} +} +class B implements Foo { + func() {} +} +// @Filename: /projects/container/exec/tsconfig.json +{ + "files": ["./index.ts"], + "references": [ + { "path": "../lib" }, + ], +} +// @Filename: /projects/container/exec/index.ts +import { Foo } from "../lib"; +class A1 implements Foo { + func() {} +} +class B1 implements Foo { + func() {} +} +// @Filename: /projects/container/compositeExec/tsconfig.json +{ + "compilerOptions": { + "composite": true, + }, + "files": ["./index.ts"], + "references": [ + { "path": "../lib" }, + ], +} +// @Filename: /projects/container/compositeExec/index.ts +import { Foo } from "../lib"; +class A2 implements Foo { + func() {} +} +class B2 implements Foo { + func() {} +} +// @Filename: /projects/container/tsconfig.json +{ + "files": [], + "include": [], + "references": [ + { "path": "./exec" }, + { "path": "./compositeExec" }, + ], +} +// @Filename: /projects/container/tsconfig.json +{ + "files": [], + "include": [], + "references": [ + { "path": "./exec" }, + { "path": "./compositeExec" }, + ], +} +// @Filename: /projects/container/tsconfig.json +{ + "files": [], + "include": [], + "references": [ + { "path": "./exec" }, + { "path": "./compositeExec" }, + ], +} +` + f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) + defer done() + f.GoToMarker(t, "impl") + // Open temp file and verify all projects alive + f.GoToMarker(t, "temp") + + // Ref projects are loaded after as part of this command + f.VerifyBaselineGoToImplementation(t, "impl") + + // Open temp file and verify all projects alive + f.CloseFileOfMarker(t, "temp") + f.GoToMarker(t, "temp") + + // Close all files and open temp file, only inferred project should be alive + f.CloseFileOfMarker(t, "impl") + f.CloseFileOfMarker(t, "temp") + f.GoToMarker(t, "temp") +} diff --git a/testdata/baselines/reference/fourslash/state/implementationsAncestorProjectRefMangement.baseline b/testdata/baselines/reference/fourslash/state/implementationsAncestorProjectRefMangement.baseline new file mode 100644 index 0000000000..03da210a2e --- /dev/null +++ b/testdata/baselines/reference/fourslash/state/implementationsAncestorProjectRefMangement.baseline @@ -0,0 +1,273 @@ +UseCaseSensitiveFileNames: true +//// [/projects/container/compositeExec/index.ts] *new* +import { Foo } from "../lib"; +class A2 implements Foo { + func() {} +} +class B2 implements Foo { + func() {} +} +//// [/projects/container/compositeExec/tsconfig.json] *new* +{ + "compilerOptions": { + "composite": true, + }, + "files": ["./index.ts"], + "references": [ + { "path": "../lib" }, + ], +} +//// [/projects/container/exec/index.ts] *new* +import { Foo } from "../lib"; +class A1 implements Foo { + func() {} +} +class B1 implements Foo { + func() {} +} +//// [/projects/container/exec/tsconfig.json] *new* +{ + "files": ["./index.ts"], + "references": [ + { "path": "../lib" }, + ], +} +//// [/projects/container/lib/bar.ts] *new* +import {Foo} from './index' +class A implements Foo { + func() {} +} +class B implements Foo { + func() {} +} +//// [/projects/container/lib/index.ts] *new* +export interface Foo { + func(); +} +export const val = 42; +//// [/projects/container/lib/tsconfig.json] *new* +{ + "compilerOptions": { + "composite": true, + }, + references: [], + files: [ + "index.ts", + "bar.ts" + ], +} +//// [/projects/container/tsconfig.json] *new* +{ + "files": [], + "include": [], + "references": [ + { "path": "./exec" }, + { "path": "./compositeExec" }, + ], +} + +//// [/projects/temp/temp.ts] *new* +let x = 10 +//// [/projects/temp/tsconfig.json] *new* +{} + + +{ + "method": "textDocument/didOpen", + "params": { + "textDocument": { + "uri": "file:///projects/container/lib/index.ts", + "languageId": "typescript", + "version": 0, + "text": "export interface Foo {\n func();\n}\nexport const val = 42;" + } + } +} + +Projects:: + [/projects/container/lib/tsconfig.json] *new* + /projects/container/lib/index.ts + /projects/container/lib/bar.ts + [/projects/container/tsconfig.json] *new* +Open Files:: + [/projects/container/lib/index.ts] *new* + /projects/container/lib/tsconfig.json (default) +Config:: + [/projects/container/lib/tsconfig.json] *new* + RetainingProjects: + /projects/container/lib/tsconfig.json + RetainingOpenFiles: + /projects/container/lib/index.ts +Config File Names:: + [/projects/container/lib/index.ts] *new* + NearestConfigFileName: /projects/container/lib/tsconfig.json + Ancestors: + /projects/container/lib/tsconfig.json /projects/container/tsconfig.json + /projects/container/tsconfig.json + +{ + "method": "textDocument/didOpen", + "params": { + "textDocument": { + "uri": "file:///projects/temp/temp.ts", + "languageId": "typescript", + "version": 0, + "text": "let x = 10" + } + } +} + +Projects:: + [/projects/container/lib/tsconfig.json] + /projects/container/lib/index.ts + /projects/container/lib/bar.ts + [/projects/container/tsconfig.json] + [/projects/temp/tsconfig.json] *new* + /projects/temp/temp.ts +Open Files:: + [/projects/container/lib/index.ts] + /projects/container/lib/tsconfig.json (default) + [/projects/temp/temp.ts] *new* + /projects/temp/tsconfig.json (default) +Config:: + [/projects/container/lib/tsconfig.json] + RetainingProjects: + /projects/container/lib/tsconfig.json + RetainingOpenFiles: + /projects/container/lib/index.ts + [/projects/temp/tsconfig.json] *new* + RetainingProjects: + /projects/temp/tsconfig.json + RetainingOpenFiles: + /projects/temp/temp.ts +Config File Names:: + [/projects/container/lib/index.ts] + NearestConfigFileName: /projects/container/lib/tsconfig.json + Ancestors: + /projects/container/lib/tsconfig.json /projects/container/tsconfig.json + /projects/container/tsconfig.json + [/projects/temp/temp.ts] *new* + NearestConfigFileName: /projects/temp/tsconfig.json + +{ + "method": "textDocument/implementation", + "params": { + "textDocument": { + "uri": "file:///projects/container/lib/index.ts" + }, + "position": { + "line": 0, + "character": 17 + } + } +} + + + + +// === goToImplementation === +// === /projects/container/lib/bar.ts === +// import {Foo} from './index' +// <|class [|A|] implements Foo { +// func() {} +// }|> +// <|class [|B|] implements Foo { +// func() {} +// }|> + +// === /projects/container/lib/index.ts === +// export interface /*GOTO IMPL*/Foo { +// func(); +// } +// export const val = 42; +{ + "method": "textDocument/didClose", + "params": { + "textDocument": { + "uri": "file:///projects/temp/temp.ts" + } + } +} + +Open Files:: + [/projects/container/lib/index.ts] + /projects/container/lib/tsconfig.json (default) + [/projects/temp/temp.ts] *closed* + +{ + "method": "textDocument/didOpen", + "params": { + "textDocument": { + "uri": "file:///projects/temp/temp.ts", + "languageId": "typescript", + "version": 0, + "text": "let x = 10" + } + } +} + +Open Files:: + [/projects/container/lib/index.ts] + /projects/container/lib/tsconfig.json (default) + [/projects/temp/temp.ts] *new* + /projects/temp/tsconfig.json (default) + +{ + "method": "textDocument/didClose", + "params": { + "textDocument": { + "uri": "file:///projects/container/lib/index.ts" + } + } +} + +Open Files:: + [/projects/container/lib/index.ts] *closed* + [/projects/temp/temp.ts] + /projects/temp/tsconfig.json (default) + +{ + "method": "textDocument/didClose", + "params": { + "textDocument": { + "uri": "file:///projects/temp/temp.ts" + } + } +} + +Open Files:: + [/projects/temp/temp.ts] *closed* + +{ + "method": "textDocument/didOpen", + "params": { + "textDocument": { + "uri": "file:///projects/temp/temp.ts", + "languageId": "typescript", + "version": 0, + "text": "let x = 10" + } + } +} + +Projects:: + [/projects/container/lib/tsconfig.json] *deleted* + /projects/container/lib/index.ts + /projects/container/lib/bar.ts + [/projects/container/tsconfig.json] *deleted* + [/projects/temp/tsconfig.json] + /projects/temp/temp.ts +Open Files:: + [/projects/temp/temp.ts] *new* + /projects/temp/tsconfig.json (default) +Config:: + [/projects/container/lib/tsconfig.json] *deleted* + [/projects/temp/tsconfig.json] + RetainingProjects: + /projects/temp/tsconfig.json + RetainingOpenFiles: + /projects/temp/temp.ts +Config File Names:: + [/projects/container/lib/index.ts] *deleted* + [/projects/temp/temp.ts] + NearestConfigFileName: /projects/temp/tsconfig.json From c3b1ef0ecac660a92570b7356e44e95b383d1ff5 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Thu, 4 Dec 2025 14:42:09 -0800 Subject: [PATCH 02/16] Implementations across projects --- internal/ls/codelens.go | 2 +- internal/ls/findallreferences.go | 67 +++++++++++++-- internal/lsp/lsproto/_generate/generate.mts | 44 ++++++++++ internal/lsp/lsproto/lsp.go | 8 ++ internal/lsp/lsproto/lsp_generated.go | 40 +++++++++ internal/lsp/server.go | 70 +++++++++++----- internal/project/untitled_test.go | 4 +- ...ationsAncestorProjectRefMangement.baseline | 81 ++++++++++++++++++- 8 files changed, 287 insertions(+), 29 deletions(-) diff --git a/internal/ls/codelens.go b/internal/ls/codelens.go index 0e93e41cb8..c125bb46f1 100644 --- a/internal/ls/codelens.go +++ b/internal/ls/codelens.go @@ -77,7 +77,7 @@ func (l *LanguageService) ResolveCodeLens(ctx context.Context, codeLens *lsproto var lensTitle string switch codeLens.Data.Kind { case lsproto.CodeLensKindReferences: - origNode, symbolsAndEntries, ok := l.ProvideSymbolsAndEntries(ctx, uri, codeLens.Range.Start, false /*isRename*/) + origNode, symbolsAndEntries, ok := l.ProvideSymbolsAndEntries(ctx, uri, codeLens.Range.Start, false /*isRename*/, false /*implementations*/) if ok { references, err := l.ProvideReferencesFromSymbolAndEntries( ctx, diff --git a/internal/ls/findallreferences.go b/internal/ls/findallreferences.go index 2e0c8fa9e1..75155ceb98 100644 --- a/internal/ls/findallreferences.go +++ b/internal/ls/findallreferences.go @@ -576,7 +576,7 @@ func (l *LanguageService) ForEachOriginalDefinitionLocation( } } -func (l *LanguageService) ProvideSymbolsAndEntries(ctx context.Context, uri lsproto.DocumentUri, documentPosition lsproto.Position, isRename bool) (*ast.Node, []*SymbolAndEntries, bool) { +func (l *LanguageService) ProvideSymbolsAndEntries(ctx context.Context, uri lsproto.DocumentUri, documentPosition lsproto.Position, isRename bool, implementations bool) (*ast.Node, []*SymbolAndEntries, bool) { // `findReferencedSymbols` except only computes the information needed to return reference locations program, sourceFile := l.getProgramAndFile(uri) position := int(l.converters.LineAndCharacterToPosition(sourceFile, documentPosition)) @@ -586,15 +586,57 @@ func (l *LanguageService) ProvideSymbolsAndEntries(ctx context.Context, uri lspr return node, nil, false } + entries := l.getSymbolAndEntries(ctx, position, node, program, isRename, implementations) + if !implementations { + return node, entries, true + } + + var implementationEntries []*SymbolAndEntries + var queue []*ReferenceEntry + var seenNodes collections.Set[*ast.Node] + addToQueue := func(symbolAndEntries []*SymbolAndEntries) { + implementationEntries = core.Concatenate(implementationEntries, symbolAndEntries) + for _, s := range symbolAndEntries { + queue = append(queue, s.references...) + } + } + + addToQueue(entries) + for len(queue) != 0 { + if ctx.Err() != nil { + return nil, nil, false + } + + entry := queue[0] + queue = queue[1:] + if !seenNodes.Has(entry.node) { + seenNodes.Add(entry.node) + addToQueue(l.getSymbolAndEntries(ctx, entry.node.Pos(), entry.node, program, isRename, implementations)) + } + } + + return node, implementationEntries, true +} + +func (l *LanguageService) getSymbolAndEntries( + ctx context.Context, + position int, + node *ast.Node, + program *compiler.Program, + isRename bool, + implementations bool, +) []*SymbolAndEntries { var options refOptions if !isRename { options.use = referenceUseReferences + if implementations { + options.implementations = true + } } else { options.use = referenceUseRename options.useAliasesForRename = true } - - return node, l.getReferencedSymbolsForNode(ctx, position, node, program, program.GetSourceFiles(), options, nil), true + return l.getReferencedSymbolsForNode(ctx, position, node, program, program.GetSourceFiles(), options, nil) } func (l *LanguageService) ProvideReferencesFromSymbolAndEntries(ctx context.Context, params *lsproto.ReferenceParams, originalNode *ast.Node, symbolsAndEntries []*SymbolAndEntries) (lsproto.ReferencesResponse, error) { @@ -605,8 +647,23 @@ func (l *LanguageService) ProvideReferencesFromSymbolAndEntries(ctx context.Cont return lsproto.LocationsOrNull{Locations: &locations}, nil } -func (l *LanguageService) ProvideImplementations(ctx context.Context, params *lsproto.ImplementationParams) (lsproto.ImplementationResponse, error) { - return l.provideImplementationsEx(ctx, params, provideImplementationsOpts{}) +func (l *LanguageService) ProvideImplementationsFromSymbolAndEntries(ctx context.Context, params *lsproto.ImplementationParams, originalNode *ast.Node, symbolsAndEntries []*SymbolAndEntries) (lsproto.ImplementationResponse, error) { + var seenNodes collections.Set[*ast.Node] + var entries []*ReferenceEntry + for _, entry := range symbolsAndEntries { + for _, ref := range entry.references { + if seenNodes.AddIfAbsent(ref.node) { + entries = append(entries, ref) + } + } + } + + if lsproto.GetClientCapabilities(ctx).TextDocument.Implementation.LinkSupport { + links := l.convertEntriesToLocationLinks(entries) + return lsproto.LocationOrLocationsOrDefinitionLinksOrNull{DefinitionLinks: &links}, nil + } + locations := l.convertEntriesToLocations(entries) + return lsproto.LocationOrLocationsOrDefinitionLinksOrNull{Locations: &locations}, nil } type provideImplementationsOpts struct { diff --git a/internal/lsp/lsproto/_generate/generate.mts b/internal/lsp/lsproto/_generate/generate.mts index 546af57593..c1083847f8 100644 --- a/internal/lsp/lsproto/_generate/generate.mts +++ b/internal/lsp/lsproto/_generate/generate.mts @@ -1003,6 +1003,18 @@ function generateCode() { } } + const locationUriProperty = getLocationUriProperty(structure); + if (locationUriProperty) { + // Generate Location method + writeLine(`func (s *${structure.name}) GetLocation() Location {`); + writeLine(`\treturn Location{`); + writeLine(`\t\tUri: s.${locationUriProperty},`); + writeLine(`\t\tRange: s.${locationUriProperty.replace(/Uri$/, "Range")},`); + writeLine(`\t}`); + writeLine(`}`); + writeLine(""); + } + // Generate UnmarshalJSONFrom method for structure validation // Skip properties marked with omitzeroValue since they're optional by nature const requiredProps = structure.properties?.filter(p => { @@ -1474,6 +1486,7 @@ function generateCode() { writeLine(`type ${name} struct {`); const uniqueTypeFields = new Map(); // Maps type name -> field name + let hasLocations = false; for (const member of members) { const type = resolveType(member.type); const memberType = type.name; @@ -1483,6 +1496,9 @@ function generateCode() { const fieldName = titleCase(member.name); uniqueTypeFields.set(memberType, fieldName); writeLine(`\t${fieldName} *${memberType}`); + if (fieldName === "Locations" && memberType === "[]Location") { + hasLocations = true; + } } } @@ -1565,6 +1581,14 @@ function generateCode() { writeLine(`\treturn fmt.Errorf("invalid ${name}: %s", data)`); writeLine(`}`); writeLine(""); + + // Generate GetLocations method + if (hasLocations) { + writeLine(`func (o *${name}) GetLocations() *[]Location {`); + writeLine(`\treturn o.Locations`); + writeLine(`}`); + writeLine(""); + } } // Generate literal types @@ -1662,6 +1686,26 @@ function hasTextDocumentPosition(structure: Structure) { return hasSomeProp(structure, "position", "Position"); } +function getLocationUriProperty(structure: Structure) { + const prop = structure.properties?.find(p => + !p.optional && + titleCase(p.name).endsWith("Uri") && + p.type.kind === "base" && + p.type.name === "DocumentUri" + ); + if ( + prop && + structure.properties.some(p => + !p.optional && + titleCase(p.name) === titleCase(prop.name).replace(/Uri$/, "Range") && + p.type.kind === "reference" && + p.type.name === "Range" + ) + ) { + return titleCase(prop.name); + } +} + /** * Main function */ diff --git a/internal/lsp/lsproto/lsp.go b/internal/lsp/lsproto/lsp.go index 0484e88e97..f8d85867e2 100644 --- a/internal/lsp/lsproto/lsp.go +++ b/internal/lsp/lsproto/lsp.go @@ -66,6 +66,14 @@ type HasTextDocumentPosition interface { TextDocumentPosition() Position } +type HasLocations interface { + GetLocations() *[]Location +} + +type HasLocation interface { + GetLocation() Location +} + type URI string // !!! type Method string diff --git a/internal/lsp/lsproto/lsp_generated.go b/internal/lsp/lsproto/lsp_generated.go index 5845274323..54c7a31f7f 100644 --- a/internal/lsp/lsproto/lsp_generated.go +++ b/internal/lsp/lsproto/lsp_generated.go @@ -109,6 +109,13 @@ type Location struct { Range Range `json:"range"` } +func (s *Location) GetLocation() Location { + return Location{ + Uri: s.Uri, + Range: s.Range, + } +} + var _ json.UnmarshalerFrom = (*Location)(nil) func (s *Location) UnmarshalJSONFrom(dec *jsontext.Decoder) error { @@ -1811,6 +1818,13 @@ type CallHierarchyItem struct { Data *CallHierarchyItemData `json:"data,omitzero"` } +func (s *CallHierarchyItem) GetLocation() Location { + return Location{ + Uri: s.Uri, + Range: s.Range, + } +} + var _ json.UnmarshalerFrom = (*CallHierarchyItem)(nil) func (s *CallHierarchyItem) UnmarshalJSONFrom(dec *jsontext.Decoder) error { @@ -3772,6 +3786,13 @@ type TypeHierarchyItem struct { Data *TypeHierarchyItemData `json:"data,omitzero"` } +func (s *TypeHierarchyItem) GetLocation() Location { + return Location{ + Uri: s.Uri, + Range: s.Range, + } +} + var _ json.UnmarshalerFrom = (*TypeHierarchyItem)(nil) func (s *TypeHierarchyItem) UnmarshalJSONFrom(dec *jsontext.Decoder) error { @@ -11916,6 +11937,13 @@ type LocationLink struct { TargetSelectionRange Range `json:"targetSelectionRange"` } +func (s *LocationLink) GetLocation() Location { + return Location{ + Uri: s.TargetUri, + Range: s.TargetRange, + } +} + var _ json.UnmarshalerFrom = (*LocationLink)(nil) func (s *LocationLink) UnmarshalJSONFrom(dec *jsontext.Decoder) error { @@ -27073,6 +27101,10 @@ func (o *LocationOrLocationsOrDefinitionLinksOrNull) UnmarshalJSONFrom(dec *json return fmt.Errorf("invalid LocationOrLocationsOrDefinitionLinksOrNull: %s", data) } +func (o *LocationOrLocationsOrDefinitionLinksOrNull) GetLocations() *[]Location { + return o.Locations +} + type FoldingRangesOrNull struct { FoldingRanges *[]*FoldingRange } @@ -27163,6 +27195,10 @@ func (o *LocationOrLocationsOrDeclarationLinksOrNull) UnmarshalJSONFrom(dec *jso return fmt.Errorf("invalid LocationOrLocationsOrDeclarationLinksOrNull: %s", data) } +func (o *LocationOrLocationsOrDeclarationLinksOrNull) GetLocations() *[]Location { + return o.Locations +} + type SelectionRangesOrNull struct { SelectionRanges *[]*SelectionRange } @@ -27915,6 +27951,10 @@ func (o *LocationsOrNull) UnmarshalJSONFrom(dec *jsontext.Decoder) error { return fmt.Errorf("invalid LocationsOrNull: %s", data) } +func (o *LocationsOrNull) GetLocations() *[]Location { + return o.Locations +} + type DocumentHighlightsOrNull struct { DocumentHighlights *[]*DocumentHighlight } diff --git a/internal/lsp/server.go b/internal/lsp/server.go index 87d031fb11..2ded4c0c88 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -539,7 +539,6 @@ var handlers = sync.OnceValue(func() handlerMap { registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentDefinitionInfo, (*Server).handleDefinition) registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentTypeDefinitionInfo, (*Server).handleTypeDefinition) registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentCompletionInfo, (*Server).handleCompletion) - registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentImplementationInfo, (*Server).handleImplementations) registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentSignatureHelpInfo, (*Server).handleSignatureHelp) registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentFormattingInfo, (*Server).handleDocumentFormat) registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentRangeFormattingInfo, (*Server).handleDocumentRangeFormat) @@ -555,6 +554,7 @@ var handlers = sync.OnceValue(func() handlerMap { registerMultiProjectReferenceRequestHandler(handlers, lsproto.TextDocumentReferencesInfo, (*Server).handleReferences, combineReferences) registerMultiProjectReferenceRequestHandler(handlers, lsproto.TextDocumentRenameInfo, (*Server).handleRename, combineRenameResponse) + registerMultiProjectReferenceRequestHandler(handlers, lsproto.TextDocumentImplementationInfo, (*Server).handleImplementations, combineImplementations) registerRequestHandler(handlers, lsproto.CallHierarchyIncomingCallsInfo, (*Server).handleCallHierarchyIncomingCalls) registerRequestHandler(handlers, lsproto.CallHierarchyOutgoingCallsInfo, (*Server).handleCallHierarchyOutgoingCalls) @@ -656,7 +656,7 @@ func registerMultiProjectReferenceRequestHandler[Req lsproto.HasTextDocumentPosi handlers handlerMap, info lsproto.RequestInfo[Req, Resp], fn func(*Server, context.Context, *ls.LanguageService, Req, *ast.Node, []*ls.SymbolAndEntries) (Resp, error), - combineResults func(iter.Seq[Resp]) Resp, + combineResults func(iter.Seq[*Resp]) Resp, ) { handlers[info.Method] = func(s *Server, ctx context.Context, req *lsproto.RequestMessage) error { var params Req @@ -699,7 +699,10 @@ func registerMultiProjectReferenceRequestHandler[Req lsproto.HasTextDocumentPosi return } } - originalNode, symbolsAndEntries, ok := ls.ProvideSymbolsAndEntries(ctx, item.Uri, item.Position, info.Method == lsproto.MethodTextDocumentRename) + originalNode, symbolsAndEntries, ok := ls.ProvideSymbolsAndEntries(ctx, item.Uri, item.Position, info.Method == lsproto.MethodTextDocumentRename, info.Method == lsproto.MethodTextDocumentImplementation) + if ctx.Err() != nil { + return + } if ok { for _, entry := range symbolsAndEntries { // Find the default definition that can be in another project @@ -760,11 +763,11 @@ func registerMultiProjectReferenceRequestHandler[Req lsproto.HasTextDocumentPosi } } - getResultsIterator := func() iter.Seq[Resp] { - return func(yield func(Resp) bool) { + getResultsIterator := func() iter.Seq[*Resp] { + return func(yield func(*Resp) bool) { var seenProjects collections.SyncSet[tspath.Path] if response, loaded := results.Load(defaultProject.Id()); loaded && response.complete { - if !yield(response.result) { + if !yield(&response.result) { return } } @@ -772,7 +775,7 @@ func registerMultiProjectReferenceRequestHandler[Req lsproto.HasTextDocumentPosi for _, project := range allProjects { if seenProjects.AddIfAbsent(project.Id()) { if response, loaded := results.Load(project.Id()); loaded && response.complete { - if !yield(response.result) { + if !yield(&response.result) { return } } @@ -781,14 +784,14 @@ func registerMultiProjectReferenceRequestHandler[Req lsproto.HasTextDocumentPosi // Prefer the searches from locations for default definition results.Range(func(key tspath.Path, response *response[Resp]) bool { if !response.forOriginalLocation && seenProjects.AddIfAbsent(key) && response.complete { - return yield(response.result) + return yield(&response.result) } return true }) // Then the searches from original locations results.Range(func(key tspath.Path, response *response[Resp]) bool { if response.forOriginalLocation && seenProjects.AddIfAbsent(key) && response.complete { - return yield(response.result) + return yield(&response.result) } return true }) @@ -865,7 +868,7 @@ func registerMultiProjectReferenceRequestHandler[Req lsproto.HasTextDocumentPosi } else { // Single result, return that directly for value := range getResultsIterator() { - resp = value + resp = *value break } } @@ -1190,25 +1193,54 @@ func (s *Server) handleReferences(ctx context.Context, ls *ls.LanguageService, p return ls.ProvideReferencesFromSymbolAndEntries(ctx, params, originalNode, symbolAndEntries) } -func combineReferences(results iter.Seq[lsproto.ReferencesResponse]) lsproto.ReferencesResponse { +func combineLocationArray[T lsproto.HasLocation]( + combined []T, + locations *[]T, + seen *collections.Set[lsproto.Location], +) []T { + for _, loc := range *locations { + if seen.AddIfAbsent(loc.GetLocation()) { + combined = append(combined, loc) + } + } + return combined +} + +func combineResponseLocations[T lsproto.HasLocations](results iter.Seq[T]) *[]lsproto.Location { var combined []lsproto.Location var seenLocations collections.Set[lsproto.Location] for resp := range results { - if resp.Locations != nil { - for _, loc := range *resp.Locations { - if !seenLocations.Has(loc) { - seenLocations.Add(loc) + if locations := resp.GetLocations(); locations != nil { + for _, loc := range *locations { + if seenLocations.AddIfAbsent(loc) { combined = append(combined, loc) } } } } - return lsproto.LocationsOrNull{Locations: &combined} + return &combined } -func (s *Server) handleImplementations(ctx context.Context, ls *ls.LanguageService, params *lsproto.ImplementationParams) (lsproto.ImplementationResponse, error) { +func combineReferences(results iter.Seq[*lsproto.ReferencesResponse]) lsproto.ReferencesResponse { + return lsproto.LocationsOrNull{Locations: combineResponseLocations(results)} +} + +func (s *Server) handleImplementations(ctx context.Context, ls *ls.LanguageService, params *lsproto.ImplementationParams, originalNode *ast.Node, symbolAndEntries []*ls.SymbolAndEntries) (lsproto.ImplementationResponse, error) { // goToImplementation - return ls.ProvideImplementations(ctx, params) + return ls.ProvideImplementationsFromSymbolAndEntries(ctx, params, originalNode, symbolAndEntries) +} + +func combineImplementations(results iter.Seq[*lsproto.ImplementationResponse]) lsproto.ImplementationResponse { + var combined []*lsproto.LocationLink + var seenLocations collections.Set[lsproto.Location] + for resp := range results { + if definitionLinks := resp.DefinitionLinks; definitionLinks != nil { + combined = combineLocationArray(combined, definitionLinks, &seenLocations) + } else if locations := resp.Locations; locations != nil { + return lsproto.LocationOrLocationsOrDefinitionLinksOrNull{Locations: combineResponseLocations(results)} + } + } + return lsproto.LocationOrLocationsOrDefinitionLinksOrNull{DefinitionLinks: &combined} } func (s *Server) handleCompletion(ctx context.Context, languageService *ls.LanguageService, params *lsproto.CompletionParams) (lsproto.CompletionResponse, error) { @@ -1282,7 +1314,7 @@ func (s *Server) handleRename(ctx context.Context, ls *ls.LanguageService, param return ls.ProvideRenameFromSymbolAndEntries(ctx, params, originalNode, symbolAndEntries) } -func combineRenameResponse(results iter.Seq[lsproto.RenameResponse]) lsproto.RenameResponse { +func combineRenameResponse(results iter.Seq[*lsproto.RenameResponse]) lsproto.RenameResponse { combined := make(map[lsproto.DocumentUri][]*lsproto.TextEdit) seenChanges := make(map[lsproto.DocumentUri]*collections.Set[lsproto.Range]) // !!! this is not used any more so we will skip this part of deduplication and combining diff --git a/internal/project/untitled_test.go b/internal/project/untitled_test.go index 3910ef0099..501b913add 100644 --- a/internal/project/untitled_test.go +++ b/internal/project/untitled_test.go @@ -71,7 +71,7 @@ x++;` Context: &lsproto.ReferenceContext{IncludeDeclaration: true}, } - originalNode, symbolAndEntries, ok := languageService.ProvideSymbolsAndEntries(ctx, refParams.TextDocumentURI(), refParams.Position, false) + originalNode, symbolAndEntries, ok := languageService.ProvideSymbolsAndEntries(ctx, refParams.TextDocumentURI(), refParams.Position, false, false) assert.Assert(t, ok) resp, err := languageService.ProvideReferencesFromSymbolAndEntries(ctx, refParams, originalNode, symbolAndEntries) assert.NilError(t, err) @@ -146,7 +146,7 @@ x++;` Context: &lsproto.ReferenceContext{IncludeDeclaration: true}, } - originalNode, symbolAndEntries, ok := languageService.ProvideSymbolsAndEntries(ctx, refParams.TextDocumentURI(), refParams.Position, false) + originalNode, symbolAndEntries, ok := languageService.ProvideSymbolsAndEntries(ctx, refParams.TextDocumentURI(), refParams.Position, false, false) assert.Assert(t, ok) resp, err := languageService.ProvideReferencesFromSymbolAndEntries(ctx, refParams, originalNode, symbolAndEntries) assert.NilError(t, err) diff --git a/testdata/baselines/reference/fourslash/state/implementationsAncestorProjectRefMangement.baseline b/testdata/baselines/reference/fourslash/state/implementationsAncestorProjectRefMangement.baseline index 03da210a2e..b073b575f1 100644 --- a/testdata/baselines/reference/fourslash/state/implementationsAncestorProjectRefMangement.baseline +++ b/testdata/baselines/reference/fourslash/state/implementationsAncestorProjectRefMangement.baseline @@ -162,10 +162,74 @@ Config File Names:: } } +Projects:: + [/projects/container/compositeExec/tsconfig.json] *new* + /projects/container/lib/index.ts + /projects/container/compositeExec/index.ts + [/projects/container/exec/tsconfig.json] *new* + /projects/container/lib/index.ts + /projects/container/exec/index.ts + [/projects/container/lib/tsconfig.json] + /projects/container/lib/index.ts + /projects/container/lib/bar.ts + [/projects/container/tsconfig.json] *modified* + [/projects/temp/tsconfig.json] + /projects/temp/temp.ts +Open Files:: + [/projects/container/lib/index.ts] *modified* + /projects/container/compositeExec/tsconfig.json *new* + /projects/container/exec/tsconfig.json *new* + /projects/container/lib/tsconfig.json (default) + [/projects/temp/temp.ts] + /projects/temp/tsconfig.json (default) +Config:: + [/projects/container/compositeExec/tsconfig.json] *new* + RetainingProjects: + /projects/container/compositeExec/tsconfig.json + /projects/container/tsconfig.json + [/projects/container/exec/tsconfig.json] *new* + RetainingProjects: + /projects/container/exec/tsconfig.json + /projects/container/tsconfig.json + [/projects/container/lib/tsconfig.json] *modified* + RetainingProjects: *modified* + /projects/container/compositeExec/tsconfig.json *new* + /projects/container/exec/tsconfig.json *new* + /projects/container/lib/tsconfig.json + /projects/container/tsconfig.json *new* + RetainingOpenFiles: + /projects/container/lib/index.ts + [/projects/container/tsconfig.json] *new* + RetainingProjects: + /projects/container/tsconfig.json + [/projects/temp/tsconfig.json] + RetainingProjects: + /projects/temp/tsconfig.json + RetainingOpenFiles: + /projects/temp/temp.ts + // === goToImplementation === +// === /projects/container/compositeExec/index.ts === +// import { Foo } from "../lib"; +// <|class [|A2|] implements Foo { +// func() {} +// }|> +// <|class [|B2|] implements Foo { +// func() {} +// }|> + +// === /projects/container/exec/index.ts === +// import { Foo } from "../lib"; +// <|class [|A1|] implements Foo { +// func() {} +// }|> +// <|class [|B1|] implements Foo { +// func() {} +// }|> + // === /projects/container/lib/bar.ts === // import {Foo} from './index' // <|class [|A|] implements Foo { @@ -191,7 +255,9 @@ Config File Names:: Open Files:: [/projects/container/lib/index.ts] - /projects/container/lib/tsconfig.json (default) + /projects/container/compositeExec/tsconfig.json + /projects/container/exec/tsconfig.json + /projects/container/lib/tsconfig.json (default) [/projects/temp/temp.ts] *closed* { @@ -208,7 +274,9 @@ Open Files:: Open Files:: [/projects/container/lib/index.ts] - /projects/container/lib/tsconfig.json (default) + /projects/container/compositeExec/tsconfig.json + /projects/container/exec/tsconfig.json + /projects/container/lib/tsconfig.json (default) [/projects/temp/temp.ts] *new* /projects/temp/tsconfig.json (default) @@ -251,6 +319,12 @@ Open Files:: } Projects:: + [/projects/container/compositeExec/tsconfig.json] *deleted* + /projects/container/lib/index.ts + /projects/container/compositeExec/index.ts + [/projects/container/exec/tsconfig.json] *deleted* + /projects/container/lib/index.ts + /projects/container/exec/index.ts [/projects/container/lib/tsconfig.json] *deleted* /projects/container/lib/index.ts /projects/container/lib/bar.ts @@ -261,7 +335,10 @@ Open Files:: [/projects/temp/temp.ts] *new* /projects/temp/tsconfig.json (default) Config:: + [/projects/container/compositeExec/tsconfig.json] *deleted* + [/projects/container/exec/tsconfig.json] *deleted* [/projects/container/lib/tsconfig.json] *deleted* + [/projects/container/tsconfig.json] *deleted* [/projects/temp/tsconfig.json] RetainingProjects: /projects/temp/tsconfig.json From 46cb8a92b82e53f92722a1c1f597a51e776eb2d5 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Thu, 4 Dec 2025 15:48:55 -0800 Subject: [PATCH 03/16] Test for codelens --- .../fourslash/tests/statecodelens_test.go | 186 +++++ ...nsOnFunctionAcrossProjects1.baseline.jsonc | 5 + .../state/codeLensAcrossProjects.baseline | 639 ++++++++++++++++++ 3 files changed, 830 insertions(+) create mode 100644 internal/fourslash/tests/statecodelens_test.go create mode 100644 testdata/baselines/reference/fourslash/codeLenses/codeLensOnFunctionAcrossProjects1.baseline.jsonc create mode 100644 testdata/baselines/reference/fourslash/state/codeLensAcrossProjects.baseline diff --git a/internal/fourslash/tests/statecodelens_test.go b/internal/fourslash/tests/statecodelens_test.go new file mode 100644 index 0000000000..e7b8850cf3 --- /dev/null +++ b/internal/fourslash/tests/statecodelens_test.go @@ -0,0 +1,186 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/fourslash" + "github.com/microsoft/typescript-go/internal/ls/lsutil" + "github.com/microsoft/typescript-go/internal/testutil" +) + +func TestCodeLensAcrossProjects(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + content := ` +// @stateBaseline: true +// @Filename: /projects/temp/temp.ts +/*temp*/let x = 10 +// @Filename: /projects/temp/tsconfig.json +{} +// @Filename: /projects/container/lib/tsconfig.json +{ + "compilerOptions": { + "composite": true, + }, + references: [], + files: [ + "index.ts", + "bar.ts" + ], +} +// @Filename: /projects/container/lib/index.ts +/*impl*/ +export interface Pointable { + getX(): number; + getY(): number; +} +export const val = 42; +// @Filename: /projects/container/lib/bar.ts +import { Pointable } from "./index"; +class Point implements Pointable { + getX(): number { + return 0; + } + getY(): number { + return 0; + } +} +// @Filename: /projects/container/exec/tsconfig.json +{ + "files": ["./index.ts"], + "references": [ + { "path": "../lib" }, + ], +} +// @Filename: /projects/container/exec/index.ts +import { Pointable } from "../lib"; +class Point1 implements Pointable { + getX(): number { + return 0; + } + getY(): number { + return 0; + } +} +// @Filename: /projects/container/compositeExec/tsconfig.json +{ + "compilerOptions": { + "composite": true, + }, + "files": ["./index.ts"], + "references": [ + { "path": "../lib" }, + ], +} +// @Filename: /projects/container/compositeExec/index.ts +import { Pointable } from "../lib"; +class Point2 implements Pointable { + getX(): number { + return 0; + } + getY(): number { + return 0; + } +} +// @Filename: /projects/container/tsconfig.json +{ + "files": [], + "include": [], + "references": [ + { "path": "./exec" }, + { "path": "./compositeExec" }, + ], +} +// @Filename: /projects/container/tsconfig.json +{ + "files": [], + "include": [], + "references": [ + { "path": "./exec" }, + { "path": "./compositeExec" }, + ], +} +// @Filename: /projects/container/tsconfig.json +{ + "files": [], + "include": [], + "references": [ + { "path": "./exec" }, + { "path": "./compositeExec" }, + ], +} +` + f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) + defer done() + f.GoToMarker(t, "impl") + // Open temp file and verify all projects alive + f.GoToMarker(t, "temp") + + // Ref projects are loaded after as part of this command + f.VerifyBaselineCodeLens(t, &lsutil.UserPreferences{ + CodeLens: lsutil.CodeLensUserPreferences{ + ReferencesCodeLensEnabled: true, + ReferencesCodeLensShowOnAllFunctions: true, + + ImplementationsCodeLensEnabled: true, + ImplementationsCodeLensShowOnInterfaceMethods: true, + ImplementationsCodeLensShowOnAllClassMethods: true, + }, + }) + + // Open temp file and verify all projects alive + f.CloseFileOfMarker(t, "temp") + f.GoToMarker(t, "temp") + + // Close all files and open temp file, only inferred project should be alive + f.CloseFileOfMarker(t, "impl") + f.CloseFileOfMarker(t, "temp") + f.GoToMarker(t, "temp") +} + +func TestCodeLensOnFunctionAcrossProjects1(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = ` +// @filename: ./a/tsconfig.json +{ + "compilerOptions": { + "composite": true, + "declaration": true, + "declarationMaps": true, + "outDir": "./dist", + "rootDir": "src" + }, + "include": ["./src"] +} + +// @filename: ./a/src/foo.ts +export function aaa() {} +aaa(); + +// @filename: ./b/tsconfig.json +{ + "compilerOptions": { + "composite": true, + "declaration": true, + "declarationMaps": true, + "outDir": "./dist", + "rootDir": "src" + }, + "references": [{ "path": "../a" }], + "include": ["./src"] +} + +// @filename: ./b/src/bar.ts +import * as foo from '../../a/dist/foo.js'; +foo.aaa(); +` + f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) + defer done() + + f.VerifyBaselineCodeLens(t, &lsutil.UserPreferences{ + CodeLens: lsutil.CodeLensUserPreferences{ + ReferencesCodeLensEnabled: true, + }, + }) +} diff --git a/testdata/baselines/reference/fourslash/codeLenses/codeLensOnFunctionAcrossProjects1.baseline.jsonc b/testdata/baselines/reference/fourslash/codeLenses/codeLensOnFunctionAcrossProjects1.baseline.jsonc new file mode 100644 index 0000000000..0cf41af97d --- /dev/null +++ b/testdata/baselines/reference/fourslash/codeLenses/codeLensOnFunctionAcrossProjects1.baseline.jsonc @@ -0,0 +1,5 @@ +// === Code Lenses === +// === /a/src/foo.ts === +// export function /*CODELENS: 1 reference*/aaa() {} +// [|aaa|](); +// \ No newline at end of file diff --git a/testdata/baselines/reference/fourslash/state/codeLensAcrossProjects.baseline b/testdata/baselines/reference/fourslash/state/codeLensAcrossProjects.baseline new file mode 100644 index 0000000000..439b3ac5e3 --- /dev/null +++ b/testdata/baselines/reference/fourslash/state/codeLensAcrossProjects.baseline @@ -0,0 +1,639 @@ +UseCaseSensitiveFileNames: true +//// [/projects/container/compositeExec/index.ts] *new* +import { Pointable } from "../lib"; +class Point2 implements Pointable { + getX(): number { + return 0; + } + getY(): number { + return 0; + } +} +//// [/projects/container/compositeExec/tsconfig.json] *new* +{ + "compilerOptions": { + "composite": true, + }, + "files": ["./index.ts"], + "references": [ + { "path": "../lib" }, + ], +} +//// [/projects/container/exec/index.ts] *new* +import { Pointable } from "../lib"; +class Point1 implements Pointable { + getX(): number { + return 0; + } + getY(): number { + return 0; + } +} +//// [/projects/container/exec/tsconfig.json] *new* +{ + "files": ["./index.ts"], + "references": [ + { "path": "../lib" }, + ], +} +//// [/projects/container/lib/bar.ts] *new* +import { Pointable } from "./index"; +class Point implements Pointable { + getX(): number { + return 0; + } + getY(): number { + return 0; + } +} +//// [/projects/container/lib/index.ts] *new* + +export interface Pointable { + getX(): number; + getY(): number; +} +export const val = 42; +//// [/projects/container/lib/tsconfig.json] *new* +{ + "compilerOptions": { + "composite": true, + }, + references: [], + files: [ + "index.ts", + "bar.ts" + ], +} +//// [/projects/container/tsconfig.json] *new* +{ + "files": [], + "include": [], + "references": [ + { "path": "./exec" }, + { "path": "./compositeExec" }, + ], +} + +//// [/projects/temp/temp.ts] *new* +let x = 10 +//// [/projects/temp/tsconfig.json] *new* +{} + + +{ + "method": "textDocument/didOpen", + "params": { + "textDocument": { + "uri": "file:///projects/container/lib/index.ts", + "languageId": "typescript", + "version": 0, + "text": "\nexport interface Pointable {\n getX(): number;\n getY(): number;\n}\nexport const val = 42;" + } + } +} + +Projects:: + [/projects/container/lib/tsconfig.json] *new* + /projects/container/lib/index.ts + /projects/container/lib/bar.ts + [/projects/container/tsconfig.json] *new* +Open Files:: + [/projects/container/lib/index.ts] *new* + /projects/container/lib/tsconfig.json (default) +Config:: + [/projects/container/lib/tsconfig.json] *new* + RetainingProjects: + /projects/container/lib/tsconfig.json + RetainingOpenFiles: + /projects/container/lib/index.ts +Config File Names:: + [/projects/container/lib/index.ts] *new* + NearestConfigFileName: /projects/container/lib/tsconfig.json + Ancestors: + /projects/container/lib/tsconfig.json /projects/container/tsconfig.json + /projects/container/tsconfig.json + +{ + "method": "textDocument/didOpen", + "params": { + "textDocument": { + "uri": "file:///projects/temp/temp.ts", + "languageId": "typescript", + "version": 0, + "text": "let x = 10" + } + } +} + +Projects:: + [/projects/container/lib/tsconfig.json] + /projects/container/lib/index.ts + /projects/container/lib/bar.ts + [/projects/container/tsconfig.json] + [/projects/temp/tsconfig.json] *new* + /projects/temp/temp.ts +Open Files:: + [/projects/container/lib/index.ts] + /projects/container/lib/tsconfig.json (default) + [/projects/temp/temp.ts] *new* + /projects/temp/tsconfig.json (default) +Config:: + [/projects/container/lib/tsconfig.json] + RetainingProjects: + /projects/container/lib/tsconfig.json + RetainingOpenFiles: + /projects/container/lib/index.ts + [/projects/temp/tsconfig.json] *new* + RetainingProjects: + /projects/temp/tsconfig.json + RetainingOpenFiles: + /projects/temp/temp.ts +Config File Names:: + [/projects/container/lib/index.ts] + NearestConfigFileName: /projects/container/lib/tsconfig.json + Ancestors: + /projects/container/lib/tsconfig.json /projects/container/tsconfig.json + /projects/container/tsconfig.json + [/projects/temp/temp.ts] *new* + NearestConfigFileName: /projects/temp/tsconfig.json + +{ + "method": "workspace/didChangeConfiguration", + "params": { + "settings": { + "typescript": { + "QuotePreference": "", + "LazyConfiguredProjectsFromExternalProject": false, + "MaximumHoverLength": 0, + "IncludeCompletionsForModuleExports": null, + "IncludeCompletionsForImportStatements": null, + "IncludeAutomaticOptionalChainCompletions": null, + "IncludeCompletionsWithSnippetText": null, + "IncludeCompletionsWithClassMemberSnippets": null, + "IncludeCompletionsWithObjectLiteralMethodSnippets": null, + "JsxAttributeCompletionStyle": "", + "ImportModuleSpecifierPreference": "", + "ImportModuleSpecifierEnding": "", + "IncludePackageJsonAutoImports": "", + "AutoImportSpecifierExcludeRegexes": [], + "AutoImportFileExcludePatterns": [], + "PreferTypeOnlyAutoImports": false, + "OrganizeImportsIgnoreCase": null, + "OrganizeImportsCollation": false, + "OrganizeImportsLocale": "", + "OrganizeImportsNumericCollation": false, + "OrganizeImportsAccentCollation": false, + "OrganizeImportsCaseFirst": 0, + "OrganizeImportsTypeOrder": 0, + "AllowTextChangesInNewFiles": false, + "UseAliasesForRename": null, + "AllowRenameOfImportPath": false, + "ProvideRefactorNotApplicableReason": false, + "InlayHints": { + "IncludeInlayParameterNameHints": "", + "IncludeInlayParameterNameHintsWhenArgumentMatchesName": false, + "IncludeInlayFunctionParameterTypeHints": false, + "IncludeInlayVariableTypeHints": false, + "IncludeInlayVariableTypeHintsWhenTypeMatchesName": false, + "IncludeInlayPropertyDeclarationTypeHints": false, + "IncludeInlayFunctionLikeReturnTypeHints": false, + "IncludeInlayEnumMemberValueHints": false + }, + "CodeLens": { + "ReferencesCodeLensEnabled": true, + "ImplementationsCodeLensEnabled": true, + "ReferencesCodeLensShowOnAllFunctions": true, + "ImplementationsCodeLensShowOnInterfaceMethods": true, + "ImplementationsCodeLensShowOnAllClassMethods": true + }, + "ExcludeLibrarySymbolsInNavTo": false, + "DisableSuggestions": false, + "DisableLineTextInReferences": false, + "DisplayPartsForJSDoc": false, + "ReportStyleChecksAsWarnings": false + } + } + } +} + +{ + "method": "textDocument/codeLens", + "params": { + "textDocument": { + "uri": "file:///projects/container/lib/index.ts" + } + } +} + +{ + "method": "codeLens/resolve", + "params": { + "range": { + "start": { + "line": 1, + "character": 17 + }, + "end": { + "line": 4, + "character": 1 + } + }, + "data": { + "kind": "references", + "uri": "file:///projects/container/lib/index.ts" + } + } +} + + + + +// === Code Lenses === +// === /projects/container/lib/bar.ts === +// import { Pointable } from "./index"; +// class Point implements [|Pointable|] { +// getX(): number { +// return 0; +// } +// --- (line: 6) skipped --- + +// === /projects/container/lib/index.ts === +// +// export interface /*CODELENS: 1 reference*/Pointable { +// getX(): number; +// getY(): number; +// } +// export const val = 42; +{ + "method": "codeLens/resolve", + "params": { + "range": { + "start": { + "line": 1, + "character": 17 + }, + "end": { + "line": 4, + "character": 1 + } + }, + "data": { + "kind": "implementations", + "uri": "file:///projects/container/lib/index.ts" + } + } +} + + + + +// === Code Lenses === +// === /projects/container/lib/bar.ts === +// import { Pointable } from "./index"; +// class [|Point|] implements Pointable { +// getX(): number { +// return 0; +// } +// --- (line: 6) skipped --- + +// === /projects/container/lib/index.ts === +// +// export interface /*CODELENS: 1 implementation*/Pointable { +// getX(): number; +// getY(): number; +// } +// export const val = 42; +{ + "method": "codeLens/resolve", + "params": { + "range": { + "start": { + "line": 2, + "character": 2 + }, + "end": { + "line": 2, + "character": 17 + } + }, + "data": { + "kind": "references", + "uri": "file:///projects/container/lib/index.ts" + } + } +} + + + + +// === Code Lenses === +// === /projects/container/lib/index.ts === +// +// export interface Pointable { +// /*CODELENS: 0 references*/getX(): number; +// getY(): number; +// } +// export const val = 42; +{ + "method": "codeLens/resolve", + "params": { + "range": { + "start": { + "line": 2, + "character": 2 + }, + "end": { + "line": 2, + "character": 17 + } + }, + "data": { + "kind": "implementations", + "uri": "file:///projects/container/lib/index.ts" + } + } +} + + + + +// === Code Lenses === +// === /projects/container/lib/bar.ts === +// import { Pointable } from "./index"; +// class Point implements Pointable { +// [|getX|](): number { +// return 0; +// } +// getY(): number { +// --- (line: 7) skipped --- + +// === /projects/container/lib/index.ts === +// +// export interface Pointable { +// /*CODELENS: 1 implementation*/getX(): number; +// getY(): number; +// } +// export const val = 42; +{ + "method": "codeLens/resolve", + "params": { + "range": { + "start": { + "line": 3, + "character": 2 + }, + "end": { + "line": 3, + "character": 17 + } + }, + "data": { + "kind": "references", + "uri": "file:///projects/container/lib/index.ts" + } + } +} + + + + +// === Code Lenses === +// === /projects/container/lib/index.ts === +// +// export interface Pointable { +// getX(): number; +// /*CODELENS: 0 references*/getY(): number; +// } +// export const val = 42; +{ + "method": "codeLens/resolve", + "params": { + "range": { + "start": { + "line": 3, + "character": 2 + }, + "end": { + "line": 3, + "character": 17 + } + }, + "data": { + "kind": "implementations", + "uri": "file:///projects/container/lib/index.ts" + } + } +} + + + + +// === Code Lenses === +// === /projects/container/lib/bar.ts === +// import { Pointable } from "./index"; +// class Point implements Pointable { +// getX(): number { +// return 0; +// } +// [|getY|](): number { +// return 0; +// } +// } + +// === /projects/container/lib/index.ts === +// +// export interface Pointable { +// getX(): number; +// /*CODELENS: 1 implementation*/getY(): number; +// } +// export const val = 42; +{ + "method": "codeLens/resolve", + "params": { + "range": { + "start": { + "line": 5, + "character": 13 + }, + "end": { + "line": 5, + "character": 21 + } + }, + "data": { + "kind": "references", + "uri": "file:///projects/container/lib/index.ts" + } + } +} + + + + +// === Code Lenses === +// === /projects/container/lib/index.ts === +// +// export interface Pointable { +// getX(): number; +// getY(): number; +// } +// export const /*CODELENS: 0 references*/val = 42; +{ + "method": "textDocument/codeLens", + "params": { + "textDocument": { + "uri": "file:///projects/temp/temp.ts" + } + } +} + +{ + "method": "workspace/didChangeConfiguration", + "params": { + "settings": { + "typescript": { + "QuotePreference": "", + "LazyConfiguredProjectsFromExternalProject": false, + "MaximumHoverLength": 0, + "IncludeCompletionsForModuleExports": true, + "IncludeCompletionsForImportStatements": true, + "IncludeAutomaticOptionalChainCompletions": null, + "IncludeCompletionsWithSnippetText": true, + "IncludeCompletionsWithClassMemberSnippets": null, + "IncludeCompletionsWithObjectLiteralMethodSnippets": null, + "JsxAttributeCompletionStyle": "", + "ImportModuleSpecifierPreference": "", + "ImportModuleSpecifierEnding": "", + "IncludePackageJsonAutoImports": "", + "AutoImportSpecifierExcludeRegexes": [], + "AutoImportFileExcludePatterns": [], + "PreferTypeOnlyAutoImports": false, + "OrganizeImportsIgnoreCase": null, + "OrganizeImportsCollation": false, + "OrganizeImportsLocale": "", + "OrganizeImportsNumericCollation": false, + "OrganizeImportsAccentCollation": false, + "OrganizeImportsCaseFirst": 0, + "OrganizeImportsTypeOrder": 0, + "AllowTextChangesInNewFiles": false, + "UseAliasesForRename": null, + "AllowRenameOfImportPath": true, + "ProvideRefactorNotApplicableReason": true, + "InlayHints": { + "IncludeInlayParameterNameHints": "", + "IncludeInlayParameterNameHintsWhenArgumentMatchesName": false, + "IncludeInlayFunctionParameterTypeHints": false, + "IncludeInlayVariableTypeHints": false, + "IncludeInlayVariableTypeHintsWhenTypeMatchesName": false, + "IncludeInlayPropertyDeclarationTypeHints": false, + "IncludeInlayFunctionLikeReturnTypeHints": false, + "IncludeInlayEnumMemberValueHints": false + }, + "CodeLens": { + "ReferencesCodeLensEnabled": false, + "ImplementationsCodeLensEnabled": false, + "ReferencesCodeLensShowOnAllFunctions": false, + "ImplementationsCodeLensShowOnInterfaceMethods": false, + "ImplementationsCodeLensShowOnAllClassMethods": false + }, + "ExcludeLibrarySymbolsInNavTo": true, + "DisableSuggestions": false, + "DisableLineTextInReferences": true, + "DisplayPartsForJSDoc": true, + "ReportStyleChecksAsWarnings": true + } + } + } +} + +{ + "method": "textDocument/didClose", + "params": { + "textDocument": { + "uri": "file:///projects/temp/temp.ts" + } + } +} + +Open Files:: + [/projects/container/lib/index.ts] + /projects/container/lib/tsconfig.json (default) + [/projects/temp/temp.ts] *closed* + +{ + "method": "textDocument/didOpen", + "params": { + "textDocument": { + "uri": "file:///projects/temp/temp.ts", + "languageId": "typescript", + "version": 0, + "text": "let x = 10" + } + } +} + +Open Files:: + [/projects/container/lib/index.ts] + /projects/container/lib/tsconfig.json (default) + [/projects/temp/temp.ts] *new* + /projects/temp/tsconfig.json (default) + +{ + "method": "textDocument/didClose", + "params": { + "textDocument": { + "uri": "file:///projects/container/lib/index.ts" + } + } +} + +Open Files:: + [/projects/container/lib/index.ts] *closed* + [/projects/temp/temp.ts] + /projects/temp/tsconfig.json (default) + +{ + "method": "textDocument/didClose", + "params": { + "textDocument": { + "uri": "file:///projects/temp/temp.ts" + } + } +} + +Open Files:: + [/projects/temp/temp.ts] *closed* + +{ + "method": "textDocument/didOpen", + "params": { + "textDocument": { + "uri": "file:///projects/temp/temp.ts", + "languageId": "typescript", + "version": 0, + "text": "let x = 10" + } + } +} + +Projects:: + [/projects/container/lib/tsconfig.json] *deleted* + /projects/container/lib/index.ts + /projects/container/lib/bar.ts + [/projects/container/tsconfig.json] *deleted* + [/projects/temp/tsconfig.json] + /projects/temp/temp.ts +Open Files:: + [/projects/temp/temp.ts] *new* + /projects/temp/tsconfig.json (default) +Config:: + [/projects/container/lib/tsconfig.json] *deleted* + [/projects/temp/tsconfig.json] + RetainingProjects: + /projects/temp/tsconfig.json + RetainingOpenFiles: + /projects/temp/temp.ts +Config File Names:: + [/projects/container/lib/index.ts] *deleted* + [/projects/temp/temp.ts] + NearestConfigFileName: /projects/temp/tsconfig.json From d2979e071cc6aff5600cdee8e7d583e975d069fe Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Thu, 4 Dec 2025 16:43:55 -0800 Subject: [PATCH 04/16] Refactor --- internal/lsp/server.go | 378 +++++++++++++++++++++-------------------- 1 file changed, 196 insertions(+), 182 deletions(-) diff --git a/internal/lsp/server.go b/internal/lsp/server.go index 2ded4c0c88..337ddde8c0 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -659,223 +659,237 @@ func registerMultiProjectReferenceRequestHandler[Req lsproto.HasTextDocumentPosi combineResults func(iter.Seq[*Resp]) Resp, ) { handlers[info.Method] = func(s *Server, ctx context.Context, req *lsproto.RequestMessage) error { - var params Req - // Ignore empty params. - if req.Params != nil { - params = req.Params.(Req) - } - // !!! sheetal: multiple projects that contain the file through symlinks - defaultProject, defaultLs, allProjects, err := s.session.GetLanguageServiceAndProjectsForFile(ctx, params.TextDocumentURI()) + resp, err := multiProjectRequestHandling(s, ctx, info, fn, combineResults, req) if err != nil { return err } - defer s.recover(req) + s.sendResult(req.ID, resp) + return nil + } +} + +func multiProjectRequestHandling[Req lsproto.HasTextDocumentPosition, Resp any]( + s *Server, + ctx context.Context, + info lsproto.RequestInfo[Req, Resp], + fn func(*Server, context.Context, *ls.LanguageService, Req, *ast.Node, []*ls.SymbolAndEntries) (Resp, error), + combineResults func(iter.Seq[*Resp]) Resp, + req *lsproto.RequestMessage, +) (Resp, error) { + var resp Resp + var params Req + // Ignore empty params. + if req.Params != nil { + params = req.Params.(Req) + } + // !!! sheetal: multiple projects that contain the file through symlinks + defaultProject, defaultLs, allProjects, err := s.session.GetLanguageServiceAndProjectsForFile(ctx, params.TextDocumentURI()) + if err != nil { + return resp, err + } + defer s.recover(req) - var results collections.SyncMap[tspath.Path, *response[Resp]] - var defaultDefinition *ls.NonLocalDefinition - canSearchProject := func(project *project.Project) bool { - _, searched := results.Load(project.Id()) - return !searched + var results collections.SyncMap[tspath.Path, *response[Resp]] + var defaultDefinition *ls.NonLocalDefinition + canSearchProject := func(project *project.Project) bool { + _, searched := results.Load(project.Id()) + return !searched + } + wg := core.NewWorkGroup(false) + var errMu sync.Mutex + var enqueueItem func(item projectAndTextDocumentPosition) + enqueueItem = func(item projectAndTextDocumentPosition) { + var response response[Resp] + if _, loaded := results.LoadOrStore(item.project.Id(), &response); loaded { + return } - wg := core.NewWorkGroup(false) - var errMu sync.Mutex - var enqueueItem func(item projectAndTextDocumentPosition) - enqueueItem = func(item projectAndTextDocumentPosition) { - var response response[Resp] - if _, loaded := results.LoadOrStore(item.project.Id(), &response); loaded { + wg.Queue(func() { + if ctx.Err() != nil { return } - wg.Queue(func() { - if ctx.Err() != nil { - return - } - defer s.recover(req) - // Process the item - ls := item.ls + defer s.recover(req) + // Process the item + ls := item.ls + if ls == nil { + // Get it now + ls = s.session.GetLanguageServiceForProjectWithFile(ctx, item.project, item.Uri) if ls == nil { - // Get it now - ls = s.session.GetLanguageServiceForProjectWithFile(ctx, item.project, item.Uri) - if ls == nil { - return - } - } - originalNode, symbolsAndEntries, ok := ls.ProvideSymbolsAndEntries(ctx, item.Uri, item.Position, info.Method == lsproto.MethodTextDocumentRename, info.Method == lsproto.MethodTextDocumentImplementation) - if ctx.Err() != nil { return } - if ok { - for _, entry := range symbolsAndEntries { - // Find the default definition that can be in another project - // Later we will use this load ancestor tree that references this location and expand search - if item.project == defaultProject && defaultDefinition == nil { - defaultDefinition = ls.GetNonLocalDefinition(ctx, entry) + } + originalNode, symbolsAndEntries, ok := ls.ProvideSymbolsAndEntries(ctx, item.Uri, item.Position, info.Method == lsproto.MethodTextDocumentRename, info.Method == lsproto.MethodTextDocumentImplementation) + if ctx.Err() != nil { + return + } + if ok { + for _, entry := range symbolsAndEntries { + // Find the default definition that can be in another project + // Later we will use this load ancestor tree that references this location and expand search + if item.project == defaultProject && defaultDefinition == nil { + defaultDefinition = ls.GetNonLocalDefinition(ctx, entry) + } + ls.ForEachOriginalDefinitionLocation(ctx, entry, func(uri lsproto.DocumentUri, position lsproto.Position) { + // Get default configured project for this file + defProjects, errProjects := s.session.GetProjectsForFile(ctx, uri) + if errProjects != nil { + return } - ls.ForEachOriginalDefinitionLocation(ctx, entry, func(uri lsproto.DocumentUri, position lsproto.Position) { - // Get default configured project for this file - defProjects, errProjects := s.session.GetProjectsForFile(ctx, uri) - if errProjects != nil { - return + for _, defProject := range defProjects { + // Optimization: don't enqueue if will be discarded + if canSearchProject(defProject) { + enqueueItem(projectAndTextDocumentPosition{ + project: defProject, + Uri: uri, + Position: position, + forOriginalLocation: true, + }) } - for _, defProject := range defProjects { - // Optimization: don't enqueue if will be discarded - if canSearchProject(defProject) { - enqueueItem(projectAndTextDocumentPosition{ - project: defProject, - Uri: uri, - Position: position, - forOriginalLocation: true, - }) - } - } - }) - } + } + }) } + } - if result, errSearch := fn(s, ctx, ls, params, originalNode, symbolsAndEntries); errSearch == nil { - response.complete = true - response.result = result - response.forOriginalLocation = item.forOriginalLocation - } else { - errMu.Lock() - defer errMu.Unlock() - if err != nil { - err = errSearch - } + if result, errSearch := fn(s, ctx, ls, params, originalNode, symbolsAndEntries); errSearch == nil { + response.complete = true + response.result = result + response.forOriginalLocation = item.forOriginalLocation + } else { + errMu.Lock() + defer errMu.Unlock() + if err != nil { + err = errSearch } - }) - } - - // Initial set of projects and locations in the queue, starting with default project - enqueueItem(projectAndTextDocumentPosition{ - project: defaultProject, - ls: defaultLs, - Uri: params.TextDocumentURI(), - Position: params.TextDocumentPosition(), - }) - for _, project := range allProjects { - if project != defaultProject { - enqueueItem(projectAndTextDocumentPosition{ - project: project, - // TODO!! symlinks need to change the URI - Uri: params.TextDocumentURI(), - Position: params.TextDocumentPosition(), - }) } + }) + } + + // Initial set of projects and locations in the queue, starting with default project + enqueueItem(projectAndTextDocumentPosition{ + project: defaultProject, + ls: defaultLs, + Uri: params.TextDocumentURI(), + Position: params.TextDocumentPosition(), + }) + for _, project := range allProjects { + if project != defaultProject { + enqueueItem(projectAndTextDocumentPosition{ + project: project, + // TODO!! symlinks need to change the URI + Uri: params.TextDocumentURI(), + Position: params.TextDocumentPosition(), + }) } + } - getResultsIterator := func() iter.Seq[*Resp] { - return func(yield func(*Resp) bool) { - var seenProjects collections.SyncSet[tspath.Path] - if response, loaded := results.Load(defaultProject.Id()); loaded && response.complete { - if !yield(&response.result) { - return - } + getResultsIterator := func() iter.Seq[*Resp] { + return func(yield func(*Resp) bool) { + var seenProjects collections.SyncSet[tspath.Path] + if response, loaded := results.Load(defaultProject.Id()); loaded && response.complete { + if !yield(&response.result) { + return } - seenProjects.Add(defaultProject.Id()) - for _, project := range allProjects { - if seenProjects.AddIfAbsent(project.Id()) { - if response, loaded := results.Load(project.Id()); loaded && response.complete { - if !yield(&response.result) { - return - } + } + seenProjects.Add(defaultProject.Id()) + for _, project := range allProjects { + if seenProjects.AddIfAbsent(project.Id()) { + if response, loaded := results.Load(project.Id()); loaded && response.complete { + if !yield(&response.result) { + return } } } - // Prefer the searches from locations for default definition - results.Range(func(key tspath.Path, response *response[Resp]) bool { - if !response.forOriginalLocation && seenProjects.AddIfAbsent(key) && response.complete { - return yield(&response.result) - } - return true - }) - // Then the searches from original locations - results.Range(func(key tspath.Path, response *response[Resp]) bool { - if response.forOriginalLocation && seenProjects.AddIfAbsent(key) && response.complete { - return yield(&response.result) - } - return true - }) } + // Prefer the searches from locations for default definition + results.Range(func(key tspath.Path, response *response[Resp]) bool { + if !response.forOriginalLocation && seenProjects.AddIfAbsent(key) && response.complete { + return yield(&response.result) + } + return true + }) + // Then the searches from original locations + results.Range(func(key tspath.Path, response *response[Resp]) bool { + if response.forOriginalLocation && seenProjects.AddIfAbsent(key) && response.complete { + return yield(&response.result) + } + return true + }) } + } - // Outer loop - to complete work if more is added after completing existing queue - for { - // Process existing known projects first - wg.RunAndWait() - if ctx.Err() != nil { - return ctx.Err() - } - // No need to use mu here since we are not in parallel at this point - if err != nil { - return err - } + // Outer loop - to complete work if more is added after completing existing queue + for { + // Process existing known projects first + wg.RunAndWait() + if ctx.Err() != nil { + return resp, ctx.Err() + } + // No need to use mu here since we are not in parallel at this point + if err != nil { + return resp, err + } - wg = core.NewWorkGroup(false) - hasMoreWork := false - if defaultDefinition != nil { - requestedProjectTrees := make(map[tspath.Path]struct{}) - results.Range(func(key tspath.Path, response *response[Resp]) bool { - if response.complete { - requestedProjectTrees[key] = struct{}{} - } - return true - }) + wg = core.NewWorkGroup(false) + hasMoreWork := false + if defaultDefinition != nil { + requestedProjectTrees := make(map[tspath.Path]struct{}) + results.Range(func(key tspath.Path, response *response[Resp]) bool { + if response.complete { + requestedProjectTrees[key] = struct{}{} + } + return true + }) - // Load more projects based on default definition found - for _, loadedProject := range s.session.GetSnapshotLoadingProjectTree(ctx, requestedProjectTrees).ProjectCollection.Projects() { - if ctx.Err() != nil { - return ctx.Err() - } + // Load more projects based on default definition found + for _, loadedProject := range s.session.GetSnapshotLoadingProjectTree(ctx, requestedProjectTrees).ProjectCollection.Projects() { + if ctx.Err() != nil { + return resp, ctx.Err() + } - // Can loop forever without this (enqueue here, dequeue above, repeat) - if !canSearchProject(loadedProject) || loadedProject.GetProgram() == nil { - continue - } + // Can loop forever without this (enqueue here, dequeue above, repeat) + if !canSearchProject(loadedProject) || loadedProject.GetProgram() == nil { + continue + } - // Enqueue the project and location for further processing - if loadedProject.HasFile(defaultDefinition.TextDocumentURI().FileName()) { - enqueueItem(projectAndTextDocumentPosition{ - project: loadedProject, - Uri: defaultDefinition.TextDocumentURI(), - Position: defaultDefinition.TextDocumentPosition(), - }) - hasMoreWork = true - } else if sourcePos := defaultDefinition.GetSourcePosition(); sourcePos != nil && loadedProject.HasFile(sourcePos.TextDocumentURI().FileName()) { - enqueueItem(projectAndTextDocumentPosition{ - project: loadedProject, - Uri: sourcePos.TextDocumentURI(), - Position: sourcePos.TextDocumentPosition(), - }) - hasMoreWork = true - } else if generatedPos := defaultDefinition.GetGeneratedPosition(); generatedPos != nil && loadedProject.HasFile(generatedPos.TextDocumentURI().FileName()) { - enqueueItem(projectAndTextDocumentPosition{ - project: loadedProject, - Uri: generatedPos.TextDocumentURI(), - Position: generatedPos.TextDocumentPosition(), - }) - hasMoreWork = true - } + // Enqueue the project and location for further processing + if loadedProject.HasFile(defaultDefinition.TextDocumentURI().FileName()) { + enqueueItem(projectAndTextDocumentPosition{ + project: loadedProject, + Uri: defaultDefinition.TextDocumentURI(), + Position: defaultDefinition.TextDocumentPosition(), + }) + hasMoreWork = true + } else if sourcePos := defaultDefinition.GetSourcePosition(); sourcePos != nil && loadedProject.HasFile(sourcePos.TextDocumentURI().FileName()) { + enqueueItem(projectAndTextDocumentPosition{ + project: loadedProject, + Uri: sourcePos.TextDocumentURI(), + Position: sourcePos.TextDocumentPosition(), + }) + hasMoreWork = true + } else if generatedPos := defaultDefinition.GetGeneratedPosition(); generatedPos != nil && loadedProject.HasFile(generatedPos.TextDocumentURI().FileName()) { + enqueueItem(projectAndTextDocumentPosition{ + project: loadedProject, + Uri: generatedPos.TextDocumentURI(), + Position: generatedPos.TextDocumentPosition(), + }) + hasMoreWork = true } } - if !hasMoreWork { - break - } } - - var resp Resp - if results.Size() > 1 { - resp = combineResults(getResultsIterator()) - } else { - // Single result, return that directly - for value := range getResultsIterator() { - resp = *value - break - } + if !hasMoreWork { + break } + } - s.sendResult(req.ID, resp) - return nil + if results.Size() > 1 { + resp = combineResults(getResultsIterator()) + } else { + // Single result, return that directly + for value := range getResultsIterator() { + resp = *value + break + } } + return resp, nil } func (s *Server) recover(req *lsproto.RequestMessage) { From 982d1224013cca821b1deaa4943bc75b4dcdff1b Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Thu, 4 Dec 2025 19:25:03 -0800 Subject: [PATCH 05/16] Code lens to use the commands for references and implementations --- internal/ls/codelens.go | 100 ------------ internal/ls/findallreferences.go | 89 ++++------- internal/lsp/server.go | 134 +++++++++++++--- internal/project/untitled_test.go | 9 +- ...nsOnFunctionAcrossProjects1.baseline.jsonc | 7 +- .../state/codeLensAcrossProjects.baseline | 143 +++++++++++++++++- 6 files changed, 292 insertions(+), 190 deletions(-) diff --git a/internal/ls/codelens.go b/internal/ls/codelens.go index c125bb46f1..edbca2926e 100644 --- a/internal/ls/codelens.go +++ b/internal/ls/codelens.go @@ -5,8 +5,6 @@ import ( "github.com/microsoft/typescript-go/internal/ast" "github.com/microsoft/typescript-go/internal/core" - "github.com/microsoft/typescript-go/internal/diagnostics" - "github.com/microsoft/typescript-go/internal/locale" "github.com/microsoft/typescript-go/internal/ls/lsutil" "github.com/microsoft/typescript-go/internal/lsp/lsproto" "github.com/microsoft/typescript-go/internal/scanner" @@ -54,104 +52,6 @@ func (l *LanguageService) ProvideCodeLenses(ctx context.Context, documentURI lsp }, nil } -func (l *LanguageService) ResolveCodeLens(ctx context.Context, codeLens *lsproto.CodeLens, showLocationsCommandName *string) (*lsproto.CodeLens, error) { - uri := codeLens.Data.Uri - _, sourceFile := l.tryGetProgramAndFile(uri.FileName()) - if sourceFile == nil || - l.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(sourceFile.End())).Line < codeLens.Range.Start.Line { - // This can happen if a codeLens/resolve request comes in after a program change. - // While it's true that handlers should latch onto a specific snapshot - // while processing requests, we just set `Data.Uri` based on - // some older snapshot's contents. The content could have been modified, - // or the file itself could have been removed from the session entirely. - // Note this won't bail out on every change, but will prevent crashing - // based on non-existent files and line maps from shortened files. - return codeLens, lsproto.ErrorCodeContentModified - } - - textDoc := lsproto.TextDocumentIdentifier{ - Uri: uri, - } - locale := locale.FromContext(ctx) - var locs []lsproto.Location - var lensTitle string - switch codeLens.Data.Kind { - case lsproto.CodeLensKindReferences: - origNode, symbolsAndEntries, ok := l.ProvideSymbolsAndEntries(ctx, uri, codeLens.Range.Start, false /*isRename*/, false /*implementations*/) - if ok { - references, err := l.ProvideReferencesFromSymbolAndEntries( - ctx, - &lsproto.ReferenceParams{ - TextDocument: textDoc, - Position: codeLens.Range.Start, - Context: &lsproto.ReferenceContext{ - // Don't include the declaration in the references count. - IncludeDeclaration: false, - }, - }, - origNode, - symbolsAndEntries, - ) - if err != nil { - return nil, err - } - - if references.Locations != nil { - locs = *references.Locations - } - } - - if len(locs) == 1 { - lensTitle = diagnostics.X_1_reference.Localize(locale) - } else { - lensTitle = diagnostics.X_0_references.Localize(locale, len(locs)) - } - case lsproto.CodeLensKindImplementations: - // "Force" link support to be false so that we only get `Locations` back, - // and don't include the "current" node in the results. - findImplsOptions := provideImplementationsOpts{ - requireLocationsResult: true, - dropOriginNodes: true, - } - implementations, err := l.provideImplementationsEx( - ctx, - &lsproto.ImplementationParams{ - TextDocument: textDoc, - Position: codeLens.Range.Start, - }, - findImplsOptions, - ) - if err != nil { - return nil, err - } - - if implementations.Locations != nil { - locs = *implementations.Locations - } - - if len(locs) == 1 { - lensTitle = diagnostics.X_1_implementation.Localize(locale) - } else { - lensTitle = diagnostics.X_0_implementations.Localize(locale, len(locs)) - } - } - - cmd := &lsproto.Command{ - Title: lensTitle, - } - if len(locs) > 0 && showLocationsCommandName != nil { - cmd.Command = *showLocationsCommandName - cmd.Arguments = &[]any{ - uri, - codeLens.Range.Start, - locs, - } - } - - codeLens.Command = cmd - return codeLens, nil -} - func (l *LanguageService) newCodeLensForNode(fileUri lsproto.DocumentUri, file *ast.SourceFile, node *ast.Node, kind lsproto.CodeLensKind) *lsproto.CodeLens { nodeForRange := node nodeName := node.Name() diff --git a/internal/ls/findallreferences.go b/internal/ls/findallreferences.go index 75155ceb98..95d47c1c49 100644 --- a/internal/ls/findallreferences.go +++ b/internal/ls/findallreferences.go @@ -576,19 +576,32 @@ func (l *LanguageService) ForEachOriginalDefinitionLocation( } } -func (l *LanguageService) ProvideSymbolsAndEntries(ctx context.Context, uri lsproto.DocumentUri, documentPosition lsproto.Position, isRename bool, implementations bool) (*ast.Node, []*SymbolAndEntries, bool) { +type SymbolEntryTransformOptions struct { + // Force the result to be Location objects. + RequireLocationsResult bool + // Omit node(s) containing the original position. + DropOriginNodes bool +} + +type SymbolAndEntriesData struct { + OriginalNode *ast.Node + SymbolsAndEntries []*SymbolAndEntries + Position int +} + +func (l *LanguageService) ProvideSymbolsAndEntries(ctx context.Context, uri lsproto.DocumentUri, documentPosition lsproto.Position, isRename bool, implementations bool) (SymbolAndEntriesData, bool) { // `findReferencedSymbols` except only computes the information needed to return reference locations program, sourceFile := l.getProgramAndFile(uri) position := int(l.converters.LineAndCharacterToPosition(sourceFile, documentPosition)) node := astnav.GetTouchingPropertyName(sourceFile, position) if isRename && node.Kind != ast.KindIdentifier { - return node, nil, false + return SymbolAndEntriesData{OriginalNode: node, Position: position}, false } entries := l.getSymbolAndEntries(ctx, position, node, program, isRename, implementations) if !implementations { - return node, entries, true + return SymbolAndEntriesData{OriginalNode: node, SymbolsAndEntries: entries, Position: position}, true } var implementationEntries []*SymbolAndEntries @@ -604,7 +617,7 @@ func (l *LanguageService) ProvideSymbolsAndEntries(ctx context.Context, uri lspr addToQueue(entries) for len(queue) != 0 { if ctx.Err() != nil { - return nil, nil, false + return SymbolAndEntriesData{}, false } entry := queue[0] @@ -614,8 +627,7 @@ func (l *LanguageService) ProvideSymbolsAndEntries(ctx context.Context, uri lspr addToQueue(l.getSymbolAndEntries(ctx, entry.node.Pos(), entry.node, program, isRename, implementations)) } } - - return node, implementationEntries, true + return SymbolAndEntriesData{OriginalNode: node, SymbolsAndEntries: implementationEntries, Position: position}, true } func (l *LanguageService) getSymbolAndEntries( @@ -639,65 +651,26 @@ func (l *LanguageService) getSymbolAndEntries( return l.getReferencedSymbolsForNode(ctx, position, node, program, program.GetSourceFiles(), options, nil) } -func (l *LanguageService) ProvideReferencesFromSymbolAndEntries(ctx context.Context, params *lsproto.ReferenceParams, originalNode *ast.Node, symbolsAndEntries []*SymbolAndEntries) (lsproto.ReferencesResponse, error) { +func (l *LanguageService) ProvideReferencesFromSymbolAndEntries(ctx context.Context, params *lsproto.ReferenceParams, data SymbolAndEntriesData, options SymbolEntryTransformOptions) (lsproto.ReferencesResponse, error) { // `findReferencedSymbols` except only computes the information needed to return reference locations - locations := core.FlatMap(symbolsAndEntries, func(s *SymbolAndEntries) []lsproto.Location { + locations := core.FlatMap(data.SymbolsAndEntries, func(s *SymbolAndEntries) []lsproto.Location { return l.convertSymbolAndEntriesToLocations(s, params.Context.IncludeDeclaration) }) return lsproto.LocationsOrNull{Locations: &locations}, nil } -func (l *LanguageService) ProvideImplementationsFromSymbolAndEntries(ctx context.Context, params *lsproto.ImplementationParams, originalNode *ast.Node, symbolsAndEntries []*SymbolAndEntries) (lsproto.ImplementationResponse, error) { +func (l *LanguageService) ProvideImplementationsFromSymbolAndEntries(ctx context.Context, params *lsproto.ImplementationParams, data SymbolAndEntriesData, options SymbolEntryTransformOptions) (lsproto.ImplementationResponse, error) { var seenNodes collections.Set[*ast.Node] var entries []*ReferenceEntry - for _, entry := range symbolsAndEntries { + for _, entry := range data.SymbolsAndEntries { for _, ref := range entry.references { - if seenNodes.AddIfAbsent(ref.node) { + if seenNodes.AddIfAbsent(ref.node) && (!options.DropOriginNodes || !ref.node.Loc.ContainsInclusive(data.Position)) { entries = append(entries, ref) } } } - if lsproto.GetClientCapabilities(ctx).TextDocument.Implementation.LinkSupport { - links := l.convertEntriesToLocationLinks(entries) - return lsproto.LocationOrLocationsOrDefinitionLinksOrNull{DefinitionLinks: &links}, nil - } - locations := l.convertEntriesToLocations(entries) - return lsproto.LocationOrLocationsOrDefinitionLinksOrNull{Locations: &locations}, nil -} - -type provideImplementationsOpts struct { - // Force the result to be Location objects. - requireLocationsResult bool - // Omit node(s) containing the original position. - dropOriginNodes bool -} - -func (l *LanguageService) provideImplementationsEx(ctx context.Context, params *lsproto.ImplementationParams, opts provideImplementationsOpts) (lsproto.ImplementationResponse, error) { - program, sourceFile := l.getProgramAndFile(params.TextDocument.Uri) - position := int(l.converters.LineAndCharacterToPosition(sourceFile, params.Position)) - node := astnav.GetTouchingPropertyName(sourceFile, position) - - var seenNodes collections.Set[*ast.Node] - var entries []*ReferenceEntry - queue := l.getImplementationReferenceEntries(ctx, program, node, position) - for len(queue) != 0 { - if ctx.Err() != nil { - return lsproto.LocationOrLocationsOrDefinitionLinksOrNull{}, ctx.Err() - } - - entry := queue[0] - queue = queue[1:] - if !seenNodes.Has(entry.node) { - seenNodes.Add(entry.node) - if !(opts.dropOriginNodes && entry.node.Loc.ContainsInclusive(position)) { - entries = append(entries, entry) - } - queue = append(queue, l.getImplementationReferenceEntries(ctx, program, entry.node, entry.node.Pos())...) - } - } - - if !opts.requireLocationsResult && lsproto.GetClientCapabilities(ctx).TextDocument.Implementation.LinkSupport { + if !options.RequireLocationsResult && lsproto.GetClientCapabilities(ctx).TextDocument.Implementation.LinkSupport { links := l.convertEntriesToLocationLinks(entries) return lsproto.LocationOrLocationsOrDefinitionLinksOrNull{DefinitionLinks: &links}, nil } @@ -705,19 +678,13 @@ func (l *LanguageService) provideImplementationsEx(ctx context.Context, params * return lsproto.LocationOrLocationsOrDefinitionLinksOrNull{Locations: &locations}, nil } -func (l *LanguageService) getImplementationReferenceEntries(ctx context.Context, program *compiler.Program, node *ast.Node, position int) []*ReferenceEntry { - options := refOptions{use: referenceUseReferences, implementations: true} - symbolsAndEntries := l.getReferencedSymbolsForNode(ctx, position, node, program, program.GetSourceFiles(), options, nil) - return core.FlatMap(symbolsAndEntries, func(s *SymbolAndEntries) []*ReferenceEntry { return s.references }) -} - -func (l *LanguageService) ProvideRenameFromSymbolAndEntries(ctx context.Context, params *lsproto.RenameParams, originalNode *ast.Node, symbolsAndEntries []*SymbolAndEntries) (lsproto.WorkspaceEditOrNull, error) { - if originalNode.Kind != ast.KindIdentifier { +func (l *LanguageService) ProvideRenameFromSymbolAndEntries(ctx context.Context, params *lsproto.RenameParams, data SymbolAndEntriesData, options SymbolEntryTransformOptions) (lsproto.WorkspaceEditOrNull, error) { + if data.OriginalNode.Kind != ast.KindIdentifier { return lsproto.WorkspaceEditOrNull{}, nil } program := l.GetProgram() - entries := core.FlatMap(symbolsAndEntries, func(s *SymbolAndEntries) []*ReferenceEntry { return s.references }) + entries := core.FlatMap(data.SymbolsAndEntries, func(s *SymbolAndEntries) []*ReferenceEntry { return s.references }) changes := make(map[lsproto.DocumentUri][]*lsproto.TextEdit) checker, done := program.GetTypeChecker(ctx) defer done() @@ -725,7 +692,7 @@ func (l *LanguageService) ProvideRenameFromSymbolAndEntries(ctx context.Context, uri := l.getFileNameOfEntry(entry) textEdit := &lsproto.TextEdit{ Range: *l.getRangeOfEntry(entry), - NewText: l.getTextForRename(originalNode, entry, params.NewName, checker), + NewText: l.getTextForRename(data.OriginalNode, entry, params.NewName, checker), } changes[uri] = append(changes[uri], textEdit) } diff --git a/internal/lsp/server.go b/internal/lsp/server.go index 337ddde8c0..cbb96c557f 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -13,9 +13,9 @@ import ( "time" "github.com/go-json-experiment/json" - "github.com/microsoft/typescript-go/internal/ast" "github.com/microsoft/typescript-go/internal/collections" "github.com/microsoft/typescript-go/internal/core" + "github.com/microsoft/typescript-go/internal/diagnostics" "github.com/microsoft/typescript-go/internal/jsonutil" "github.com/microsoft/typescript-go/internal/locale" "github.com/microsoft/typescript-go/internal/ls" @@ -655,11 +655,11 @@ type response[Resp any] struct { func registerMultiProjectReferenceRequestHandler[Req lsproto.HasTextDocumentPosition, Resp any]( handlers handlerMap, info lsproto.RequestInfo[Req, Resp], - fn func(*Server, context.Context, *ls.LanguageService, Req, *ast.Node, []*ls.SymbolAndEntries) (Resp, error), + fn func(*Server, context.Context, *ls.LanguageService, Req, ls.SymbolAndEntriesData, ls.SymbolEntryTransformOptions) (Resp, error), combineResults func(iter.Seq[*Resp]) Resp, ) { handlers[info.Method] = func(s *Server, ctx context.Context, req *lsproto.RequestMessage) error { - resp, err := multiProjectRequestHandling(s, ctx, info, fn, combineResults, req) + resp, err := multiProjectRequestHandling(s, ctx, info, fn, combineResults, req, ls.SymbolEntryTransformOptions{}) if err != nil { return err } @@ -672,9 +672,10 @@ func multiProjectRequestHandling[Req lsproto.HasTextDocumentPosition, Resp any]( s *Server, ctx context.Context, info lsproto.RequestInfo[Req, Resp], - fn func(*Server, context.Context, *ls.LanguageService, Req, *ast.Node, []*ls.SymbolAndEntries) (Resp, error), + fn func(*Server, context.Context, *ls.LanguageService, Req, ls.SymbolAndEntriesData, ls.SymbolEntryTransformOptions) (Resp, error), combineResults func(iter.Seq[*Resp]) Resp, req *lsproto.RequestMessage, + options ls.SymbolEntryTransformOptions, ) (Resp, error) { var resp Resp var params Req @@ -717,12 +718,12 @@ func multiProjectRequestHandling[Req lsproto.HasTextDocumentPosition, Resp any]( return } } - originalNode, symbolsAndEntries, ok := ls.ProvideSymbolsAndEntries(ctx, item.Uri, item.Position, info.Method == lsproto.MethodTextDocumentRename, info.Method == lsproto.MethodTextDocumentImplementation) + data, ok := ls.ProvideSymbolsAndEntries(ctx, item.Uri, item.Position, info.Method == lsproto.MethodTextDocumentRename, info.Method == lsproto.MethodTextDocumentImplementation) if ctx.Err() != nil { return } if ok { - for _, entry := range symbolsAndEntries { + for _, entry := range data.SymbolsAndEntries { // Find the default definition that can be in another project // Later we will use this load ancestor tree that references this location and expand search if item.project == defaultProject && defaultDefinition == nil { @@ -749,7 +750,7 @@ func multiProjectRequestHandling[Req lsproto.HasTextDocumentPosition, Resp any]( } } - if result, errSearch := fn(s, ctx, ls, params, originalNode, symbolsAndEntries); errSearch == nil { + if result, errSearch := fn(s, ctx, ls, params, data, options); errSearch == nil { response.complete = true response.result = result response.forOriginalLocation = item.forOriginalLocation @@ -1202,9 +1203,9 @@ func (s *Server) handleTypeDefinition(ctx context.Context, ls *ls.LanguageServic return ls.ProvideTypeDefinition(ctx, params.TextDocument.Uri, params.Position) } -func (s *Server) handleReferences(ctx context.Context, ls *ls.LanguageService, params *lsproto.ReferenceParams, originalNode *ast.Node, symbolAndEntries []*ls.SymbolAndEntries) (lsproto.ReferencesResponse, error) { +func (s *Server) handleReferences(ctx context.Context, ls *ls.LanguageService, params *lsproto.ReferenceParams, data ls.SymbolAndEntriesData, options ls.SymbolEntryTransformOptions) (lsproto.ReferencesResponse, error) { // findAllReferences - return ls.ProvideReferencesFromSymbolAndEntries(ctx, params, originalNode, symbolAndEntries) + return ls.ProvideReferencesFromSymbolAndEntries(ctx, params, data, options) } func combineLocationArray[T lsproto.HasLocation]( @@ -1239,9 +1240,9 @@ func combineReferences(results iter.Seq[*lsproto.ReferencesResponse]) lsproto.Re return lsproto.LocationsOrNull{Locations: combineResponseLocations(results)} } -func (s *Server) handleImplementations(ctx context.Context, ls *ls.LanguageService, params *lsproto.ImplementationParams, originalNode *ast.Node, symbolAndEntries []*ls.SymbolAndEntries) (lsproto.ImplementationResponse, error) { +func (s *Server) handleImplementations(ctx context.Context, ls *ls.LanguageService, params *lsproto.ImplementationParams, data ls.SymbolAndEntriesData, options ls.SymbolEntryTransformOptions) (lsproto.ImplementationResponse, error) { // goToImplementation - return ls.ProvideImplementationsFromSymbolAndEntries(ctx, params, originalNode, symbolAndEntries) + return ls.ProvideImplementationsFromSymbolAndEntries(ctx, params, data, options) } func combineImplementations(results iter.Seq[*lsproto.ImplementationResponse]) lsproto.ImplementationResponse { @@ -1324,8 +1325,8 @@ func (s *Server) handleDocumentSymbol(ctx context.Context, ls *ls.LanguageServic return ls.ProvideDocumentSymbols(ctx, params.TextDocument.Uri) } -func (s *Server) handleRename(ctx context.Context, ls *ls.LanguageService, params *lsproto.RenameParams, originalNode *ast.Node, symbolAndEntries []*ls.SymbolAndEntries) (lsproto.RenameResponse, error) { - return ls.ProvideRenameFromSymbolAndEntries(ctx, params, originalNode, symbolAndEntries) +func (s *Server) handleRename(ctx context.Context, ls *ls.LanguageService, params *lsproto.RenameParams, data ls.SymbolAndEntriesData, options ls.SymbolEntryTransformOptions) (lsproto.RenameResponse, error) { + return ls.ProvideRenameFromSymbolAndEntries(ctx, params, data, options) } func combineRenameResponse(results iter.Seq[*lsproto.RenameResponse]) lsproto.RenameResponse { @@ -1392,13 +1393,110 @@ func (s *Server) handleCodeLens(ctx context.Context, ls *ls.LanguageService, par } func (s *Server) handleCodeLensResolve(ctx context.Context, codeLens *lsproto.CodeLens, reqMsg *lsproto.RequestMessage) (*lsproto.CodeLens, error) { - ls, err := s.session.GetLanguageService(ctx, codeLens.Data.Uri) - if err != nil { - return nil, err + textDocument := lsproto.TextDocumentIdentifier{Uri: codeLens.Data.Uri} + position := codeLens.Range.Start + var locs []lsproto.Location + var lensTitle string + switch codeLens.Data.Kind { + case lsproto.CodeLensKindReferences: + referencesResp, err := multiProjectRequestHandling( + s, + ctx, + lsproto.TextDocumentReferencesInfo, + (*Server).handleReferences, + combineReferences, + &lsproto.RequestMessage{ + ID: reqMsg.ID, + Params: &lsproto.ReferenceParams{ + TextDocument: textDocument, + Position: position, + Context: &lsproto.ReferenceContext{ + // Don't include the declaration in the references count. + IncludeDeclaration: false, + }, + }, + }, + ls.SymbolEntryTransformOptions{}, + ) + if ctx.Err() != nil { + return nil, ctx.Err() + } + if err != nil { + // This can happen if a codeLens/resolve request comes in after a program change. + // While it's true that handlers should latch onto a specific snapshot + // while processing requests, we just set `Data.Uri` based on + // some older snapshot's contents. The content could have been modified, + // or the file itself could have been removed from the session entirely. + // Note this won't bail out on every change, but will prevent crashing + // based on non-existent files and line maps from shortened files. + return codeLens, lsproto.ErrorCodeContentModified + } + if referencesResp.Locations != nil { + locs = *referencesResp.Locations + if len(locs) == 1 { + lensTitle = diagnostics.X_1_reference.Localize(locale.FromContext(ctx)) + } else { + lensTitle = diagnostics.X_0_references.Localize(locale.FromContext(ctx), len(locs)) + } + } + + case lsproto.CodeLensKindImplementations: + implResp, err := multiProjectRequestHandling( + s, + ctx, + lsproto.TextDocumentImplementationInfo, + (*Server).handleImplementations, + combineImplementations, + &lsproto.RequestMessage{ + ID: reqMsg.ID, + Params: &lsproto.ImplementationParams{ + TextDocument: textDocument, + Position: position, + }, + }, + // "Force" link support to be false so that we only get `Locations` back, + // and don't include the "current" node in the results. + ls.SymbolEntryTransformOptions{ + RequireLocationsResult: true, + DropOriginNodes: true, + }, + ) + if ctx.Err() != nil { + return nil, ctx.Err() + } + if err != nil { + // This can happen if a codeLens/resolve request comes in after a program change. + // While it's true that handlers should latch onto a specific snapshot + // while processing requests, we just set `Data.Uri` based on + // some older snapshot's contents. The content could have been modified, + // or the file itself could have been removed from the session entirely. + // Note this won't bail out on every change, but will prevent crashing + // based on non-existent files and line maps from shortened files. + return codeLens, lsproto.ErrorCodeContentModified + } + if implResp.Locations != nil { + locs = *implResp.Locations + if len(locs) == 1 { + lensTitle = diagnostics.X_1_implementation.Localize(locale.FromContext(ctx)) + } else { + lensTitle = diagnostics.X_0_implementations.Localize(locale.FromContext(ctx), len(locs)) + } + } } - defer s.recover(reqMsg) - return ls.ResolveCodeLens(ctx, codeLens, s.initializeParams.InitializationOptions.CodeLensShowLocationsCommandName) + cmd := &lsproto.Command{ + Title: lensTitle, + } + if len(locs) > 0 && s.initializeParams.InitializationOptions.CodeLensShowLocationsCommandName != nil { + cmd.Command = *s.initializeParams.InitializationOptions.CodeLensShowLocationsCommandName + cmd.Arguments = &[]any{ + codeLens.Data.Uri, + codeLens.Range.Start, + locs, + } + } + codeLens.Command = cmd + return codeLens, nil } func (s *Server) handlePrepareCallHierarchy( diff --git a/internal/project/untitled_test.go b/internal/project/untitled_test.go index 501b913add..4ba20ef71d 100644 --- a/internal/project/untitled_test.go +++ b/internal/project/untitled_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/microsoft/typescript-go/internal/bundled" + "github.com/microsoft/typescript-go/internal/ls" "github.com/microsoft/typescript-go/internal/ls/lsconv" "github.com/microsoft/typescript-go/internal/lsp/lsproto" "github.com/microsoft/typescript-go/internal/testutil/projecttestutil" @@ -71,9 +72,9 @@ x++;` Context: &lsproto.ReferenceContext{IncludeDeclaration: true}, } - originalNode, symbolAndEntries, ok := languageService.ProvideSymbolsAndEntries(ctx, refParams.TextDocumentURI(), refParams.Position, false, false) + data, ok := languageService.ProvideSymbolsAndEntries(ctx, refParams.TextDocumentURI(), refParams.Position, false, false) assert.Assert(t, ok) - resp, err := languageService.ProvideReferencesFromSymbolAndEntries(ctx, refParams, originalNode, symbolAndEntries) + resp, err := languageService.ProvideReferencesFromSymbolAndEntries(ctx, refParams, data, ls.SymbolEntryTransformOptions{}) assert.NilError(t, err) refs := *resp.Locations @@ -146,9 +147,9 @@ x++;` Context: &lsproto.ReferenceContext{IncludeDeclaration: true}, } - originalNode, symbolAndEntries, ok := languageService.ProvideSymbolsAndEntries(ctx, refParams.TextDocumentURI(), refParams.Position, false, false) + data, ok := languageService.ProvideSymbolsAndEntries(ctx, refParams.TextDocumentURI(), refParams.Position, false, false) assert.Assert(t, ok) - resp, err := languageService.ProvideReferencesFromSymbolAndEntries(ctx, refParams, originalNode, symbolAndEntries) + resp, err := languageService.ProvideReferencesFromSymbolAndEntries(ctx, refParams, data, ls.SymbolEntryTransformOptions{}) assert.NilError(t, err) refs := *resp.Locations diff --git a/testdata/baselines/reference/fourslash/codeLenses/codeLensOnFunctionAcrossProjects1.baseline.jsonc b/testdata/baselines/reference/fourslash/codeLenses/codeLensOnFunctionAcrossProjects1.baseline.jsonc index 0cf41af97d..0fcab7f983 100644 --- a/testdata/baselines/reference/fourslash/codeLenses/codeLensOnFunctionAcrossProjects1.baseline.jsonc +++ b/testdata/baselines/reference/fourslash/codeLenses/codeLensOnFunctionAcrossProjects1.baseline.jsonc @@ -1,5 +1,10 @@ // === Code Lenses === // === /a/src/foo.ts === -// export function /*CODELENS: 1 reference*/aaa() {} +// export function /*CODELENS: 2 references*/aaa() {} // [|aaa|](); +// + +// === /b/src/bar.ts === +// import * as foo from '../../a/dist/foo.js'; +// foo.[|aaa|](); // \ No newline at end of file diff --git a/testdata/baselines/reference/fourslash/state/codeLensAcrossProjects.baseline b/testdata/baselines/reference/fourslash/state/codeLensAcrossProjects.baseline index 439b3ac5e3..1d19ad7caa 100644 --- a/testdata/baselines/reference/fourslash/state/codeLensAcrossProjects.baseline +++ b/testdata/baselines/reference/fourslash/state/codeLensAcrossProjects.baseline @@ -245,10 +245,72 @@ Config File Names:: } } +Projects:: + [/projects/container/compositeExec/tsconfig.json] *new* + /projects/container/lib/index.ts + /projects/container/compositeExec/index.ts + [/projects/container/exec/tsconfig.json] *new* + /projects/container/lib/index.ts + /projects/container/exec/index.ts + [/projects/container/lib/tsconfig.json] + /projects/container/lib/index.ts + /projects/container/lib/bar.ts + [/projects/container/tsconfig.json] *modified* + [/projects/temp/tsconfig.json] + /projects/temp/temp.ts +Open Files:: + [/projects/container/lib/index.ts] *modified* + /projects/container/compositeExec/tsconfig.json *new* + /projects/container/exec/tsconfig.json *new* + /projects/container/lib/tsconfig.json (default) + [/projects/temp/temp.ts] + /projects/temp/tsconfig.json (default) +Config:: + [/projects/container/compositeExec/tsconfig.json] *new* + RetainingProjects: + /projects/container/compositeExec/tsconfig.json + /projects/container/tsconfig.json + [/projects/container/exec/tsconfig.json] *new* + RetainingProjects: + /projects/container/exec/tsconfig.json + /projects/container/tsconfig.json + [/projects/container/lib/tsconfig.json] *modified* + RetainingProjects: *modified* + /projects/container/compositeExec/tsconfig.json *new* + /projects/container/exec/tsconfig.json *new* + /projects/container/lib/tsconfig.json + /projects/container/tsconfig.json *new* + RetainingOpenFiles: + /projects/container/lib/index.ts + [/projects/container/tsconfig.json] *new* + RetainingProjects: + /projects/container/tsconfig.json + [/projects/temp/tsconfig.json] + RetainingProjects: + /projects/temp/tsconfig.json + RetainingOpenFiles: + /projects/temp/temp.ts + // === Code Lenses === +// === /projects/container/compositeExec/index.ts === +// import { Pointable } from "../lib"; +// class Point2 implements [|Pointable|] { +// getX(): number { +// return 0; +// } +// --- (line: 6) skipped --- + +// === /projects/container/exec/index.ts === +// import { Pointable } from "../lib"; +// class Point1 implements [|Pointable|] { +// getX(): number { +// return 0; +// } +// --- (line: 6) skipped --- + // === /projects/container/lib/bar.ts === // import { Pointable } from "./index"; // class Point implements [|Pointable|] { @@ -259,7 +321,7 @@ Config File Names:: // === /projects/container/lib/index.ts === // -// export interface /*CODELENS: 1 reference*/Pointable { +// export interface /*CODELENS: 3 references*/Pointable { // getX(): number; // getY(): number; // } @@ -288,6 +350,22 @@ Config File Names:: // === Code Lenses === +// === /projects/container/compositeExec/index.ts === +// import { Pointable } from "../lib"; +// class [|Point2|] implements Pointable { +// getX(): number { +// return 0; +// } +// --- (line: 6) skipped --- + +// === /projects/container/exec/index.ts === +// import { Pointable } from "../lib"; +// class [|Point1|] implements Pointable { +// getX(): number { +// return 0; +// } +// --- (line: 6) skipped --- + // === /projects/container/lib/bar.ts === // import { Pointable } from "./index"; // class [|Point|] implements Pointable { @@ -298,7 +376,7 @@ Config File Names:: // === /projects/container/lib/index.ts === // -// export interface /*CODELENS: 1 implementation*/Pointable { +// export interface /*CODELENS: 3 implementations*/Pointable { // getX(): number; // getY(): number; // } @@ -358,6 +436,24 @@ Config File Names:: // === Code Lenses === +// === /projects/container/compositeExec/index.ts === +// import { Pointable } from "../lib"; +// class Point2 implements Pointable { +// [|getX|](): number { +// return 0; +// } +// getY(): number { +// --- (line: 7) skipped --- + +// === /projects/container/exec/index.ts === +// import { Pointable } from "../lib"; +// class Point1 implements Pointable { +// [|getX|](): number { +// return 0; +// } +// getY(): number { +// --- (line: 7) skipped --- + // === /projects/container/lib/bar.ts === // import { Pointable } from "./index"; // class Point implements Pointable { @@ -370,7 +466,7 @@ Config File Names:: // === /projects/container/lib/index.ts === // // export interface Pointable { -// /*CODELENS: 1 implementation*/getX(): number; +// /*CODELENS: 3 implementations*/getX(): number; // getY(): number; // } // export const val = 42; @@ -429,6 +525,28 @@ Config File Names:: // === Code Lenses === +// === /projects/container/compositeExec/index.ts === +// import { Pointable } from "../lib"; +// class Point2 implements Pointable { +// getX(): number { +// return 0; +// } +// [|getY|](): number { +// return 0; +// } +// } + +// === /projects/container/exec/index.ts === +// import { Pointable } from "../lib"; +// class Point1 implements Pointable { +// getX(): number { +// return 0; +// } +// [|getY|](): number { +// return 0; +// } +// } + // === /projects/container/lib/bar.ts === // import { Pointable } from "./index"; // class Point implements Pointable { @@ -444,7 +562,7 @@ Config File Names:: // // export interface Pointable { // getX(): number; -// /*CODELENS: 1 implementation*/getY(): number; +// /*CODELENS: 3 implementations*/getY(): number; // } // export const val = 42; { @@ -557,7 +675,9 @@ Config File Names:: Open Files:: [/projects/container/lib/index.ts] - /projects/container/lib/tsconfig.json (default) + /projects/container/compositeExec/tsconfig.json + /projects/container/exec/tsconfig.json + /projects/container/lib/tsconfig.json (default) [/projects/temp/temp.ts] *closed* { @@ -574,7 +694,9 @@ Open Files:: Open Files:: [/projects/container/lib/index.ts] - /projects/container/lib/tsconfig.json (default) + /projects/container/compositeExec/tsconfig.json + /projects/container/exec/tsconfig.json + /projects/container/lib/tsconfig.json (default) [/projects/temp/temp.ts] *new* /projects/temp/tsconfig.json (default) @@ -617,6 +739,12 @@ Open Files:: } Projects:: + [/projects/container/compositeExec/tsconfig.json] *deleted* + /projects/container/lib/index.ts + /projects/container/compositeExec/index.ts + [/projects/container/exec/tsconfig.json] *deleted* + /projects/container/lib/index.ts + /projects/container/exec/index.ts [/projects/container/lib/tsconfig.json] *deleted* /projects/container/lib/index.ts /projects/container/lib/bar.ts @@ -627,7 +755,10 @@ Open Files:: [/projects/temp/temp.ts] *new* /projects/temp/tsconfig.json (default) Config:: + [/projects/container/compositeExec/tsconfig.json] *deleted* + [/projects/container/exec/tsconfig.json] *deleted* [/projects/container/lib/tsconfig.json] *deleted* + [/projects/container/tsconfig.json] *deleted* [/projects/temp/tsconfig.json] RetainingProjects: /projects/temp/tsconfig.json From 26c20af153c49e4925bc29973f01702be66853c7 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Thu, 4 Dec 2025 20:16:23 -0800 Subject: [PATCH 06/16] Rename --- internal/fourslash/tests/stateimplementations_test.go | 2 +- ...angement.baseline => implementationsAcrossProjects.baseline} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename testdata/baselines/reference/fourslash/state/{implementationsAncestorProjectRefMangement.baseline => implementationsAcrossProjects.baseline} (100%) diff --git a/internal/fourslash/tests/stateimplementations_test.go b/internal/fourslash/tests/stateimplementations_test.go index 7332e48abc..92c816dbe9 100644 --- a/internal/fourslash/tests/stateimplementations_test.go +++ b/internal/fourslash/tests/stateimplementations_test.go @@ -7,7 +7,7 @@ import ( "github.com/microsoft/typescript-go/internal/testutil" ) -func TestImplementationsAncestorProjectRefMangement(t *testing.T) { +func TestImplementationsAcrossProjects(t *testing.T) { t.Parallel() defer testutil.RecoverAndFail(t, "Panic on fourslash test") content := ` diff --git a/testdata/baselines/reference/fourslash/state/implementationsAncestorProjectRefMangement.baseline b/testdata/baselines/reference/fourslash/state/implementationsAcrossProjects.baseline similarity index 100% rename from testdata/baselines/reference/fourslash/state/implementationsAncestorProjectRefMangement.baseline rename to testdata/baselines/reference/fourslash/state/implementationsAcrossProjects.baseline From ab6b5772d88c595b476028855d7734e660b81d51 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Fri, 5 Dec 2025 09:38:03 -0800 Subject: [PATCH 07/16] rename function --- internal/lsp/server.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/lsp/server.go b/internal/lsp/server.go index cbb96c557f..6986ddc1d0 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -659,7 +659,7 @@ func registerMultiProjectReferenceRequestHandler[Req lsproto.HasTextDocumentPosi combineResults func(iter.Seq[*Resp]) Resp, ) { handlers[info.Method] = func(s *Server, ctx context.Context, req *lsproto.RequestMessage) error { - resp, err := multiProjectRequestHandling(s, ctx, info, fn, combineResults, req, ls.SymbolEntryTransformOptions{}) + resp, err := handleMultiProjectRequest(s, ctx, info, fn, combineResults, req, ls.SymbolEntryTransformOptions{}) if err != nil { return err } @@ -668,7 +668,7 @@ func registerMultiProjectReferenceRequestHandler[Req lsproto.HasTextDocumentPosi } } -func multiProjectRequestHandling[Req lsproto.HasTextDocumentPosition, Resp any]( +func handleMultiProjectRequest[Req lsproto.HasTextDocumentPosition, Resp any]( s *Server, ctx context.Context, info lsproto.RequestInfo[Req, Resp], @@ -1399,7 +1399,7 @@ func (s *Server) handleCodeLensResolve(ctx context.Context, codeLens *lsproto.Co var lensTitle string switch codeLens.Data.Kind { case lsproto.CodeLensKindReferences: - referencesResp, err := multiProjectRequestHandling( + referencesResp, err := handleMultiProjectRequest( s, ctx, lsproto.TextDocumentReferencesInfo, @@ -1441,7 +1441,7 @@ func (s *Server) handleCodeLensResolve(ctx context.Context, codeLens *lsproto.Co } case lsproto.CodeLensKindImplementations: - implResp, err := multiProjectRequestHandling( + implResp, err := handleMultiProjectRequest( s, ctx, lsproto.TextDocumentImplementationInfo, From 7f752c78fad7e5f0fff5da0cb4665e0aa19d2764 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Fri, 5 Dec 2025 09:50:26 -0800 Subject: [PATCH 08/16] Refactor to set --- internal/lsp/server.go | 6 +++--- internal/project/project.go | 6 +++--- internal/project/projectcollectionbuilder.go | 17 ++++++++-------- internal/project/session.go | 2 +- internal/project/snapshot.go | 21 +++++++++++++++++--- 5 files changed, 33 insertions(+), 19 deletions(-) diff --git a/internal/lsp/server.go b/internal/lsp/server.go index 6986ddc1d0..47d996249b 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -832,16 +832,16 @@ func handleMultiProjectRequest[Req lsproto.HasTextDocumentPosition, Resp any]( wg = core.NewWorkGroup(false) hasMoreWork := false if defaultDefinition != nil { - requestedProjectTrees := make(map[tspath.Path]struct{}) + var requestedProjectTrees collections.Set[tspath.Path] results.Range(func(key tspath.Path, response *response[Resp]) bool { if response.complete { - requestedProjectTrees[key] = struct{}{} + requestedProjectTrees.Add(key) } return true }) // Load more projects based on default definition found - for _, loadedProject := range s.session.GetSnapshotLoadingProjectTree(ctx, requestedProjectTrees).ProjectCollection.Projects() { + for _, loadedProject := range s.session.GetSnapshotLoadingProjectTree(ctx, &requestedProjectTrees).ProjectCollection.Projects() { if ctx.Err() != nil { return resp, ctx.Err() } diff --git a/internal/project/project.go b/internal/project/project.go index bb6aabfa66..890c318efa 100644 --- a/internal/project/project.go +++ b/internal/project/project.go @@ -288,16 +288,16 @@ func (p *Project) setPotentialProjectReference(configFilePath tspath.Path) { p.potentialProjectReferences.Add(configFilePath) } -func (p *Project) hasPotentialProjectReference(references map[tspath.Path]struct{}) bool { +func (p *Project) hasPotentialProjectReference(projectTreeRequest *ProjectTreeRequest) bool { if p.CommandLine != nil { for _, path := range p.CommandLine.ResolvedProjectReferencePaths() { - if _, has := references[p.toPath(path)]; has { + if projectTreeRequest.IsProjectReferenced(p.toPath(path)) { return true } } } else if p.potentialProjectReferences != nil { for path := range p.potentialProjectReferences.Keys() { - if _, has := references[path]; has { + if projectTreeRequest.IsProjectReferenced(path) { return true } } diff --git a/internal/project/projectcollectionbuilder.go b/internal/project/projectcollectionbuilder.go index e95ab6b54b..fb06c9f6cc 100644 --- a/internal/project/projectcollectionbuilder.go +++ b/internal/project/projectcollectionbuilder.go @@ -412,7 +412,7 @@ func (b *ProjectCollectionBuilder) DidRequestProject(projectId tspath.Path, logg } } -func (b *ProjectCollectionBuilder) DidRequestProjectTrees(projectsReferenced map[tspath.Path]struct{}, logger *logging.LogTree) { +func (b *ProjectCollectionBuilder) DidRequestProjectTrees(projectTreeRequest *ProjectTreeRequest, logger *logging.LogTree) { startTime := time.Now() var currentProjects []tspath.Path @@ -428,10 +428,10 @@ func (b *ProjectCollectionBuilder) DidRequestProjectTrees(projectsReferenced map if entry, ok := b.configuredProjects.Load(projectId); ok { // If this project has potential project reference for any of the project we are loading ancestor tree for // load this project first - if project := entry.Value(); project != nil && (projectsReferenced == nil || project.hasPotentialProjectReference(projectsReferenced)) { + if project := entry.Value(); project != nil && (projectTreeRequest.IsAllProjects() || project.hasPotentialProjectReference(projectTreeRequest)) { b.updateProgram(entry, logger) } - b.ensureProjectTree(wg, entry, projectsReferenced, &seenProjects, logger) + b.ensureProjectTree(wg, entry, projectTreeRequest, &seenProjects, logger) } }) } @@ -439,14 +439,14 @@ func (b *ProjectCollectionBuilder) DidRequestProjectTrees(projectsReferenced map if logger != nil { elapsed := time.Since(startTime) - logger.Log(fmt.Sprintf("Completed project tree request for %v in %v", maps.Keys(projectsReferenced), elapsed)) + logger.Log(fmt.Sprintf("Completed project tree request for %v in %v", projectTreeRequest.Projects(), elapsed)) } } func (b *ProjectCollectionBuilder) ensureProjectTree( wg core.WorkGroup, entry *dirty.SyncMapEntry[tspath.Path, *Project], - projectsReferenced map[tspath.Path]struct{}, + projectTreeRequest *ProjectTreeRequest, seenProjects *collections.SyncSet[tspath.Path], logger *logging.LogTree, ) { @@ -475,11 +475,10 @@ func (b *ProjectCollectionBuilder) ensureProjectTree( } for _, childConfig := range children { wg.Queue(func() { - if projectsReferenced != nil && program.RangeResolvedProjectReferenceInChildConfig( + if !projectTreeRequest.IsAllProjects() && program.RangeResolvedProjectReferenceInChildConfig( childConfig, func(referencePath tspath.Path, config *tsoptions.ParsedCommandLine, _ *tsoptions.ParsedCommandLine, _ int) bool { - _, isReferenced := projectsReferenced[referencePath] - return !isReferenced + return !projectTreeRequest.IsProjectReferenced(referencePath) }) { return } @@ -489,7 +488,7 @@ func (b *ProjectCollectionBuilder) ensureProjectTree( b.updateProgram(childProjectEntry, logger) // Ensure children for this project - b.ensureProjectTree(wg, childProjectEntry, projectsReferenced, seenProjects, logger) + b.ensureProjectTree(wg, childProjectEntry, projectTreeRequest, seenProjects, logger) }) } } diff --git a/internal/project/session.go b/internal/project/session.go index 84ecdc3d6b..2f0a965eae 100644 --- a/internal/project/session.go +++ b/internal/project/session.go @@ -505,7 +505,7 @@ func (s *Session) GetLanguageServiceForProjectWithFile(ctx context.Context, proj func (s *Session) GetSnapshotLoadingProjectTree( ctx context.Context, // If null, all project trees need to be loaded, otherwise only those that are referenced - requestedProjectTrees map[tspath.Path]struct{}, + requestedProjectTrees *collections.Set[tspath.Path], ) *Snapshot { snapshot := s.getSnapshot( ctx, diff --git a/internal/project/snapshot.go b/internal/project/snapshot.go index 9b4afbfa3c..ddd30078c9 100644 --- a/internal/project/snapshot.go +++ b/internal/project/snapshot.go @@ -139,7 +139,22 @@ type APISnapshotRequest struct { type ProjectTreeRequest struct { // If null, all project trees need to be loaded, otherwise only those that are referenced - referencedProjects map[tspath.Path]struct{} + referencedProjects *collections.Set[tspath.Path] +} + +func (p *ProjectTreeRequest) IsAllProjects() bool { + return p.referencedProjects == nil +} + +func (p *ProjectTreeRequest) IsProjectReferenced(projectID tspath.Path) bool { + return p.referencedProjects.Has(projectID) +} + +func (p *ProjectTreeRequest) Projects() []tspath.Path { + if p.referencedProjects == nil { + return nil + } + return slices.Collect(maps.Keys(p.referencedProjects.Keys())) } type ResourceRequest struct { @@ -214,7 +229,7 @@ func (s *Snapshot) Clone(ctx context.Context, change SnapshotChange, overlays ma details += fmt.Sprintf(" Projects: %v", change.Projects) } if change.ProjectTree != nil { - details += fmt.Sprintf(" ProjectTree: %v", slices.Collect(maps.Keys(change.ProjectTree.referencedProjects))) + details += fmt.Sprintf(" ProjectTree: %v", change.ProjectTree.Projects()) } return details } @@ -296,7 +311,7 @@ func (s *Snapshot) Clone(ctx context.Context, change SnapshotChange, overlays ma } if change.ProjectTree != nil { - projectCollectionBuilder.DidRequestProjectTrees(change.ProjectTree.referencedProjects, logger.Fork("DidRequestProjectTrees")) + projectCollectionBuilder.DidRequestProjectTrees(change.ProjectTree, logger.Fork("DidRequestProjectTrees")) } projectCollection, configFileRegistry := projectCollectionBuilder.Finalize(logger) From 1bddd75ef18d9dbcec150d8a93d842491df3dfa4 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Fri, 5 Dec 2025 11:28:17 -0800 Subject: [PATCH 09/16] Fix per feedback --- internal/lsp/lsproto/_generate/generate.mts | 4 +-- internal/lsp/lsproto/lsp_generated.go | 14 +++++----- internal/lsp/server.go | 30 +++++++++------------ 3 files changed, 22 insertions(+), 26 deletions(-) diff --git a/internal/lsp/lsproto/_generate/generate.mts b/internal/lsp/lsproto/_generate/generate.mts index c1083847f8..2ea801f879 100644 --- a/internal/lsp/lsproto/_generate/generate.mts +++ b/internal/lsp/lsproto/_generate/generate.mts @@ -1006,7 +1006,7 @@ function generateCode() { const locationUriProperty = getLocationUriProperty(structure); if (locationUriProperty) { // Generate Location method - writeLine(`func (s *${structure.name}) GetLocation() Location {`); + writeLine(`func (s ${structure.name}) GetLocation() Location {`); writeLine(`\treturn Location{`); writeLine(`\t\tUri: s.${locationUriProperty},`); writeLine(`\t\tRange: s.${locationUriProperty.replace(/Uri$/, "Range")},`); @@ -1584,7 +1584,7 @@ function generateCode() { // Generate GetLocations method if (hasLocations) { - writeLine(`func (o *${name}) GetLocations() *[]Location {`); + writeLine(`func (o ${name}) GetLocations() *[]Location {`); writeLine(`\treturn o.Locations`); writeLine(`}`); writeLine(""); diff --git a/internal/lsp/lsproto/lsp_generated.go b/internal/lsp/lsproto/lsp_generated.go index 54c7a31f7f..a93e686a73 100644 --- a/internal/lsp/lsproto/lsp_generated.go +++ b/internal/lsp/lsproto/lsp_generated.go @@ -109,7 +109,7 @@ type Location struct { Range Range `json:"range"` } -func (s *Location) GetLocation() Location { +func (s Location) GetLocation() Location { return Location{ Uri: s.Uri, Range: s.Range, @@ -1818,7 +1818,7 @@ type CallHierarchyItem struct { Data *CallHierarchyItemData `json:"data,omitzero"` } -func (s *CallHierarchyItem) GetLocation() Location { +func (s CallHierarchyItem) GetLocation() Location { return Location{ Uri: s.Uri, Range: s.Range, @@ -3786,7 +3786,7 @@ type TypeHierarchyItem struct { Data *TypeHierarchyItemData `json:"data,omitzero"` } -func (s *TypeHierarchyItem) GetLocation() Location { +func (s TypeHierarchyItem) GetLocation() Location { return Location{ Uri: s.Uri, Range: s.Range, @@ -11937,7 +11937,7 @@ type LocationLink struct { TargetSelectionRange Range `json:"targetSelectionRange"` } -func (s *LocationLink) GetLocation() Location { +func (s LocationLink) GetLocation() Location { return Location{ Uri: s.TargetUri, Range: s.TargetRange, @@ -27101,7 +27101,7 @@ func (o *LocationOrLocationsOrDefinitionLinksOrNull) UnmarshalJSONFrom(dec *json return fmt.Errorf("invalid LocationOrLocationsOrDefinitionLinksOrNull: %s", data) } -func (o *LocationOrLocationsOrDefinitionLinksOrNull) GetLocations() *[]Location { +func (o LocationOrLocationsOrDefinitionLinksOrNull) GetLocations() *[]Location { return o.Locations } @@ -27195,7 +27195,7 @@ func (o *LocationOrLocationsOrDeclarationLinksOrNull) UnmarshalJSONFrom(dec *jso return fmt.Errorf("invalid LocationOrLocationsOrDeclarationLinksOrNull: %s", data) } -func (o *LocationOrLocationsOrDeclarationLinksOrNull) GetLocations() *[]Location { +func (o LocationOrLocationsOrDeclarationLinksOrNull) GetLocations() *[]Location { return o.Locations } @@ -27951,7 +27951,7 @@ func (o *LocationsOrNull) UnmarshalJSONFrom(dec *jsontext.Decoder) error { return fmt.Errorf("invalid LocationsOrNull: %s", data) } -func (o *LocationsOrNull) GetLocations() *[]Location { +func (o LocationsOrNull) GetLocations() *[]Location { return o.Locations } diff --git a/internal/lsp/server.go b/internal/lsp/server.go index 47d996249b..f62c76dbf7 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -656,7 +656,7 @@ func registerMultiProjectReferenceRequestHandler[Req lsproto.HasTextDocumentPosi handlers handlerMap, info lsproto.RequestInfo[Req, Resp], fn func(*Server, context.Context, *ls.LanguageService, Req, ls.SymbolAndEntriesData, ls.SymbolEntryTransformOptions) (Resp, error), - combineResults func(iter.Seq[*Resp]) Resp, + combineResults func(iter.Seq[Resp]) Resp, ) { handlers[info.Method] = func(s *Server, ctx context.Context, req *lsproto.RequestMessage) error { resp, err := handleMultiProjectRequest(s, ctx, info, fn, combineResults, req, ls.SymbolEntryTransformOptions{}) @@ -673,7 +673,7 @@ func handleMultiProjectRequest[Req lsproto.HasTextDocumentPosition, Resp any]( ctx context.Context, info lsproto.RequestInfo[Req, Resp], fn func(*Server, context.Context, *ls.LanguageService, Req, ls.SymbolAndEntriesData, ls.SymbolEntryTransformOptions) (Resp, error), - combineResults func(iter.Seq[*Resp]) Resp, + combineResults func(iter.Seq[Resp]) Resp, req *lsproto.RequestMessage, options ls.SymbolEntryTransformOptions, ) (Resp, error) { @@ -782,11 +782,11 @@ func handleMultiProjectRequest[Req lsproto.HasTextDocumentPosition, Resp any]( } } - getResultsIterator := func() iter.Seq[*Resp] { - return func(yield func(*Resp) bool) { + getResultsIterator := func() iter.Seq[Resp] { + return func(yield func(Resp) bool) { var seenProjects collections.SyncSet[tspath.Path] if response, loaded := results.Load(defaultProject.Id()); loaded && response.complete { - if !yield(&response.result) { + if !yield(response.result) { return } } @@ -794,7 +794,7 @@ func handleMultiProjectRequest[Req lsproto.HasTextDocumentPosition, Resp any]( for _, project := range allProjects { if seenProjects.AddIfAbsent(project.Id()) { if response, loaded := results.Load(project.Id()); loaded && response.complete { - if !yield(&response.result) { + if !yield(response.result) { return } } @@ -803,14 +803,14 @@ func handleMultiProjectRequest[Req lsproto.HasTextDocumentPosition, Resp any]( // Prefer the searches from locations for default definition results.Range(func(key tspath.Path, response *response[Resp]) bool { if !response.forOriginalLocation && seenProjects.AddIfAbsent(key) && response.complete { - return yield(&response.result) + return yield(response.result) } return true }) // Then the searches from original locations results.Range(func(key tspath.Path, response *response[Resp]) bool { if response.forOriginalLocation && seenProjects.AddIfAbsent(key) && response.complete { - return yield(&response.result) + return yield(response.result) } return true }) @@ -886,7 +886,7 @@ func handleMultiProjectRequest[Req lsproto.HasTextDocumentPosition, Resp any]( } else { // Single result, return that directly for value := range getResultsIterator() { - resp = *value + resp = value break } } @@ -1226,17 +1226,13 @@ func combineResponseLocations[T lsproto.HasLocations](results iter.Seq[T]) *[]ls var seenLocations collections.Set[lsproto.Location] for resp := range results { if locations := resp.GetLocations(); locations != nil { - for _, loc := range *locations { - if seenLocations.AddIfAbsent(loc) { - combined = append(combined, loc) - } - } + combineLocationArray(combined, locations, &seenLocations) } } return &combined } -func combineReferences(results iter.Seq[*lsproto.ReferencesResponse]) lsproto.ReferencesResponse { +func combineReferences(results iter.Seq[lsproto.ReferencesResponse]) lsproto.ReferencesResponse { return lsproto.LocationsOrNull{Locations: combineResponseLocations(results)} } @@ -1245,7 +1241,7 @@ func (s *Server) handleImplementations(ctx context.Context, ls *ls.LanguageServi return ls.ProvideImplementationsFromSymbolAndEntries(ctx, params, data, options) } -func combineImplementations(results iter.Seq[*lsproto.ImplementationResponse]) lsproto.ImplementationResponse { +func combineImplementations(results iter.Seq[lsproto.ImplementationResponse]) lsproto.ImplementationResponse { var combined []*lsproto.LocationLink var seenLocations collections.Set[lsproto.Location] for resp := range results { @@ -1329,7 +1325,7 @@ func (s *Server) handleRename(ctx context.Context, ls *ls.LanguageService, param return ls.ProvideRenameFromSymbolAndEntries(ctx, params, data, options) } -func combineRenameResponse(results iter.Seq[*lsproto.RenameResponse]) lsproto.RenameResponse { +func combineRenameResponse(results iter.Seq[lsproto.RenameResponse]) lsproto.RenameResponse { combined := make(map[lsproto.DocumentUri][]*lsproto.TextEdit) seenChanges := make(map[lsproto.DocumentUri]*collections.Set[lsproto.Range]) // !!! this is not used any more so we will skip this part of deduplication and combining From 51d1de99e2590a71c0663ad86d0872652458f769 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Fri, 5 Dec 2025 12:47:11 -0800 Subject: [PATCH 10/16] Fix incorrect fn use --- 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 f62c76dbf7..9b01732f79 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -1226,7 +1226,7 @@ func combineResponseLocations[T lsproto.HasLocations](results iter.Seq[T]) *[]ls var seenLocations collections.Set[lsproto.Location] for resp := range results { if locations := resp.GetLocations(); locations != nil { - combineLocationArray(combined, locations, &seenLocations) + combined = combineLocationArray(combined, locations, &seenLocations) } } return &combined From 9aca74f03434d251081d6219d19d2bd969a01c83 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Fri, 5 Dec 2025 10:23:05 -0800 Subject: [PATCH 11/16] refactor to use ls methods --- internal/lsp/server.go | 30 ++++++++---------------------- 1 file changed, 8 insertions(+), 22 deletions(-) diff --git a/internal/lsp/server.go b/internal/lsp/server.go index 9b01732f79..5e17d6efac 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -552,9 +552,9 @@ var handlers = sync.OnceValue(func() handlerMap { registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentPrepareCallHierarchyInfo, (*Server).handlePrepareCallHierarchy) registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentFoldingRangeInfo, (*Server).handleFoldingRange) - registerMultiProjectReferenceRequestHandler(handlers, lsproto.TextDocumentReferencesInfo, (*Server).handleReferences, combineReferences) - registerMultiProjectReferenceRequestHandler(handlers, lsproto.TextDocumentRenameInfo, (*Server).handleRename, combineRenameResponse) - registerMultiProjectReferenceRequestHandler(handlers, lsproto.TextDocumentImplementationInfo, (*Server).handleImplementations, combineImplementations) + registerMultiProjectReferenceRequestHandler(handlers, lsproto.TextDocumentReferencesInfo, (*ls.LanguageService).ProvideReferencesFromSymbolAndEntries, combineReferences) + registerMultiProjectReferenceRequestHandler(handlers, lsproto.TextDocumentRenameInfo, (*ls.LanguageService).ProvideRenameFromSymbolAndEntries, combineRenameResponse) + registerMultiProjectReferenceRequestHandler(handlers, lsproto.TextDocumentImplementationInfo, (*ls.LanguageService).ProvideImplementationsFromSymbolAndEntries, combineImplementations) registerRequestHandler(handlers, lsproto.CallHierarchyIncomingCallsInfo, (*Server).handleCallHierarchyIncomingCalls) registerRequestHandler(handlers, lsproto.CallHierarchyOutgoingCallsInfo, (*Server).handleCallHierarchyOutgoingCalls) @@ -655,7 +655,7 @@ type response[Resp any] struct { func registerMultiProjectReferenceRequestHandler[Req lsproto.HasTextDocumentPosition, Resp any]( handlers handlerMap, info lsproto.RequestInfo[Req, Resp], - fn func(*Server, context.Context, *ls.LanguageService, Req, ls.SymbolAndEntriesData, ls.SymbolEntryTransformOptions) (Resp, error), + fn func(*ls.LanguageService, context.Context, Req, ls.SymbolAndEntriesData, ls.SymbolEntryTransformOptions) (Resp, error), combineResults func(iter.Seq[Resp]) Resp, ) { handlers[info.Method] = func(s *Server, ctx context.Context, req *lsproto.RequestMessage) error { @@ -672,7 +672,7 @@ func handleMultiProjectRequest[Req lsproto.HasTextDocumentPosition, Resp any]( s *Server, ctx context.Context, info lsproto.RequestInfo[Req, Resp], - fn func(*Server, context.Context, *ls.LanguageService, Req, ls.SymbolAndEntriesData, ls.SymbolEntryTransformOptions) (Resp, error), + fn func(*ls.LanguageService, context.Context, Req, ls.SymbolAndEntriesData, ls.SymbolEntryTransformOptions) (Resp, error), combineResults func(iter.Seq[Resp]) Resp, req *lsproto.RequestMessage, options ls.SymbolEntryTransformOptions, @@ -750,7 +750,7 @@ func handleMultiProjectRequest[Req lsproto.HasTextDocumentPosition, Resp any]( } } - if result, errSearch := fn(s, ctx, ls, params, data, options); errSearch == nil { + if result, errSearch := fn(ls, ctx, params, data, options); errSearch == nil { response.complete = true response.result = result response.forOriginalLocation = item.forOriginalLocation @@ -1203,11 +1203,6 @@ func (s *Server) handleTypeDefinition(ctx context.Context, ls *ls.LanguageServic return ls.ProvideTypeDefinition(ctx, params.TextDocument.Uri, params.Position) } -func (s *Server) handleReferences(ctx context.Context, ls *ls.LanguageService, params *lsproto.ReferenceParams, data ls.SymbolAndEntriesData, options ls.SymbolEntryTransformOptions) (lsproto.ReferencesResponse, error) { - // findAllReferences - return ls.ProvideReferencesFromSymbolAndEntries(ctx, params, data, options) -} - func combineLocationArray[T lsproto.HasLocation]( combined []T, locations *[]T, @@ -1236,11 +1231,6 @@ func combineReferences(results iter.Seq[lsproto.ReferencesResponse]) lsproto.Ref return lsproto.LocationsOrNull{Locations: combineResponseLocations(results)} } -func (s *Server) handleImplementations(ctx context.Context, ls *ls.LanguageService, params *lsproto.ImplementationParams, data ls.SymbolAndEntriesData, options ls.SymbolEntryTransformOptions) (lsproto.ImplementationResponse, error) { - // goToImplementation - return ls.ProvideImplementationsFromSymbolAndEntries(ctx, params, data, options) -} - func combineImplementations(results iter.Seq[lsproto.ImplementationResponse]) lsproto.ImplementationResponse { var combined []*lsproto.LocationLink var seenLocations collections.Set[lsproto.Location] @@ -1321,10 +1311,6 @@ func (s *Server) handleDocumentSymbol(ctx context.Context, ls *ls.LanguageServic return ls.ProvideDocumentSymbols(ctx, params.TextDocument.Uri) } -func (s *Server) handleRename(ctx context.Context, ls *ls.LanguageService, params *lsproto.RenameParams, data ls.SymbolAndEntriesData, options ls.SymbolEntryTransformOptions) (lsproto.RenameResponse, error) { - return ls.ProvideRenameFromSymbolAndEntries(ctx, params, data, options) -} - func combineRenameResponse(results iter.Seq[lsproto.RenameResponse]) lsproto.RenameResponse { combined := make(map[lsproto.DocumentUri][]*lsproto.TextEdit) seenChanges := make(map[lsproto.DocumentUri]*collections.Set[lsproto.Range]) @@ -1399,7 +1385,7 @@ func (s *Server) handleCodeLensResolve(ctx context.Context, codeLens *lsproto.Co s, ctx, lsproto.TextDocumentReferencesInfo, - (*Server).handleReferences, + (*ls.LanguageService).ProvideReferencesFromSymbolAndEntries, combineReferences, &lsproto.RequestMessage{ ID: reqMsg.ID, @@ -1441,7 +1427,7 @@ func (s *Server) handleCodeLensResolve(ctx context.Context, codeLens *lsproto.Co s, ctx, lsproto.TextDocumentImplementationInfo, - (*Server).handleImplementations, + (*ls.LanguageService).ProvideImplementationsFromSymbolAndEntries, combineImplementations, &lsproto.RequestMessage{ ID: reqMsg.ID, From d9c40d3a7228d53a1aaca7e2ac9aac663ec0bf98 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Fri, 5 Dec 2025 13:26:56 -0800 Subject: [PATCH 12/16] Move the orchestrating to ls package --- internal/ls/codelens.go | 77 +++++ internal/ls/crossproject.go | 347 +++++++++++++++++++ internal/ls/findallreferences.go | 72 +++- internal/lsp/server.go | 478 ++++---------------------- internal/project/project.go | 3 + internal/project/projectcollection.go | 5 +- internal/project/session.go | 4 +- internal/project/snapshot.go | 3 +- internal/project/untitled_test.go | 9 +- 9 files changed, 557 insertions(+), 441 deletions(-) create mode 100644 internal/ls/crossproject.go diff --git a/internal/ls/codelens.go b/internal/ls/codelens.go index edbca2926e..53d133098d 100644 --- a/internal/ls/codelens.go +++ b/internal/ls/codelens.go @@ -5,6 +5,8 @@ import ( "github.com/microsoft/typescript-go/internal/ast" "github.com/microsoft/typescript-go/internal/core" + "github.com/microsoft/typescript-go/internal/diagnostics" + "github.com/microsoft/typescript-go/internal/locale" "github.com/microsoft/typescript-go/internal/ls/lsutil" "github.com/microsoft/typescript-go/internal/lsp/lsproto" "github.com/microsoft/typescript-go/internal/scanner" @@ -52,6 +54,81 @@ func (l *LanguageService) ProvideCodeLenses(ctx context.Context, documentURI lsp }, nil } +func (l *LanguageService) ResolveCodeLens(ctx context.Context, codeLens *lsproto.CodeLens, showLocationsCommandName *string, orchestrator CrossProjectOrchestrator) (*lsproto.CodeLens, error) { + uri := codeLens.Data.Uri + textDoc := lsproto.TextDocumentIdentifier{Uri: uri} + locale := locale.FromContext(ctx) + var locs []lsproto.Location + var lensTitle string + switch codeLens.Data.Kind { + case lsproto.CodeLensKindReferences: + referencesResp, err := l.ProvideReferences(ctx, &lsproto.ReferenceParams{ + TextDocument: textDoc, + Position: codeLens.Range.Start, + Context: &lsproto.ReferenceContext{ + // Don't include the declaration in the references count. + IncludeDeclaration: false, + }, + }, orchestrator) + if err != nil { + return nil, err + } + if referencesResp.Locations != nil { + locs = *referencesResp.Locations + } + + if len(locs) == 1 { + lensTitle = diagnostics.X_1_reference.Localize(locale) + } else { + lensTitle = diagnostics.X_0_references.Localize(locale, len(locs)) + } + case lsproto.CodeLensKindImplementations: + + implementations, err := l.provideImplementationsEx( + ctx, + &lsproto.ImplementationParams{ + TextDocument: textDoc, + Position: codeLens.Range.Start, + }, + // "Force" link support to be false so that we only get `Locations` back, + // and don't include the "current" node in the results. + symbolEntryTransformOptions{ + requireLocationsResult: true, + dropOriginNodes: true, + }, + orchestrator, + ) + if err != nil { + return nil, err + } + + if implementations.Locations != nil { + locs = *implementations.Locations + } + + if len(locs) == 1 { + lensTitle = diagnostics.X_1_implementation.Localize(locale) + } else { + lensTitle = diagnostics.X_0_implementations.Localize(locale, len(locs)) + } + } + + cmd := &lsproto.Command{ + Title: lensTitle, + } + if len(locs) > 0 && showLocationsCommandName != nil { + cmd.Command = *showLocationsCommandName + cmd.Arguments = &[]any{ + uri, + codeLens.Range.Start, + locs, + } + } + + codeLens.Command = cmd + return codeLens, nil +} + func (l *LanguageService) newCodeLensForNode(fileUri lsproto.DocumentUri, file *ast.SourceFile, node *ast.Node, kind lsproto.CodeLensKind) *lsproto.CodeLens { nodeForRange := node nodeName := node.Name() diff --git a/internal/ls/crossproject.go b/internal/ls/crossproject.go new file mode 100644 index 0000000000..b46021d015 --- /dev/null +++ b/internal/ls/crossproject.go @@ -0,0 +1,347 @@ +package ls + +import ( + "context" + "iter" + "sync" + + "github.com/microsoft/typescript-go/internal/collections" + "github.com/microsoft/typescript-go/internal/compiler" + "github.com/microsoft/typescript-go/internal/core" + "github.com/microsoft/typescript-go/internal/lsp/lsproto" + "github.com/microsoft/typescript-go/internal/tspath" +) + +type Project interface { + Id() tspath.Path + GetProgram() *compiler.Program + HasFile(fileName string) bool +} + +type projectAndTextDocumentPosition struct { + project Project + ls *LanguageService + Uri lsproto.DocumentUri + Position lsproto.Position + forOriginalLocation bool +} + +type response[Resp any] struct { + complete bool + result Resp + forOriginalLocation bool +} + +type CrossProjectOrchestrator interface { + GetDefaultProject() Project + GetAllProjectsForInitialRequest() []Project + GetLanguageServiceForProjectWithFile(ctx context.Context, project Project, uri lsproto.DocumentUri) *LanguageService + GetProjectsForFile(ctx context.Context, uri lsproto.DocumentUri) ([]Project, error) + GetProjectsLoadingProjectTree(ctx context.Context, requestedProjectTrees *collections.Set[tspath.Path]) iter.Seq[Project] + Recover() +} + +func handleCrossProject[Req lsproto.HasTextDocumentPosition, Resp any]( + defaultLs *LanguageService, + ctx context.Context, + params Req, + orchestrator CrossProjectOrchestrator, + symbolAndEntriesToResp func(*LanguageService, context.Context, Req, SymbolAndEntriesData, symbolEntryTransformOptions) (Resp, error), + combineResults func(iter.Seq[Resp]) Resp, + isRename bool, + implementations bool, + options symbolEntryTransformOptions, +) (Resp, error) { + var resp Resp + var err error + + // Single project + if orchestrator == nil { + data, _ := defaultLs.provideSymbolsAndEntries(ctx, params.TextDocumentURI(), params.TextDocumentPosition(), isRename, implementations) + return symbolAndEntriesToResp(defaultLs, ctx, params, data, options) + } + + defaultProject := orchestrator.GetDefaultProject() + allProjects := orchestrator.GetAllProjectsForInitialRequest() + var results collections.SyncMap[tspath.Path, *response[Resp]] + var defaultDefinition *nonLocalDefinition + canSearchProject := func(project Project) bool { + _, searched := results.Load(project.Id()) + return !searched + } + wg := core.NewWorkGroup(false) + var errMu sync.Mutex + var enqueueItem func(item projectAndTextDocumentPosition) + enqueueItem = func(item projectAndTextDocumentPosition) { + var response response[Resp] + if _, loaded := results.LoadOrStore(item.project.Id(), &response); loaded { + return + } + wg.Queue(func() { + if ctx.Err() != nil { + return + } + defer orchestrator.Recover() + // Process the item + ls := item.ls + if ls == nil { + // Get it now + ls = orchestrator.GetLanguageServiceForProjectWithFile(ctx, item.project, item.Uri) + if ls == nil { + return + } + } + data, ok := ls.provideSymbolsAndEntries(ctx, item.Uri, item.Position, isRename, implementations) + if ctx.Err() != nil { + return + } + if ok { + for _, entry := range data.SymbolsAndEntries { + // Find the default definition that can be in another project + // Later we will use this load ancestor tree that references this location and expand search + if item.project == defaultProject && defaultDefinition == nil { + defaultDefinition = ls.getNonLocalDefinition(ctx, entry) + } + ls.forEachOriginalDefinitionLocation(ctx, entry, func(uri lsproto.DocumentUri, position lsproto.Position) { + // Get default configured project for this file + defProjects, errProjects := orchestrator.GetProjectsForFile(ctx, uri) + if errProjects != nil { + return + } + for _, defProject := range defProjects { + // Optimization: don't enqueue if will be discarded + if canSearchProject(defProject) { + enqueueItem(projectAndTextDocumentPosition{ + project: defProject, + Uri: uri, + Position: position, + forOriginalLocation: true, + }) + } + } + }) + } + } + + if result, errSearch := symbolAndEntriesToResp(ls, ctx, params, data, options); errSearch == nil { + response.complete = true + response.result = result + response.forOriginalLocation = item.forOriginalLocation + } else { + errMu.Lock() + defer errMu.Unlock() + if err == nil { + err = errSearch + } + } + }) + } + + // Initial set of projects and locations in the queue, starting with default project + enqueueItem(projectAndTextDocumentPosition{ + project: defaultProject, + ls: defaultLs, + Uri: params.TextDocumentURI(), + Position: params.TextDocumentPosition(), + }) + for _, project := range allProjects { + if project != defaultProject { + enqueueItem(projectAndTextDocumentPosition{ + project: project, + // TODO!! symlinks need to change the URI + Uri: params.TextDocumentURI(), + Position: params.TextDocumentPosition(), + }) + } + } + + getResultsIterator := func() iter.Seq[Resp] { + return func(yield func(Resp) bool) { + var seenProjects collections.SyncSet[tspath.Path] + if response, loaded := results.Load(defaultProject.Id()); loaded && response.complete { + if !yield(response.result) { + return + } + } + seenProjects.Add(defaultProject.Id()) + for _, project := range allProjects { + if seenProjects.AddIfAbsent(project.Id()) { + if response, loaded := results.Load(project.Id()); loaded && response.complete { + if !yield(response.result) { + return + } + } + } + } + // Prefer the searches from locations for default definition + results.Range(func(key tspath.Path, response *response[Resp]) bool { + if !response.forOriginalLocation && seenProjects.AddIfAbsent(key) && response.complete { + return yield(response.result) + } + return true + }) + // Then the searches from original locations + results.Range(func(key tspath.Path, response *response[Resp]) bool { + if response.forOriginalLocation && seenProjects.AddIfAbsent(key) && response.complete { + return yield(response.result) + } + return true + }) + } + } + + // Outer loop - to complete work if more is added after completing existing queue + for { + // Process existing known projects first + wg.RunAndWait() + if ctx.Err() != nil { + return resp, ctx.Err() + } + // No need to use mu here since we are not in parallel at this point + if err != nil { + return resp, err + } + + wg = core.NewWorkGroup(false) + hasMoreWork := false + if defaultDefinition != nil { + var requestedProjectTrees collections.Set[tspath.Path] + results.Range(func(key tspath.Path, response *response[Resp]) bool { + if response.complete { + requestedProjectTrees.Add(key) + } + return true + }) + + // Load more projects based on default definition found + for loadedProject := range orchestrator.GetProjectsLoadingProjectTree(ctx, &requestedProjectTrees) { + if ctx.Err() != nil { + return resp, ctx.Err() + } + + // Can loop forever without this (enqueue here, dequeue above, repeat) + if !canSearchProject(loadedProject) || loadedProject.GetProgram() == nil { + continue + } + + // Enqueue the project and location for further processing + if loadedProject.HasFile(defaultDefinition.TextDocumentURI().FileName()) { + enqueueItem(projectAndTextDocumentPosition{ + project: loadedProject, + Uri: defaultDefinition.TextDocumentURI(), + Position: defaultDefinition.TextDocumentPosition(), + }) + hasMoreWork = true + } else if sourcePos := defaultDefinition.GetSourcePosition(); sourcePos != nil && loadedProject.HasFile(sourcePos.TextDocumentURI().FileName()) { + enqueueItem(projectAndTextDocumentPosition{ + project: loadedProject, + Uri: sourcePos.TextDocumentURI(), + Position: sourcePos.TextDocumentPosition(), + }) + hasMoreWork = true + } else if generatedPos := defaultDefinition.GetGeneratedPosition(); generatedPos != nil && loadedProject.HasFile(generatedPos.TextDocumentURI().FileName()) { + enqueueItem(projectAndTextDocumentPosition{ + project: loadedProject, + Uri: generatedPos.TextDocumentURI(), + Position: generatedPos.TextDocumentPosition(), + }) + hasMoreWork = true + } + } + } + if !hasMoreWork { + break + } + } + + if results.Size() > 1 { + resp = combineResults(getResultsIterator()) + } else { + // Single result, return that directly + for value := range getResultsIterator() { + resp = value + break + } + } + return resp, nil +} + +func combineLocationArray[T lsproto.HasLocation]( + combined []T, + locations *[]T, + seen *collections.Set[lsproto.Location], +) []T { + for _, loc := range *locations { + if seen.AddIfAbsent(loc.GetLocation()) { + combined = append(combined, loc) + } + } + return combined +} + +func combineResponseLocations[T lsproto.HasLocations](results iter.Seq[T]) *[]lsproto.Location { + var combined []lsproto.Location + var seenLocations collections.Set[lsproto.Location] + for resp := range results { + if locations := resp.GetLocations(); locations != nil { + combined = combineLocationArray(combined, locations, &seenLocations) + } + } + return &combined +} + +func combineReferences(results iter.Seq[lsproto.ReferencesResponse]) lsproto.ReferencesResponse { + return lsproto.LocationsOrNull{Locations: combineResponseLocations(results)} +} + +func combineImplementations(results iter.Seq[lsproto.ImplementationResponse]) lsproto.ImplementationResponse { + var combined []*lsproto.LocationLink + var seenLocations collections.Set[lsproto.Location] + for resp := range results { + if definitionLinks := resp.DefinitionLinks; definitionLinks != nil { + combined = combineLocationArray(combined, definitionLinks, &seenLocations) + } else if locations := resp.Locations; locations != nil { + return lsproto.LocationOrLocationsOrDefinitionLinksOrNull{Locations: combineResponseLocations(results)} + } + } + return lsproto.LocationOrLocationsOrDefinitionLinksOrNull{DefinitionLinks: &combined} +} + +func combineRenameResponse(results iter.Seq[lsproto.RenameResponse]) lsproto.RenameResponse { + combined := make(map[lsproto.DocumentUri][]*lsproto.TextEdit) + seenChanges := make(map[lsproto.DocumentUri]*collections.Set[lsproto.Range]) + // !!! this is not used any more so we will skip this part of deduplication and combining + // DocumentChanges *[]TextDocumentEditOrCreateFileOrRenameFileOrDeleteFile `json:"documentChanges,omitzero"` + // ChangeAnnotations *map[string]*ChangeAnnotation `json:"changeAnnotations,omitzero"` + + for resp := range results { + if resp.WorkspaceEdit != nil && resp.WorkspaceEdit.Changes != nil { + for doc, changes := range *resp.WorkspaceEdit.Changes { + seenSet, ok := seenChanges[doc] + if !ok { + seenSet = &collections.Set[lsproto.Range]{} + seenChanges[doc] = seenSet + } + changesForDoc, exists := combined[doc] + if !exists { + changesForDoc = []*lsproto.TextEdit{} + } + for _, change := range changes { + if !seenSet.Has(change.Range) { + seenSet.Add(change.Range) + changesForDoc = append(changesForDoc, change) + } + } + combined[doc] = changesForDoc + } + } + } + if len(combined) > 0 { + return lsproto.RenameResponse{ + WorkspaceEdit: &lsproto.WorkspaceEdit{ + Changes: &combined, + }, + } + } + return lsproto.RenameResponse{} +} diff --git a/internal/ls/findallreferences.go b/internal/ls/findallreferences.go index 95d47c1c49..64c27ebd62 100644 --- a/internal/ls/findallreferences.go +++ b/internal/ls/findallreferences.go @@ -445,7 +445,7 @@ var _ lsproto.HasTextDocumentPosition = (*position)(nil) func (nld *position) TextDocumentURI() lsproto.DocumentUri { return nld.uri } func (nld *position) TextDocumentPosition() lsproto.Position { return nld.pos } -type NonLocalDefinition struct { +type nonLocalDefinition struct { position GetSourcePosition func() lsproto.HasTextDocumentPosition GetGeneratedPosition func() lsproto.HasTextDocumentPosition @@ -459,7 +459,7 @@ func getFileAndStartPosFromDeclaration(declaration *ast.Node) (*ast.SourceFile, return file, core.TextPos(textRange.Pos()) } -func (l *LanguageService) GetNonLocalDefinition(ctx context.Context, entry *SymbolAndEntries) *NonLocalDefinition { +func (l *LanguageService) getNonLocalDefinition(ctx context.Context, entry *SymbolAndEntries) *nonLocalDefinition { if !entry.canUseDefinitionSymbol() { return nil } @@ -472,7 +472,7 @@ func (l *LanguageService) GetNonLocalDefinition(ctx context.Context, entry *Symb if isDefinitionVisible(emitResolver, d) { file, startPos := getFileAndStartPosFromDeclaration(d) fileName := file.FileName() - return &NonLocalDefinition{ + return &nonLocalDefinition{ position: position{ uri: lsconv.FileNameToDocumentURI(fileName), pos: l.converters.PositionToLineAndCharacter(file, startPos), @@ -545,7 +545,7 @@ func isDefinitionVisible(emitResolver *checker.EmitResolver, declaration *ast.No } } -func (l *LanguageService) ForEachOriginalDefinitionLocation( +func (l *LanguageService) forEachOriginalDefinitionLocation( ctx context.Context, entry *SymbolAndEntries, cb func(lsproto.DocumentUri, lsproto.Position), @@ -576,11 +576,11 @@ func (l *LanguageService) ForEachOriginalDefinitionLocation( } } -type SymbolEntryTransformOptions struct { +type symbolEntryTransformOptions struct { // Force the result to be Location objects. - RequireLocationsResult bool + requireLocationsResult bool // Omit node(s) containing the original position. - DropOriginNodes bool + dropOriginNodes bool } type SymbolAndEntriesData struct { @@ -589,7 +589,7 @@ type SymbolAndEntriesData struct { Position int } -func (l *LanguageService) ProvideSymbolsAndEntries(ctx context.Context, uri lsproto.DocumentUri, documentPosition lsproto.Position, isRename bool, implementations bool) (SymbolAndEntriesData, bool) { +func (l *LanguageService) provideSymbolsAndEntries(ctx context.Context, uri lsproto.DocumentUri, documentPosition lsproto.Position, isRename bool, implementations bool) (SymbolAndEntriesData, bool) { // `findReferencedSymbols` except only computes the information needed to return reference locations program, sourceFile := l.getProgramAndFile(uri) position := int(l.converters.LineAndCharacterToPosition(sourceFile, documentPosition)) @@ -651,7 +651,21 @@ func (l *LanguageService) getSymbolAndEntries( return l.getReferencedSymbolsForNode(ctx, position, node, program, program.GetSourceFiles(), options, nil) } -func (l *LanguageService) ProvideReferencesFromSymbolAndEntries(ctx context.Context, params *lsproto.ReferenceParams, data SymbolAndEntriesData, options SymbolEntryTransformOptions) (lsproto.ReferencesResponse, error) { +func (l *LanguageService) ProvideReferences(ctx context.Context, params *lsproto.ReferenceParams, orchestrator CrossProjectOrchestrator) (lsproto.ReferencesResponse, error) { + return handleCrossProject( + l, + ctx, + params, + orchestrator, + (*LanguageService).symbolAndEntriesToReferences, + combineReferences, + false, /*isRename*/ + false, /*implementations*/ + symbolEntryTransformOptions{}, + ) +} + +func (l *LanguageService) symbolAndEntriesToReferences(ctx context.Context, params *lsproto.ReferenceParams, data SymbolAndEntriesData, options symbolEntryTransformOptions) (lsproto.ReferencesResponse, error) { // `findReferencedSymbols` except only computes the information needed to return reference locations locations := core.FlatMap(data.SymbolsAndEntries, func(s *SymbolAndEntries) []lsproto.Location { return l.convertSymbolAndEntriesToLocations(s, params.Context.IncludeDeclaration) @@ -659,18 +673,36 @@ func (l *LanguageService) ProvideReferencesFromSymbolAndEntries(ctx context.Cont return lsproto.LocationsOrNull{Locations: &locations}, nil } -func (l *LanguageService) ProvideImplementationsFromSymbolAndEntries(ctx context.Context, params *lsproto.ImplementationParams, data SymbolAndEntriesData, options SymbolEntryTransformOptions) (lsproto.ImplementationResponse, error) { +func (l *LanguageService) ProvideImplementations(ctx context.Context, params *lsproto.ImplementationParams, orchestrator CrossProjectOrchestrator) (lsproto.ImplementationResponse, error) { + return l.provideImplementationsEx(ctx, params, symbolEntryTransformOptions{}, orchestrator) +} + +func (l *LanguageService) provideImplementationsEx(ctx context.Context, params *lsproto.ImplementationParams, options symbolEntryTransformOptions, orchestrator CrossProjectOrchestrator) (lsproto.ImplementationResponse, error) { + return handleCrossProject( + l, + ctx, + params, + orchestrator, + (*LanguageService).symbolAndEntriesToImplementations, + combineImplementations, + false, /*isRename*/ + true, /*implementations*/ + options, + ) +} + +func (l *LanguageService) symbolAndEntriesToImplementations(ctx context.Context, params *lsproto.ImplementationParams, data SymbolAndEntriesData, options symbolEntryTransformOptions) (lsproto.ImplementationResponse, error) { var seenNodes collections.Set[*ast.Node] var entries []*ReferenceEntry for _, entry := range data.SymbolsAndEntries { for _, ref := range entry.references { - if seenNodes.AddIfAbsent(ref.node) && (!options.DropOriginNodes || !ref.node.Loc.ContainsInclusive(data.Position)) { + if seenNodes.AddIfAbsent(ref.node) && (!options.dropOriginNodes || !ref.node.Loc.ContainsInclusive(data.Position)) { entries = append(entries, ref) } } } - if !options.RequireLocationsResult && lsproto.GetClientCapabilities(ctx).TextDocument.Implementation.LinkSupport { + if !options.requireLocationsResult && lsproto.GetClientCapabilities(ctx).TextDocument.Implementation.LinkSupport { links := l.convertEntriesToLocationLinks(entries) return lsproto.LocationOrLocationsOrDefinitionLinksOrNull{DefinitionLinks: &links}, nil } @@ -678,7 +710,21 @@ func (l *LanguageService) ProvideImplementationsFromSymbolAndEntries(ctx context return lsproto.LocationOrLocationsOrDefinitionLinksOrNull{Locations: &locations}, nil } -func (l *LanguageService) ProvideRenameFromSymbolAndEntries(ctx context.Context, params *lsproto.RenameParams, data SymbolAndEntriesData, options SymbolEntryTransformOptions) (lsproto.WorkspaceEditOrNull, error) { +func (l *LanguageService) ProvideRename(ctx context.Context, params *lsproto.RenameParams, orchestrator CrossProjectOrchestrator) (lsproto.WorkspaceEditOrNull, error) { + return handleCrossProject( + l, + ctx, + params, + orchestrator, + (*LanguageService).symbolAndEntriesToRename, + combineRenameResponse, + true, /*isRename*/ + false, /*implementations*/ + symbolEntryTransformOptions{}, + ) +} + +func (l *LanguageService) symbolAndEntriesToRename(ctx context.Context, params *lsproto.RenameParams, data SymbolAndEntriesData, options symbolEntryTransformOptions) (lsproto.WorkspaceEditOrNull, error) { if data.OriginalNode.Kind != ast.KindIdentifier { return lsproto.WorkspaceEditOrNull{}, nil } diff --git a/internal/lsp/server.go b/internal/lsp/server.go index 5e17d6efac..da79e6b71c 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -15,7 +15,6 @@ import ( "github.com/go-json-experiment/json" "github.com/microsoft/typescript-go/internal/collections" "github.com/microsoft/typescript-go/internal/core" - "github.com/microsoft/typescript-go/internal/diagnostics" "github.com/microsoft/typescript-go/internal/jsonutil" "github.com/microsoft/typescript-go/internal/locale" "github.com/microsoft/typescript-go/internal/ls" @@ -552,9 +551,9 @@ var handlers = sync.OnceValue(func() handlerMap { registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentPrepareCallHierarchyInfo, (*Server).handlePrepareCallHierarchy) registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentFoldingRangeInfo, (*Server).handleFoldingRange) - registerMultiProjectReferenceRequestHandler(handlers, lsproto.TextDocumentReferencesInfo, (*ls.LanguageService).ProvideReferencesFromSymbolAndEntries, combineReferences) - registerMultiProjectReferenceRequestHandler(handlers, lsproto.TextDocumentRenameInfo, (*ls.LanguageService).ProvideRenameFromSymbolAndEntries, combineRenameResponse) - registerMultiProjectReferenceRequestHandler(handlers, lsproto.TextDocumentImplementationInfo, (*ls.LanguageService).ProvideImplementationsFromSymbolAndEntries, combineImplementations) + registerMultiProjectReferenceRequestHandler(handlers, lsproto.TextDocumentReferencesInfo, (*ls.LanguageService).ProvideReferences) + registerMultiProjectReferenceRequestHandler(handlers, lsproto.TextDocumentRenameInfo, (*ls.LanguageService).ProvideRename) + registerMultiProjectReferenceRequestHandler(handlers, lsproto.TextDocumentImplementationInfo, (*ls.LanguageService).ProvideImplementations) registerRequestHandler(handlers, lsproto.CallHierarchyIncomingCallsInfo, (*Server).handleCallHierarchyIncomingCalls) registerRequestHandler(handlers, lsproto.CallHierarchyOutgoingCallsInfo, (*Server).handleCallHierarchyOutgoingCalls) @@ -638,28 +637,24 @@ func registerLanguageServiceDocumentRequestHandler[Req lsproto.HasTextDocumentUR } } -type projectAndTextDocumentPosition struct { - project *project.Project - ls *ls.LanguageService - Uri lsproto.DocumentUri - Position lsproto.Position - forOriginalLocation bool -} - -type response[Resp any] struct { - complete bool - result Resp - forOriginalLocation bool -} - func registerMultiProjectReferenceRequestHandler[Req lsproto.HasTextDocumentPosition, Resp any]( handlers handlerMap, info lsproto.RequestInfo[Req, Resp], - fn func(*ls.LanguageService, context.Context, Req, ls.SymbolAndEntriesData, ls.SymbolEntryTransformOptions) (Resp, error), - combineResults func(iter.Seq[Resp]) Resp, + fn func(*ls.LanguageService, context.Context, Req, ls.CrossProjectOrchestrator) (Resp, error), ) { handlers[info.Method] = func(s *Server, ctx context.Context, req *lsproto.RequestMessage) error { - resp, err := handleMultiProjectRequest(s, ctx, info, fn, combineResults, req, ls.SymbolEntryTransformOptions{}) + var params Req + // Ignore empty params. + if req.Params != nil { + params = req.Params.(Req) + } + // !!! sheetal: multiple projects that contain the file through symlinks + defaultProject, defaultLs, allProjects, err := s.session.GetLanguageServiceAndProjectsForFile(ctx, params.TextDocumentURI()) + if err != nil { + return err + } + defer s.recover(req) + resp, err := fn(defaultLs, ctx, params, &crossProjectOrchestrator{s, req, defaultProject, allProjects}) if err != nil { return err } @@ -668,229 +663,43 @@ func registerMultiProjectReferenceRequestHandler[Req lsproto.HasTextDocumentPosi } } -func handleMultiProjectRequest[Req lsproto.HasTextDocumentPosition, Resp any]( - s *Server, - ctx context.Context, - info lsproto.RequestInfo[Req, Resp], - fn func(*ls.LanguageService, context.Context, Req, ls.SymbolAndEntriesData, ls.SymbolEntryTransformOptions) (Resp, error), - combineResults func(iter.Seq[Resp]) Resp, - req *lsproto.RequestMessage, - options ls.SymbolEntryTransformOptions, -) (Resp, error) { - var resp Resp - var params Req - // Ignore empty params. - if req.Params != nil { - params = req.Params.(Req) - } - // !!! sheetal: multiple projects that contain the file through symlinks - defaultProject, defaultLs, allProjects, err := s.session.GetLanguageServiceAndProjectsForFile(ctx, params.TextDocumentURI()) - if err != nil { - return resp, err - } - defer s.recover(req) - - var results collections.SyncMap[tspath.Path, *response[Resp]] - var defaultDefinition *ls.NonLocalDefinition - canSearchProject := func(project *project.Project) bool { - _, searched := results.Load(project.Id()) - return !searched - } - wg := core.NewWorkGroup(false) - var errMu sync.Mutex - var enqueueItem func(item projectAndTextDocumentPosition) - enqueueItem = func(item projectAndTextDocumentPosition) { - var response response[Resp] - if _, loaded := results.LoadOrStore(item.project.Id(), &response); loaded { - return - } - wg.Queue(func() { - if ctx.Err() != nil { - return - } - defer s.recover(req) - // Process the item - ls := item.ls - if ls == nil { - // Get it now - ls = s.session.GetLanguageServiceForProjectWithFile(ctx, item.project, item.Uri) - if ls == nil { - return - } - } - data, ok := ls.ProvideSymbolsAndEntries(ctx, item.Uri, item.Position, info.Method == lsproto.MethodTextDocumentRename, info.Method == lsproto.MethodTextDocumentImplementation) - if ctx.Err() != nil { - return - } - if ok { - for _, entry := range data.SymbolsAndEntries { - // Find the default definition that can be in another project - // Later we will use this load ancestor tree that references this location and expand search - if item.project == defaultProject && defaultDefinition == nil { - defaultDefinition = ls.GetNonLocalDefinition(ctx, entry) - } - ls.ForEachOriginalDefinitionLocation(ctx, entry, func(uri lsproto.DocumentUri, position lsproto.Position) { - // Get default configured project for this file - defProjects, errProjects := s.session.GetProjectsForFile(ctx, uri) - if errProjects != nil { - return - } - for _, defProject := range defProjects { - // Optimization: don't enqueue if will be discarded - if canSearchProject(defProject) { - enqueueItem(projectAndTextDocumentPosition{ - project: defProject, - Uri: uri, - Position: position, - forOriginalLocation: true, - }) - } - } - }) - } - } - - if result, errSearch := fn(ls, ctx, params, data, options); errSearch == nil { - response.complete = true - response.result = result - response.forOriginalLocation = item.forOriginalLocation - } else { - errMu.Lock() - defer errMu.Unlock() - if err != nil { - err = errSearch - } - } - }) - } +type crossProjectOrchestrator struct { + server *Server + req *lsproto.RequestMessage + defaultProject *project.Project + allProjects []ls.Project +} - // Initial set of projects and locations in the queue, starting with default project - enqueueItem(projectAndTextDocumentPosition{ - project: defaultProject, - ls: defaultLs, - Uri: params.TextDocumentURI(), - Position: params.TextDocumentPosition(), - }) - for _, project := range allProjects { - if project != defaultProject { - enqueueItem(projectAndTextDocumentPosition{ - project: project, - // TODO!! symlinks need to change the URI - Uri: params.TextDocumentURI(), - Position: params.TextDocumentPosition(), - }) - } - } +var _ ls.CrossProjectOrchestrator = (*crossProjectOrchestrator)(nil) - getResultsIterator := func() iter.Seq[Resp] { - return func(yield func(Resp) bool) { - var seenProjects collections.SyncSet[tspath.Path] - if response, loaded := results.Load(defaultProject.Id()); loaded && response.complete { - if !yield(response.result) { - return - } - } - seenProjects.Add(defaultProject.Id()) - for _, project := range allProjects { - if seenProjects.AddIfAbsent(project.Id()) { - if response, loaded := results.Load(project.Id()); loaded && response.complete { - if !yield(response.result) { - return - } - } - } - } - // Prefer the searches from locations for default definition - results.Range(func(key tspath.Path, response *response[Resp]) bool { - if !response.forOriginalLocation && seenProjects.AddIfAbsent(key) && response.complete { - return yield(response.result) - } - return true - }) - // Then the searches from original locations - results.Range(func(key tspath.Path, response *response[Resp]) bool { - if response.forOriginalLocation && seenProjects.AddIfAbsent(key) && response.complete { - return yield(response.result) - } - return true - }) - } - } - - // Outer loop - to complete work if more is added after completing existing queue - for { - // Process existing known projects first - wg.RunAndWait() - if ctx.Err() != nil { - return resp, ctx.Err() - } - // No need to use mu here since we are not in parallel at this point - if err != nil { - return resp, err - } +func (c *crossProjectOrchestrator) GetDefaultProject() ls.Project { + return c.defaultProject +} - wg = core.NewWorkGroup(false) - hasMoreWork := false - if defaultDefinition != nil { - var requestedProjectTrees collections.Set[tspath.Path] - results.Range(func(key tspath.Path, response *response[Resp]) bool { - if response.complete { - requestedProjectTrees.Add(key) - } - return true - }) +func (c *crossProjectOrchestrator) GetAllProjectsForInitialRequest() []ls.Project { + return c.allProjects +} - // Load more projects based on default definition found - for _, loadedProject := range s.session.GetSnapshotLoadingProjectTree(ctx, &requestedProjectTrees).ProjectCollection.Projects() { - if ctx.Err() != nil { - return resp, ctx.Err() - } +func (c *crossProjectOrchestrator) GetLanguageServiceForProjectWithFile(ctx context.Context, p ls.Project, uri lsproto.DocumentUri) *ls.LanguageService { + return c.server.session.GetLanguageServiceForProjectWithFile(ctx, p.(*project.Project), uri) +} - // Can loop forever without this (enqueue here, dequeue above, repeat) - if !canSearchProject(loadedProject) || loadedProject.GetProgram() == nil { - continue - } +func (c *crossProjectOrchestrator) GetProjectsForFile(ctx context.Context, uri lsproto.DocumentUri) ([]ls.Project, error) { + return c.server.session.GetProjectsForFile(ctx, uri) +} - // Enqueue the project and location for further processing - if loadedProject.HasFile(defaultDefinition.TextDocumentURI().FileName()) { - enqueueItem(projectAndTextDocumentPosition{ - project: loadedProject, - Uri: defaultDefinition.TextDocumentURI(), - Position: defaultDefinition.TextDocumentPosition(), - }) - hasMoreWork = true - } else if sourcePos := defaultDefinition.GetSourcePosition(); sourcePos != nil && loadedProject.HasFile(sourcePos.TextDocumentURI().FileName()) { - enqueueItem(projectAndTextDocumentPosition{ - project: loadedProject, - Uri: sourcePos.TextDocumentURI(), - Position: sourcePos.TextDocumentPosition(), - }) - hasMoreWork = true - } else if generatedPos := defaultDefinition.GetGeneratedPosition(); generatedPos != nil && loadedProject.HasFile(generatedPos.TextDocumentURI().FileName()) { - enqueueItem(projectAndTextDocumentPosition{ - project: loadedProject, - Uri: generatedPos.TextDocumentURI(), - Position: generatedPos.TextDocumentPosition(), - }) - hasMoreWork = true - } +func (c *crossProjectOrchestrator) GetProjectsLoadingProjectTree(ctx context.Context, requestedProjectTrees *collections.Set[tspath.Path]) iter.Seq[ls.Project] { + return func(yield func(ls.Project) bool) { + for _, p := range c.server.session.GetSnapshotLoadingProjectTree(ctx, requestedProjectTrees).ProjectCollection.Projects() { + if !yield(p) { + return } } - if !hasMoreWork { - break - } } +} - if results.Size() > 1 { - resp = combineResults(getResultsIterator()) - } else { - // Single result, return that directly - for value := range getResultsIterator() { - resp = value - break - } - } - return resp, nil +func (c *crossProjectOrchestrator) Recover() { + c.server.recover(c.req) } func (s *Server) recover(req *lsproto.RequestMessage) { @@ -1203,47 +1012,6 @@ func (s *Server) handleTypeDefinition(ctx context.Context, ls *ls.LanguageServic return ls.ProvideTypeDefinition(ctx, params.TextDocument.Uri, params.Position) } -func combineLocationArray[T lsproto.HasLocation]( - combined []T, - locations *[]T, - seen *collections.Set[lsproto.Location], -) []T { - for _, loc := range *locations { - if seen.AddIfAbsent(loc.GetLocation()) { - combined = append(combined, loc) - } - } - return combined -} - -func combineResponseLocations[T lsproto.HasLocations](results iter.Seq[T]) *[]lsproto.Location { - var combined []lsproto.Location - var seenLocations collections.Set[lsproto.Location] - for resp := range results { - if locations := resp.GetLocations(); locations != nil { - combined = combineLocationArray(combined, locations, &seenLocations) - } - } - return &combined -} - -func combineReferences(results iter.Seq[lsproto.ReferencesResponse]) lsproto.ReferencesResponse { - return lsproto.LocationsOrNull{Locations: combineResponseLocations(results)} -} - -func combineImplementations(results iter.Seq[lsproto.ImplementationResponse]) lsproto.ImplementationResponse { - var combined []*lsproto.LocationLink - var seenLocations collections.Set[lsproto.Location] - for resp := range results { - if definitionLinks := resp.DefinitionLinks; definitionLinks != nil { - combined = combineLocationArray(combined, definitionLinks, &seenLocations) - } else if locations := resp.Locations; locations != nil { - return lsproto.LocationOrLocationsOrDefinitionLinksOrNull{Locations: combineResponseLocations(results)} - } - } - return lsproto.LocationOrLocationsOrDefinitionLinksOrNull{DefinitionLinks: &combined} -} - func (s *Server) handleCompletion(ctx context.Context, languageService *ls.LanguageService, params *lsproto.CompletionParams) (lsproto.CompletionResponse, error) { return languageService.ProvideCompletion( ctx, @@ -1311,45 +1079,6 @@ func (s *Server) handleDocumentSymbol(ctx context.Context, ls *ls.LanguageServic return ls.ProvideDocumentSymbols(ctx, params.TextDocument.Uri) } -func combineRenameResponse(results iter.Seq[lsproto.RenameResponse]) lsproto.RenameResponse { - combined := make(map[lsproto.DocumentUri][]*lsproto.TextEdit) - seenChanges := make(map[lsproto.DocumentUri]*collections.Set[lsproto.Range]) - // !!! this is not used any more so we will skip this part of deduplication and combining - // DocumentChanges *[]TextDocumentEditOrCreateFileOrRenameFileOrDeleteFile `json:"documentChanges,omitzero"` - // ChangeAnnotations *map[string]*ChangeAnnotation `json:"changeAnnotations,omitzero"` - - for resp := range results { - if resp.WorkspaceEdit != nil && resp.WorkspaceEdit.Changes != nil { - for doc, changes := range *resp.WorkspaceEdit.Changes { - seenSet, ok := seenChanges[doc] - if !ok { - seenSet = &collections.Set[lsproto.Range]{} - seenChanges[doc] = seenSet - } - changesForDoc, exists := combined[doc] - if !exists { - changesForDoc = []*lsproto.TextEdit{} - } - for _, change := range changes { - if !seenSet.Has(change.Range) { - seenSet.Add(change.Range) - changesForDoc = append(changesForDoc, change) - } - } - combined[doc] = changesForDoc - } - } - } - if len(combined) > 0 { - return lsproto.RenameResponse{ - WorkspaceEdit: &lsproto.WorkspaceEdit{ - Changes: &combined, - }, - } - } - return lsproto.RenameResponse{} -} - func (s *Server) handleDocumentHighlight(ctx context.Context, ls *ls.LanguageService, params *lsproto.DocumentHighlightParams) (lsproto.DocumentHighlightResponse, error) { return ls.ProvideDocumentHighlights(ctx, params.TextDocument.Uri, params.Position) } @@ -1375,110 +1104,27 @@ func (s *Server) handleCodeLens(ctx context.Context, ls *ls.LanguageService, par } func (s *Server) handleCodeLensResolve(ctx context.Context, codeLens *lsproto.CodeLens, reqMsg *lsproto.RequestMessage) (*lsproto.CodeLens, error) { - textDocument := lsproto.TextDocumentIdentifier{Uri: codeLens.Data.Uri} - position := codeLens.Range.Start - var locs []lsproto.Location - var lensTitle string - switch codeLens.Data.Kind { - case lsproto.CodeLensKindReferences: - referencesResp, err := handleMultiProjectRequest( - s, - ctx, - lsproto.TextDocumentReferencesInfo, - (*ls.LanguageService).ProvideReferencesFromSymbolAndEntries, - combineReferences, - &lsproto.RequestMessage{ - ID: reqMsg.ID, - Params: &lsproto.ReferenceParams{ - TextDocument: textDocument, - Position: position, - Context: &lsproto.ReferenceContext{ - // Don't include the declaration in the references count. - IncludeDeclaration: false, - }, - }, - }, - ls.SymbolEntryTransformOptions{}, - ) - if ctx.Err() != nil { - return nil, ctx.Err() - } - if err != nil { - // This can happen if a codeLens/resolve request comes in after a program change. - // While it's true that handlers should latch onto a specific snapshot - // while processing requests, we just set `Data.Uri` based on - // some older snapshot's contents. The content could have been modified, - // or the file itself could have been removed from the session entirely. - // Note this won't bail out on every change, but will prevent crashing - // based on non-existent files and line maps from shortened files. - return codeLens, lsproto.ErrorCodeContentModified - } - if referencesResp.Locations != nil { - locs = *referencesResp.Locations - if len(locs) == 1 { - lensTitle = diagnostics.X_1_reference.Localize(locale.FromContext(ctx)) - } else { - lensTitle = diagnostics.X_0_references.Localize(locale.FromContext(ctx), len(locs)) - } - } - - case lsproto.CodeLensKindImplementations: - implResp, err := handleMultiProjectRequest( - s, - ctx, - lsproto.TextDocumentImplementationInfo, - (*ls.LanguageService).ProvideImplementationsFromSymbolAndEntries, - combineImplementations, - &lsproto.RequestMessage{ - ID: reqMsg.ID, - Params: &lsproto.ImplementationParams{ - TextDocument: textDocument, - Position: position, - }, - }, - // "Force" link support to be false so that we only get `Locations` back, - // and don't include the "current" node in the results. - ls.SymbolEntryTransformOptions{ - RequireLocationsResult: true, - DropOriginNodes: true, - }, - ) - if ctx.Err() != nil { - return nil, ctx.Err() - } - if err != nil { - // This can happen if a codeLens/resolve request comes in after a program change. - // While it's true that handlers should latch onto a specific snapshot - // while processing requests, we just set `Data.Uri` based on - // some older snapshot's contents. The content could have been modified, - // or the file itself could have been removed from the session entirely. - // Note this won't bail out on every change, but will prevent crashing - // based on non-existent files and line maps from shortened files. - return codeLens, lsproto.ErrorCodeContentModified - } - if implResp.Locations != nil { - locs = *implResp.Locations - if len(locs) == 1 { - lensTitle = diagnostics.X_1_implementation.Localize(locale.FromContext(ctx)) - } else { - lensTitle = diagnostics.X_0_implementations.Localize(locale.FromContext(ctx), len(locs)) - } - } + defaultProject, defaultLs, allProjects, err := s.session.GetLanguageServiceAndProjectsForFile(ctx, codeLens.Data.Uri) + if ctx.Err() != nil { + return nil, ctx.Err() } - - cmd := &lsproto.Command{ - Title: lensTitle, - } - if len(locs) > 0 && s.initializeParams.InitializationOptions.CodeLensShowLocationsCommandName != nil { - cmd.Command = *s.initializeParams.InitializationOptions.CodeLensShowLocationsCommandName - cmd.Arguments = &[]any{ - codeLens.Data.Uri, - codeLens.Range.Start, - locs, - } + if err != nil { + // This can happen if a codeLens/resolve request comes in after a program change. + // While it's true that handlers should latch onto a specific snapshot + // while processing requests, we just set `Data.Uri` based on + // some older snapshot's contents. The content could have been modified, + // or the file itself could have been removed from the session entirely. + // Note this won't bail out on every change, but will prevent crashing + // based on non-existent files and line maps from shortened files. + return codeLens, lsproto.ErrorCodeContentModified } - codeLens.Command = cmd - return codeLens, nil + defer s.recover(reqMsg) + return defaultLs.ResolveCodeLens( + ctx, + codeLens, + s.initializeParams.InitializationOptions.CodeLensShowLocationsCommandName, + &crossProjectOrchestrator{s, reqMsg, defaultProject, allProjects}, + ) } func (s *Server) handlePrepareCallHierarchy( diff --git a/internal/project/project.go b/internal/project/project.go index 890c318efa..c73234c0ed 100644 --- a/internal/project/project.go +++ b/internal/project/project.go @@ -9,6 +9,7 @@ import ( "github.com/microsoft/typescript-go/internal/collections" "github.com/microsoft/typescript-go/internal/compiler" "github.com/microsoft/typescript-go/internal/core" + "github.com/microsoft/typescript-go/internal/ls" "github.com/microsoft/typescript-go/internal/lsp/lsproto" "github.com/microsoft/typescript-go/internal/project/ata" "github.com/microsoft/typescript-go/internal/project/logging" @@ -86,6 +87,8 @@ type Project struct { typingsFiles []string } +var _ ls.Project = (*Project)(nil) + func NewConfiguredProject( configFileName string, configFilePath tspath.Path, diff --git a/internal/project/projectcollection.go b/internal/project/projectcollection.go index 127e0986ed..079c80d0ce 100644 --- a/internal/project/projectcollection.go +++ b/internal/project/projectcollection.go @@ -6,6 +6,7 @@ import ( "github.com/microsoft/typescript-go/internal/collections" "github.com/microsoft/typescript-go/internal/core" + "github.com/microsoft/typescript-go/internal/ls" "github.com/microsoft/typescript-go/internal/tspath" ) @@ -93,8 +94,8 @@ func (c *ProjectCollection) InferredProject() *Project { return c.inferredProject } -func (c *ProjectCollection) GetProjectsContainingFile(path tspath.Path) []*Project { - var projects []*Project +func (c *ProjectCollection) GetProjectsContainingFile(path tspath.Path) []ls.Project { + var projects []ls.Project for _, project := range c.ConfiguredProjects() { if project.containsFile(path) { projects = append(projects, project) diff --git a/internal/project/session.go b/internal/project/session.go index 2f0a965eae..ede64c0ce8 100644 --- a/internal/project/session.go +++ b/internal/project/session.go @@ -464,7 +464,7 @@ func (s *Session) GetLanguageService(ctx context.Context, uri lsproto.DocumentUr return languageService, nil } -func (s *Session) GetLanguageServiceAndProjectsForFile(ctx context.Context, uri lsproto.DocumentUri) (*Project, *ls.LanguageService, []*Project, error) { +func (s *Session) GetLanguageServiceAndProjectsForFile(ctx context.Context, uri lsproto.DocumentUri) (*Project, *ls.LanguageService, []ls.Project, error) { snapshot, project, defaultLs, err := s.getSnapshotAndDefaultProject(ctx, uri) if err != nil { return nil, nil, nil, err @@ -474,7 +474,7 @@ func (s *Session) GetLanguageServiceAndProjectsForFile(ctx context.Context, uri return project, defaultLs, allProjects, nil } -func (s *Session) GetProjectsForFile(ctx context.Context, uri lsproto.DocumentUri) ([]*Project, error) { +func (s *Session) GetProjectsForFile(ctx context.Context, uri lsproto.DocumentUri) ([]ls.Project, error) { snapshot := s.getSnapshot( ctx, ResourceRequest{Documents: []lsproto.DocumentUri{uri}}, diff --git a/internal/project/snapshot.go b/internal/project/snapshot.go index ddd30078c9..c87aaac8cf 100644 --- a/internal/project/snapshot.go +++ b/internal/project/snapshot.go @@ -11,6 +11,7 @@ import ( "github.com/microsoft/typescript-go/internal/collections" "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/format" + "github.com/microsoft/typescript-go/internal/ls" "github.com/microsoft/typescript-go/internal/ls/lsconv" "github.com/microsoft/typescript-go/internal/ls/lsutil" "github.com/microsoft/typescript-go/internal/lsp/lsproto" @@ -78,7 +79,7 @@ func (s *Snapshot) GetDefaultProject(uri lsproto.DocumentUri) *Project { return s.ProjectCollection.GetDefaultProject(fileName, path) } -func (s *Snapshot) GetProjectsContainingFile(uri lsproto.DocumentUri) []*Project { +func (s *Snapshot) GetProjectsContainingFile(uri lsproto.DocumentUri) []ls.Project { fileName := uri.FileName() path := s.toPath(fileName) // TODO!! sheetal may be change this to handle symlinks!! diff --git a/internal/project/untitled_test.go b/internal/project/untitled_test.go index 4ba20ef71d..2208a84e8a 100644 --- a/internal/project/untitled_test.go +++ b/internal/project/untitled_test.go @@ -6,7 +6,6 @@ import ( "testing" "github.com/microsoft/typescript-go/internal/bundled" - "github.com/microsoft/typescript-go/internal/ls" "github.com/microsoft/typescript-go/internal/ls/lsconv" "github.com/microsoft/typescript-go/internal/lsp/lsproto" "github.com/microsoft/typescript-go/internal/testutil/projecttestutil" @@ -72,9 +71,7 @@ x++;` Context: &lsproto.ReferenceContext{IncludeDeclaration: true}, } - data, ok := languageService.ProvideSymbolsAndEntries(ctx, refParams.TextDocumentURI(), refParams.Position, false, false) - assert.Assert(t, ok) - resp, err := languageService.ProvideReferencesFromSymbolAndEntries(ctx, refParams, data, ls.SymbolEntryTransformOptions{}) + resp, err := languageService.ProvideReferences(ctx, refParams, nil) assert.NilError(t, err) refs := *resp.Locations @@ -147,9 +144,7 @@ x++;` Context: &lsproto.ReferenceContext{IncludeDeclaration: true}, } - data, ok := languageService.ProvideSymbolsAndEntries(ctx, refParams.TextDocumentURI(), refParams.Position, false, false) - assert.Assert(t, ok) - resp, err := languageService.ProvideReferencesFromSymbolAndEntries(ctx, refParams, data, ls.SymbolEntryTransformOptions{}) + resp, err := languageService.ProvideReferences(ctx, refParams, nil) assert.NilError(t, err) refs := *resp.Locations From d3ffd88c284eebb709b9f2f05c61059cf23f702d Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Thu, 4 Dec 2025 19:49:39 -0800 Subject: [PATCH 13/16] Remove duplicate --- internal/lsp/server.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/internal/lsp/server.go b/internal/lsp/server.go index da79e6b71c..afe35a66ea 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -558,9 +558,6 @@ var handlers = sync.OnceValue(func() handlerMap { registerRequestHandler(handlers, lsproto.CallHierarchyIncomingCallsInfo, (*Server).handleCallHierarchyIncomingCalls) registerRequestHandler(handlers, lsproto.CallHierarchyOutgoingCallsInfo, (*Server).handleCallHierarchyOutgoingCalls) - registerRequestHandler(handlers, lsproto.CallHierarchyIncomingCallsInfo, (*Server).handleCallHierarchyIncomingCalls) - registerRequestHandler(handlers, lsproto.CallHierarchyOutgoingCallsInfo, (*Server).handleCallHierarchyOutgoingCalls) - registerRequestHandler(handlers, lsproto.WorkspaceSymbolInfo, (*Server).handleWorkspaceSymbol) registerRequestHandler(handlers, lsproto.CompletionItemResolveInfo, (*Server).handleCompletionItemResolve) registerRequestHandler(handlers, lsproto.CodeLensResolveInfo, (*Server).handleCodeLensResolve) From 51eadd5d4bdacc69488816b0b516bcd7cd28bf12 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Thu, 4 Dec 2025 19:56:34 -0800 Subject: [PATCH 14/16] Test --- .../tests/statecallhierarchy_test.go | 116 +++++ .../state/callHierarchyAcrossProject.baseline | 442 ++++++++++++++++++ 2 files changed, 558 insertions(+) create mode 100644 internal/fourslash/tests/statecallhierarchy_test.go create mode 100644 testdata/baselines/reference/fourslash/state/callHierarchyAcrossProject.baseline diff --git a/internal/fourslash/tests/statecallhierarchy_test.go b/internal/fourslash/tests/statecallhierarchy_test.go new file mode 100644 index 0000000000..a98c6cfeca --- /dev/null +++ b/internal/fourslash/tests/statecallhierarchy_test.go @@ -0,0 +1,116 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/fourslash" + "github.com/microsoft/typescript-go/internal/testutil" +) + +func TestCallHierarchyAcrossProject(t *testing.T) { + t.Parallel() + + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = ` +// @stateBaseline: true +// @Filename: /projects/temp/temp.ts +/*temp*/let x = 10 +// @Filename: /projects/temp/tsconfig.json +{} +// @Filename: /projects/container/lib/tsconfig.json +{ + "compilerOptions": { + "composite": true, + }, + references: [], + files: [ + "index.ts", + "bar.ts", + "baz.ts" + ], +} +// @Filename: /projects/container/lib/index.ts +export function /*call*/createModelReference() {} +// @Filename: /projects/container/lib/bar.ts +import { createModelReference } from "./index"; +function openElementsAtEditor() { + createModelReference(); +} +// @Filename: /projects/container/lib/baz.ts +import { createModelReference } from "./index"; +function registerDefaultLanguageCommand() { + createModelReference(); +} +// @Filename: /projects/container/exec/tsconfig.json +{ + "files": ["./index.ts"], + "references": [ + { "path": "../lib" }, + ], +} +// @Filename: /projects/container/exec/index.ts +import { createModelReference } from "../lib"; +function openElementsAtEditor1() { + createModelReference(); +} +// @Filename: /projects/container/compositeExec/tsconfig.json +{ + "compilerOptions": { + "composite": true, + }, + "files": ["./index.ts"], + "references": [ + { "path": "../lib" }, + ], +} +// @Filename: /projects/container/compositeExec/index.ts +import { createModelReference } from "../lib"; +function openElementsAtEditor2() { + createModelReference(); +} +// @Filename: /projects/container/tsconfig.json +{ + "files": [], + "include": [], + "references": [ + { "path": "./exec" }, + { "path": "./compositeExec" }, + ], +} +// @Filename: /projects/container/tsconfig.json +{ + "files": [], + "include": [], + "references": [ + { "path": "./exec" }, + { "path": "./compositeExec" }, + ], +} +// @Filename: /projects/container/tsconfig.json +{ + "files": [], + "include": [], + "references": [ + { "path": "./exec" }, + { "path": "./compositeExec" }, + ], +}` + f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) + defer done() + f.GoToMarker(t, "call") + // Open temp file and verify all projects alive + f.GoToMarker(t, "temp") + + // Ref projects are loaded after as part of this command + f.GoToMarker(t, "call") + f.VerifyBaselineCallHierarchy(t) + + // Open temp file and verify all projects alive + f.CloseFileOfMarker(t, "temp") + f.GoToMarker(t, "temp") + + // Close all files and open temp file, only inferred project should be alive + f.CloseFileOfMarker(t, "call") + f.CloseFileOfMarker(t, "temp") + f.GoToMarker(t, "temp") +} diff --git a/testdata/baselines/reference/fourslash/state/callHierarchyAcrossProject.baseline b/testdata/baselines/reference/fourslash/state/callHierarchyAcrossProject.baseline new file mode 100644 index 0000000000..aba0064325 --- /dev/null +++ b/testdata/baselines/reference/fourslash/state/callHierarchyAcrossProject.baseline @@ -0,0 +1,442 @@ +UseCaseSensitiveFileNames: true +//// [/projects/container/compositeExec/index.ts] *new* +import { createModelReference } from "../lib"; +function openElementsAtEditor2() { + createModelReference(); +} +//// [/projects/container/compositeExec/tsconfig.json] *new* +{ + "compilerOptions": { + "composite": true, + }, + "files": ["./index.ts"], + "references": [ + { "path": "../lib" }, + ], +} +//// [/projects/container/exec/index.ts] *new* +import { createModelReference } from "../lib"; +function openElementsAtEditor1() { + createModelReference(); +} +//// [/projects/container/exec/tsconfig.json] *new* +{ + "files": ["./index.ts"], + "references": [ + { "path": "../lib" }, + ], +} +//// [/projects/container/lib/bar.ts] *new* +import { createModelReference } from "./index"; +function openElementsAtEditor() { + createModelReference(); +} +//// [/projects/container/lib/baz.ts] *new* +import { createModelReference } from "./index"; +function registerDefaultLanguageCommand() { + createModelReference(); +} +//// [/projects/container/lib/index.ts] *new* +export function createModelReference() {} +//// [/projects/container/lib/tsconfig.json] *new* +{ + "compilerOptions": { + "composite": true, + }, + references: [], + files: [ + "index.ts", + "bar.ts", + "baz.ts" + ], +} +//// [/projects/container/tsconfig.json] *new* +{ + "files": [], + "include": [], + "references": [ + { "path": "./exec" }, + { "path": "./compositeExec" }, + ], +} +//// [/projects/temp/temp.ts] *new* +let x = 10 +//// [/projects/temp/tsconfig.json] *new* +{} + + +{ + "method": "textDocument/didOpen", + "params": { + "textDocument": { + "uri": "file:///projects/container/lib/index.ts", + "languageId": "typescript", + "version": 0, + "text": "export function createModelReference() {}" + } + } +} + +Projects:: + [/projects/container/lib/tsconfig.json] *new* + /projects/container/lib/index.ts + /projects/container/lib/bar.ts + /projects/container/lib/baz.ts + [/projects/container/tsconfig.json] *new* +Open Files:: + [/projects/container/lib/index.ts] *new* + /projects/container/lib/tsconfig.json (default) +Config:: + [/projects/container/lib/tsconfig.json] *new* + RetainingProjects: + /projects/container/lib/tsconfig.json + RetainingOpenFiles: + /projects/container/lib/index.ts +Config File Names:: + [/projects/container/lib/index.ts] *new* + NearestConfigFileName: /projects/container/lib/tsconfig.json + Ancestors: + /projects/container/lib/tsconfig.json /projects/container/tsconfig.json + /projects/container/tsconfig.json + +{ + "method": "textDocument/didOpen", + "params": { + "textDocument": { + "uri": "file:///projects/temp/temp.ts", + "languageId": "typescript", + "version": 0, + "text": "let x = 10" + } + } +} + +Projects:: + [/projects/container/lib/tsconfig.json] + /projects/container/lib/index.ts + /projects/container/lib/bar.ts + /projects/container/lib/baz.ts + [/projects/container/tsconfig.json] + [/projects/temp/tsconfig.json] *new* + /projects/temp/temp.ts +Open Files:: + [/projects/container/lib/index.ts] + /projects/container/lib/tsconfig.json (default) + [/projects/temp/temp.ts] *new* + /projects/temp/tsconfig.json (default) +Config:: + [/projects/container/lib/tsconfig.json] + RetainingProjects: + /projects/container/lib/tsconfig.json + RetainingOpenFiles: + /projects/container/lib/index.ts + [/projects/temp/tsconfig.json] *new* + RetainingProjects: + /projects/temp/tsconfig.json + RetainingOpenFiles: + /projects/temp/temp.ts +Config File Names:: + [/projects/container/lib/index.ts] + NearestConfigFileName: /projects/container/lib/tsconfig.json + Ancestors: + /projects/container/lib/tsconfig.json /projects/container/tsconfig.json + /projects/container/tsconfig.json + [/projects/temp/temp.ts] *new* + NearestConfigFileName: /projects/temp/tsconfig.json + +{ + "method": "textDocument/prepareCallHierarchy", + "params": { + "textDocument": { + "uri": "file:///projects/container/lib/index.ts" + }, + "position": { + "line": 0, + "character": 16 + } + } +} + +{ + "method": "callHierarchy/incomingCalls", + "params": { + "item": { + "name": "createModelReference", + "kind": 12, + "uri": "file:///projects/container/lib/index.ts", + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 0, + "character": 41 + } + }, + "selectionRange": { + "start": { + "line": 0, + "character": 16 + }, + "end": { + "line": 0, + "character": 36 + } + } + } + } +} + +{ + "method": "callHierarchy/outgoingCalls", + "params": { + "item": { + "name": "createModelReference", + "kind": 12, + "uri": "file:///projects/container/lib/index.ts", + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 0, + "character": 41 + } + }, + "selectionRange": { + "start": { + "line": 0, + "character": 16 + }, + "end": { + "line": 0, + "character": 36 + } + } + } + } +} + +{ + "method": "callHierarchy/incomingCalls", + "params": { + "item": { + "name": "openElementsAtEditor", + "kind": 12, + "uri": "file:///projects/container/lib/bar.ts", + "range": { + "start": { + "line": 1, + "character": 0 + }, + "end": { + "line": 3, + "character": 1 + } + }, + "selectionRange": { + "start": { + "line": 1, + "character": 9 + }, + "end": { + "line": 1, + "character": 29 + } + } + } + } +} + +{ + "method": "callHierarchy/incomingCalls", + "params": { + "item": { + "name": "registerDefaultLanguageCommand", + "kind": 12, + "uri": "file:///projects/container/lib/baz.ts", + "range": { + "start": { + "line": 1, + "character": 0 + }, + "end": { + "line": 3, + "character": 1 + } + }, + "selectionRange": { + "start": { + "line": 1, + "character": 9 + }, + "end": { + "line": 1, + "character": 39 + } + } + } + } +} + + + + +// === Call Hierarchy === +╭ name: createModelReference +├ kind: function +├ file: /projects/container/lib/index.ts +├ span: +│ ╭ /projects/container/lib/index.ts:1:1-1:42 +│ │ 1: export function createModelReference() {} +│ │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +│ ╰ +├ selectionSpan: +│ ╭ /projects/container/lib/index.ts:1:17-1:37 +│ │ 1: export function createModelReference() {} +│ │ ^^^^^^^^^^^^^^^^^^^^ +│ ╰ +├ incoming: +│ ╭ from: +│ │ ╭ name: openElementsAtEditor +│ │ ├ kind: function +│ │ ├ file: /projects/container/lib/bar.ts +│ │ ├ span: +│ │ │ ╭ /projects/container/lib/bar.ts:2:1-4:2 +│ │ │ │ 2: function openElementsAtEditor() { +│ │ │ │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +│ │ │ │ 3: createModelReference(); +│ │ │ │ ^^^^^^^^^^^^^^^^^^^^^^^^^ +│ │ │ │ 4: } +│ │ │ │ ^ +│ │ │ ╰ +│ │ ├ selectionSpan: +│ │ │ ╭ /projects/container/lib/bar.ts:2:10-2:30 +│ │ │ │ 2: function openElementsAtEditor() { +│ │ │ │ ^^^^^^^^^^^^^^^^^^^^ +│ │ │ ╰ +│ │ ╰ incoming: none +│ ├ fromSpans: +│ │ ╭ /projects/container/lib/bar.ts:3:3-3:23 +│ │ │ 3: createModelReference(); +│ │ │ ^^^^^^^^^^^^^^^^^^^^ +│ ╰ ╰ +│ ╭ from: +│ │ ╭ name: registerDefaultLanguageCommand +│ │ ├ kind: function +│ │ ├ file: /projects/container/lib/baz.ts +│ │ ├ span: +│ │ │ ╭ /projects/container/lib/baz.ts:2:1-4:2 +│ │ │ │ 2: function registerDefaultLanguageCommand() { +│ │ │ │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +│ │ │ │ 3: createModelReference(); +│ │ │ │ ^^^^^^^^^^^^^^^^^^^^^^^^^ +│ │ │ │ 4: } +│ │ │ │ ^ +│ │ │ ╰ +│ │ ├ selectionSpan: +│ │ │ ╭ /projects/container/lib/baz.ts:2:10-2:40 +│ │ │ │ 2: function registerDefaultLanguageCommand() { +│ │ │ │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +│ │ │ ╰ +│ │ ╰ incoming: none +│ ├ fromSpans: +│ │ ╭ /projects/container/lib/baz.ts:3:3-3:23 +│ │ │ 3: createModelReference(); +│ │ │ ^^^^^^^^^^^^^^^^^^^^ +│ ╰ ╰ +╰ outgoing: none +{ + "method": "textDocument/didClose", + "params": { + "textDocument": { + "uri": "file:///projects/temp/temp.ts" + } + } +} + +Open Files:: + [/projects/container/lib/index.ts] + /projects/container/lib/tsconfig.json (default) + [/projects/temp/temp.ts] *closed* + +{ + "method": "textDocument/didOpen", + "params": { + "textDocument": { + "uri": "file:///projects/temp/temp.ts", + "languageId": "typescript", + "version": 0, + "text": "let x = 10" + } + } +} + +Open Files:: + [/projects/container/lib/index.ts] + /projects/container/lib/tsconfig.json (default) + [/projects/temp/temp.ts] *new* + /projects/temp/tsconfig.json (default) + +{ + "method": "textDocument/didClose", + "params": { + "textDocument": { + "uri": "file:///projects/container/lib/index.ts" + } + } +} + +Open Files:: + [/projects/container/lib/index.ts] *closed* + [/projects/temp/temp.ts] + /projects/temp/tsconfig.json (default) + +{ + "method": "textDocument/didClose", + "params": { + "textDocument": { + "uri": "file:///projects/temp/temp.ts" + } + } +} + +Open Files:: + [/projects/temp/temp.ts] *closed* + +{ + "method": "textDocument/didOpen", + "params": { + "textDocument": { + "uri": "file:///projects/temp/temp.ts", + "languageId": "typescript", + "version": 0, + "text": "let x = 10" + } + } +} + +Projects:: + [/projects/container/lib/tsconfig.json] *deleted* + /projects/container/lib/index.ts + /projects/container/lib/bar.ts + /projects/container/lib/baz.ts + [/projects/container/tsconfig.json] *deleted* + [/projects/temp/tsconfig.json] + /projects/temp/temp.ts +Open Files:: + [/projects/temp/temp.ts] *new* + /projects/temp/tsconfig.json (default) +Config:: + [/projects/container/lib/tsconfig.json] *deleted* + [/projects/temp/tsconfig.json] + RetainingProjects: + /projects/temp/tsconfig.json + RetainingOpenFiles: + /projects/temp/temp.ts +Config File Names:: + [/projects/container/lib/index.ts] *deleted* + [/projects/temp/temp.ts] + NearestConfigFileName: /projects/temp/tsconfig.json From ad493513370a6a0ea17c8a46447e270002733d78 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Thu, 4 Dec 2025 20:33:17 -0800 Subject: [PATCH 15/16] Multi project incoming calls --- internal/ls/callhierarchy.go | 80 ++++++-- internal/ls/crossproject.go | 15 ++ internal/ls/documenthighlights.go | 3 +- internal/ls/findallreferences.go | 12 +- internal/lsp/server.go | 6 +- .../state/callHierarchyAcrossProject.baseline | 174 +++++++++++++++++- 6 files changed, 262 insertions(+), 28 deletions(-) diff --git a/internal/ls/callhierarchy.go b/internal/ls/callhierarchy.go index 766d0eb64f..d467b737af 100644 --- a/internal/ls/callhierarchy.go +++ b/internal/ls/callhierarchy.go @@ -4,6 +4,7 @@ import ( "context" "slices" "strings" + "sync" "github.com/microsoft/typescript-go/internal/ast" "github.com/microsoft/typescript-go/internal/astnav" @@ -567,24 +568,78 @@ func (l *LanguageService) convertCallSiteGroupToIncomingCall(program *compiler.P } } +type incomingEntry struct { + ls *LanguageService + node *ast.Node + + sourceFileOnce sync.Once + sourceFile *ast.SourceFile + + documentUriOnce sync.Once + documentUri lsproto.DocumentUri + + positionOnce sync.Once + position lsproto.Position +} + +var _ lsproto.HasTextDocumentPosition = (*incomingEntry)(nil) + +func (d *incomingEntry) getSourceFile() *ast.SourceFile { + d.sourceFileOnce.Do(func() { + d.sourceFile = ast.GetSourceFileOfNode(d.node) + }) + return d.sourceFile +} + +func (d *incomingEntry) TextDocumentURI() lsproto.DocumentUri { + d.documentUriOnce.Do(func() { + d.documentUri = lsconv.FileNameToDocumentURI(d.getSourceFile().FileName()) + }) + return d.documentUri +} + +func (d *incomingEntry) TextDocumentPosition() lsproto.Position { + d.positionOnce.Do(func() { + start := scanner.GetTokenPosOfNode(d.node, d.getSourceFile(), false /*includeJsDoc*/) + d.position = d.ls.createLspPosition(start, d.getSourceFile()) + }) + return d.position +} + // Gets the call sites that call into the provided call hierarchy declaration. -func (l *LanguageService) getIncomingCalls(ctx context.Context, program *compiler.Program, declaration *ast.Node) []*lsproto.CallHierarchyIncomingCall { +func (l *LanguageService) getIncomingCalls(ctx context.Context, program *compiler.Program, declaration *ast.Node, orchestrator CrossProjectOrchestrator) (lsproto.CallHierarchyIncomingCallsResponse, error) { // Source files and modules have no incoming calls. if ast.IsSourceFile(declaration) || ast.IsModuleDeclaration(declaration) || ast.IsClassStaticBlockDeclaration(declaration) { - return nil + return lsproto.CallHierarchyIncomingCallsOrNull{}, nil } location := getCallHierarchyDeclarationReferenceNode(declaration) if location == nil { - return nil + return lsproto.CallHierarchyIncomingCallsOrNull{}, nil } - sourceFiles := program.GetSourceFiles() - options := refOptions{use: referenceUseReferences} - symbolsAndEntries := l.getReferencedSymbolsForNode(ctx, 0, location, program, sourceFiles, options, nil) + incomingEntry := &incomingEntry{ + ls: l, + node: location, + } + + return handleCrossProject( + l, + ctx, + incomingEntry, + orchestrator, + (*LanguageService).symbolAndEntriesToIncomingCalls, + combineIncomingCalls, + false, + false, + symbolEntryTransformOptions{}, + ) +} +func (l *LanguageService) symbolAndEntriesToIncomingCalls(ctx context.Context, params *incomingEntry, data SymbolAndEntriesData, options symbolEntryTransformOptions) (lsproto.CallHierarchyIncomingCallsResponse, error) { + program := l.GetProgram() var refEntries []*ReferenceEntry - for _, symbolAndEntry := range symbolsAndEntries { + for _, symbolAndEntry := range data.SymbolsAndEntries { refEntries = append(refEntries, symbolAndEntry.references...) } @@ -596,7 +651,7 @@ func (l *LanguageService) getIncomingCalls(ctx context.Context, program *compile } if len(callSites) == 0 { - return nil + return lsproto.CallHierarchyIncomingCallsOrNull{}, nil } grouped := make(map[ast.NodeId][]*callSite) @@ -620,7 +675,7 @@ func (l *LanguageService) getIncomingCalls(ctx context.Context, program *compile return lsproto.CompareRanges(&a.FromRanges[0], &b.FromRanges[0]) }) - return result + return lsproto.CallHierarchyIncomingCallsOrNull{CallHierarchyIncomingCalls: &result}, nil } type callSiteCollector struct { @@ -947,6 +1002,7 @@ func (l *LanguageService) ProvidePrepareCallHierarchy( func (l *LanguageService) ProvideCallHierarchyIncomingCalls( ctx context.Context, item *lsproto.CallHierarchyItem, + orchestrator CrossProjectOrchestrator, ) (lsproto.CallHierarchyIncomingCallsResponse, error) { program := l.GetProgram() fileName := item.Uri.FileName() @@ -986,11 +1042,7 @@ func (l *LanguageService) ProvideCallHierarchyIncomingCalls( return lsproto.CallHierarchyIncomingCallsOrNull{}, nil } - calls := l.getIncomingCalls(ctx, program, decl) - if calls == nil { - return lsproto.CallHierarchyIncomingCallsOrNull{}, nil - } - return lsproto.CallHierarchyIncomingCallsOrNull{CallHierarchyIncomingCalls: &calls}, nil + return l.getIncomingCalls(ctx, program, decl, orchestrator) } func (l *LanguageService) ProvideCallHierarchyOutgoingCalls( diff --git a/internal/ls/crossproject.go b/internal/ls/crossproject.go index b46021d015..009787f839 100644 --- a/internal/ls/crossproject.go +++ b/internal/ls/crossproject.go @@ -345,3 +345,18 @@ func combineRenameResponse(results iter.Seq[lsproto.RenameResponse]) lsproto.Ren } return lsproto.RenameResponse{} } + +func combineIncomingCalls(results iter.Seq[lsproto.CallHierarchyIncomingCallsResponse]) lsproto.CallHierarchyIncomingCallsResponse { + var combined []*lsproto.CallHierarchyIncomingCall + var seenCalls collections.Set[lsproto.Location] + for resp := range results { + if resp.CallHierarchyIncomingCalls != nil { + for _, call := range *resp.CallHierarchyIncomingCalls { + if seenCalls.AddIfAbsent(call.From.GetLocation()) { + combined = append(combined, call) + } + } + } + } + return lsproto.CallHierarchyIncomingCallsResponse{CallHierarchyIncomingCalls: &combined} +} diff --git a/internal/ls/documenthighlights.go b/internal/ls/documenthighlights.go index 63dd7376fb..969d5e1246 100644 --- a/internal/ls/documenthighlights.go +++ b/internal/ls/documenthighlights.go @@ -5,7 +5,6 @@ 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/lsutil" "github.com/microsoft/typescript-go/internal/scanner" @@ -52,7 +51,7 @@ func (l *LanguageService) ProvideDocumentHighlights(ctx context.Context, documen 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, &collections.Set[string]{}) + referenceEntries := l.getReferencedSymbolsForNode(ctx, position, node, program, []*ast.SourceFile{sourceFile}, options) if referenceEntries == nil { return nil } diff --git a/internal/ls/findallreferences.go b/internal/ls/findallreferences.go index 64c27ebd62..d6758c69f2 100644 --- a/internal/ls/findallreferences.go +++ b/internal/ls/findallreferences.go @@ -648,7 +648,7 @@ func (l *LanguageService) getSymbolAndEntries( options.use = referenceUseRename options.useAliasesForRename = true } - return l.getReferencedSymbolsForNode(ctx, position, node, program, program.GetSourceFiles(), options, nil) + return l.getReferencedSymbolsForNode(ctx, position, node, program, program.GetSourceFiles(), options) } func (l *LanguageService) ProvideReferences(ctx context.Context, params *lsproto.ReferenceParams, orchestrator CrossProjectOrchestrator) (lsproto.ReferencesResponse, error) { @@ -928,13 +928,11 @@ func (l *LanguageService) mergeReferences(program *compiler.Program, referencesT // === functions for find all ref implementation === -func (l *LanguageService) getReferencedSymbolsForNode(ctx context.Context, position int, node *ast.Node, program *compiler.Program, sourceFiles []*ast.SourceFile, options refOptions, sourceFilesSet *collections.Set[string]) []*SymbolAndEntries { +func (l *LanguageService) getReferencedSymbolsForNode(ctx context.Context, position int, node *ast.Node, program *compiler.Program, sourceFiles []*ast.SourceFile, options refOptions) []*SymbolAndEntries { // !!! cancellationToken - if sourceFilesSet == nil || sourceFilesSet.Len() == 0 { - sourceFilesSet = collections.NewSetWithSizeHint[string](len(sourceFiles)) - for _, file := range sourceFiles { - sourceFilesSet.Add(file.FileName()) - } + sourceFilesSet := collections.NewSetWithSizeHint[string](len(sourceFiles)) + for _, file := range sourceFiles { + sourceFilesSet.Add(file.FileName()) } if options.use == referenceUseReferences || options.use == referenceUseRename { diff --git a/internal/lsp/server.go b/internal/lsp/server.go index afe35a66ea..23b224237d 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -1135,13 +1135,13 @@ func (s *Server) handlePrepareCallHierarchy( func (s *Server) handleCallHierarchyIncomingCalls( ctx context.Context, params *lsproto.CallHierarchyIncomingCallsParams, - _ *lsproto.RequestMessage, + reqMsg *lsproto.RequestMessage, ) (lsproto.CallHierarchyIncomingCallsResponse, error) { - languageService, err := s.session.GetLanguageService(ctx, params.Item.Uri) + defaultProject, defaultLs, allProjects, err := s.session.GetLanguageServiceAndProjectsForFile(ctx, params.Item.Uri) if err != nil { return lsproto.CallHierarchyIncomingCallsOrNull{}, err } - return languageService.ProvideCallHierarchyIncomingCalls(ctx, params.Item) + return defaultLs.ProvideCallHierarchyIncomingCalls(ctx, params.Item, &crossProjectOrchestrator{s, reqMsg, defaultProject, allProjects}) } func (s *Server) handleCallHierarchyOutgoingCalls( diff --git a/testdata/baselines/reference/fourslash/state/callHierarchyAcrossProject.baseline b/testdata/baselines/reference/fourslash/state/callHierarchyAcrossProject.baseline index aba0064325..3504013755 100644 --- a/testdata/baselines/reference/fourslash/state/callHierarchyAcrossProject.baseline +++ b/testdata/baselines/reference/fourslash/state/callHierarchyAcrossProject.baseline @@ -188,6 +188,53 @@ Config File Names:: } } +Projects:: + [/projects/container/compositeExec/tsconfig.json] *new* + /projects/container/lib/index.ts + /projects/container/compositeExec/index.ts + [/projects/container/exec/tsconfig.json] *new* + /projects/container/lib/index.ts + /projects/container/exec/index.ts + [/projects/container/lib/tsconfig.json] + /projects/container/lib/index.ts + /projects/container/lib/bar.ts + /projects/container/lib/baz.ts + [/projects/container/tsconfig.json] *modified* + [/projects/temp/tsconfig.json] + /projects/temp/temp.ts +Open Files:: + [/projects/container/lib/index.ts] *modified* + /projects/container/compositeExec/tsconfig.json *new* + /projects/container/exec/tsconfig.json *new* + /projects/container/lib/tsconfig.json (default) + [/projects/temp/temp.ts] + /projects/temp/tsconfig.json (default) +Config:: + [/projects/container/compositeExec/tsconfig.json] *new* + RetainingProjects: + /projects/container/compositeExec/tsconfig.json + /projects/container/tsconfig.json + [/projects/container/exec/tsconfig.json] *new* + RetainingProjects: + /projects/container/exec/tsconfig.json + /projects/container/tsconfig.json + [/projects/container/lib/tsconfig.json] *modified* + RetainingProjects: *modified* + /projects/container/compositeExec/tsconfig.json *new* + /projects/container/exec/tsconfig.json *new* + /projects/container/lib/tsconfig.json + /projects/container/tsconfig.json *new* + RetainingOpenFiles: + /projects/container/lib/index.ts + [/projects/container/tsconfig.json] *new* + RetainingProjects: + /projects/container/tsconfig.json + [/projects/temp/tsconfig.json] + RetainingProjects: + /projects/temp/tsconfig.json + RetainingOpenFiles: + /projects/temp/temp.ts + { "method": "callHierarchy/outgoingCalls", "params": { @@ -281,6 +328,68 @@ Config File Names:: } } +{ + "method": "callHierarchy/incomingCalls", + "params": { + "item": { + "name": "openElementsAtEditor2", + "kind": 12, + "uri": "file:///projects/container/compositeExec/index.ts", + "range": { + "start": { + "line": 1, + "character": 0 + }, + "end": { + "line": 3, + "character": 1 + } + }, + "selectionRange": { + "start": { + "line": 1, + "character": 9 + }, + "end": { + "line": 1, + "character": 30 + } + } + } + } +} + +{ + "method": "callHierarchy/incomingCalls", + "params": { + "item": { + "name": "openElementsAtEditor1", + "kind": 12, + "uri": "file:///projects/container/exec/index.ts", + "range": { + "start": { + "line": 1, + "character": 0 + }, + "end": { + "line": 3, + "character": 1 + } + }, + "selectionRange": { + "start": { + "line": 1, + "character": 9 + }, + "end": { + "line": 1, + "character": 30 + } + } + } + } +} + @@ -347,6 +456,54 @@ Config File Names:: │ │ │ 3: createModelReference(); │ │ │ ^^^^^^^^^^^^^^^^^^^^ │ ╰ ╰ +│ ╭ from: +│ │ ╭ name: openElementsAtEditor2 +│ │ ├ kind: function +│ │ ├ file: /projects/container/compositeExec/index.ts +│ │ ├ span: +│ │ │ ╭ /projects/container/compositeExec/index.ts:2:1-4:2 +│ │ │ │ 2: function openElementsAtEditor2() { +│ │ │ │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +│ │ │ │ 3: createModelReference(); +│ │ │ │ ^^^^^^^^^^^^^^^^^^^^^^^^^ +│ │ │ │ 4: } +│ │ │ │ ^ +│ │ │ ╰ +│ │ ├ selectionSpan: +│ │ │ ╭ /projects/container/compositeExec/index.ts:2:10-2:31 +│ │ │ │ 2: function openElementsAtEditor2() { +│ │ │ │ ^^^^^^^^^^^^^^^^^^^^^ +│ │ │ ╰ +│ │ ╰ incoming: none +│ ├ fromSpans: +│ │ ╭ /projects/container/compositeExec/index.ts:3:3-3:23 +│ │ │ 3: createModelReference(); +│ │ │ ^^^^^^^^^^^^^^^^^^^^ +│ ╰ ╰ +│ ╭ from: +│ │ ╭ name: openElementsAtEditor1 +│ │ ├ kind: function +│ │ ├ file: /projects/container/exec/index.ts +│ │ ├ span: +│ │ │ ╭ /projects/container/exec/index.ts:2:1-4:2 +│ │ │ │ 2: function openElementsAtEditor1() { +│ │ │ │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +│ │ │ │ 3: createModelReference(); +│ │ │ │ ^^^^^^^^^^^^^^^^^^^^^^^^^ +│ │ │ │ 4: } +│ │ │ │ ^ +│ │ │ ╰ +│ │ ├ selectionSpan: +│ │ │ ╭ /projects/container/exec/index.ts:2:10-2:31 +│ │ │ │ 2: function openElementsAtEditor1() { +│ │ │ │ ^^^^^^^^^^^^^^^^^^^^^ +│ │ │ ╰ +│ │ ╰ incoming: none +│ ├ fromSpans: +│ │ ╭ /projects/container/exec/index.ts:3:3-3:23 +│ │ │ 3: createModelReference(); +│ │ │ ^^^^^^^^^^^^^^^^^^^^ +│ ╰ ╰ ╰ outgoing: none { "method": "textDocument/didClose", @@ -359,7 +516,9 @@ Config File Names:: Open Files:: [/projects/container/lib/index.ts] - /projects/container/lib/tsconfig.json (default) + /projects/container/compositeExec/tsconfig.json + /projects/container/exec/tsconfig.json + /projects/container/lib/tsconfig.json (default) [/projects/temp/temp.ts] *closed* { @@ -376,7 +535,9 @@ Open Files:: Open Files:: [/projects/container/lib/index.ts] - /projects/container/lib/tsconfig.json (default) + /projects/container/compositeExec/tsconfig.json + /projects/container/exec/tsconfig.json + /projects/container/lib/tsconfig.json (default) [/projects/temp/temp.ts] *new* /projects/temp/tsconfig.json (default) @@ -419,6 +580,12 @@ Open Files:: } Projects:: + [/projects/container/compositeExec/tsconfig.json] *deleted* + /projects/container/lib/index.ts + /projects/container/compositeExec/index.ts + [/projects/container/exec/tsconfig.json] *deleted* + /projects/container/lib/index.ts + /projects/container/exec/index.ts [/projects/container/lib/tsconfig.json] *deleted* /projects/container/lib/index.ts /projects/container/lib/bar.ts @@ -430,7 +597,10 @@ Open Files:: [/projects/temp/temp.ts] *new* /projects/temp/tsconfig.json (default) Config:: + [/projects/container/compositeExec/tsconfig.json] *deleted* + [/projects/container/exec/tsconfig.json] *deleted* [/projects/container/lib/tsconfig.json] *deleted* + [/projects/container/tsconfig.json] *deleted* [/projects/temp/tsconfig.json] RetainingProjects: /projects/temp/tsconfig.json From 61388d6cc8bb3da84839efafbfe9fb8c715dcb5b Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Fri, 5 Dec 2025 17:00:28 -0800 Subject: [PATCH 16/16] Fix recover --- internal/ls/crossproject.go | 8 ++++++-- internal/lsp/server.go | 22 +++++++++++++--------- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/internal/ls/crossproject.go b/internal/ls/crossproject.go index 009787f839..dfc93de72a 100644 --- a/internal/ls/crossproject.go +++ b/internal/ls/crossproject.go @@ -38,7 +38,7 @@ type CrossProjectOrchestrator interface { GetLanguageServiceForProjectWithFile(ctx context.Context, project Project, uri lsproto.DocumentUri) *LanguageService GetProjectsForFile(ctx context.Context, uri lsproto.DocumentUri) ([]Project, error) GetProjectsLoadingProjectTree(ctx context.Context, requestedProjectTrees *collections.Set[tspath.Path]) iter.Seq[Project] - Recover() + RecoverWith(r any) } func handleCrossProject[Req lsproto.HasTextDocumentPosition, Resp any]( @@ -81,7 +81,11 @@ func handleCrossProject[Req lsproto.HasTextDocumentPosition, Resp any]( if ctx.Err() != nil { return } - defer orchestrator.Recover() + defer func() { + if r := recover(); r != nil { + orchestrator.RecoverWith(r) + } + }() // Process the item ls := item.ls if ls == nil { diff --git a/internal/lsp/server.go b/internal/lsp/server.go index 23b224237d..8ee454a4ec 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -695,19 +695,23 @@ func (c *crossProjectOrchestrator) GetProjectsLoadingProjectTree(ctx context.Con } } -func (c *crossProjectOrchestrator) Recover() { - c.server.recover(c.req) +func (c *crossProjectOrchestrator) RecoverWith(r any) { + c.server.recoverWith(c.req, r) +} + +func (s *Server) recoverWith(req *lsproto.RequestMessage, r any) { + stack := debug.Stack() + s.Log("panic handling request", req.Method, r, string(stack)) + if req.ID != nil { + s.sendError(req.ID, fmt.Errorf("%w: panic handling request %s: %v", lsproto.ErrorCodeInternalError, req.Method, r)) + } else { + s.Log("unhandled panic in notification", req.Method, r) + } } func (s *Server) recover(req *lsproto.RequestMessage) { if r := recover(); r != nil { - stack := debug.Stack() - s.Log("panic handling request", req.Method, r, string(stack)) - if req.ID != nil { - s.sendError(req.ID, fmt.Errorf("%w: panic handling request %s: %v", lsproto.ErrorCodeInternalError, req.Method, r)) - } else { - s.Log("unhandled panic in notification", req.Method, r) - } + s.recoverWith(req, r) } }