From e37b9832bc1d17430c1e16d6e7b7d06762689202 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 4 Dec 2025 20:12:21 +0000 Subject: [PATCH 1/5] Initial plan From 17ac60f425cbe44c1c4680122e98af15eca84445 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 4 Dec 2025 20:24:59 +0000 Subject: [PATCH 2/5] Add test demonstrating CodeLens cross-project reference issue Co-authored-by: DanielRosenwasser <972891+DanielRosenwasser@users.noreply.github.com> --- .../tests/codeLensAcrossProjects_test.go | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 internal/fourslash/tests/codeLensAcrossProjects_test.go diff --git a/internal/fourslash/tests/codeLensAcrossProjects_test.go b/internal/fourslash/tests/codeLensAcrossProjects_test.go new file mode 100644 index 0000000000..e8d5a4fccc --- /dev/null +++ b/internal/fourslash/tests/codeLensAcrossProjects_test.go @@ -0,0 +1,56 @@ +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 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, + }, + }) +} From 8eb94e92058480665d6467993bd8f775cdc7b820 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 4 Dec 2025 20:31:59 +0000 Subject: [PATCH 3/5] Implement multi-project search for CodeLens references The handleCodeLensResolve now uses multi-project search mechanism similar to handleReferences to find all references across project references, not just within the current project. Co-authored-by: DanielRosenwasser <972891+DanielRosenwasser@users.noreply.github.com> --- internal/lsp/server.go | 254 +++++++++++++++++- ...nsOnFunctionAcrossProjects1.baseline.jsonc | 10 + 2 files changed, 263 insertions(+), 1 deletion(-) create mode 100644 testdata/baselines/reference/fourslash/codeLenses/codeLensOnFunctionAcrossProjects1.baseline.jsonc diff --git a/internal/lsp/server.go b/internal/lsp/server.go index 87d031fb11..c43c964731 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -16,6 +16,7 @@ import ( "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" @@ -1346,15 +1347,266 @@ 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) { + defer s.recover(reqMsg) + + // For references code lens, use multi-project search to find all references across projects + if codeLens.Data.Kind == lsproto.CodeLensKindReferences { + return s.resolveReferencesCodeLensAcrossProjects(ctx, codeLens) + } + + // For other code lens kinds (like implementations), use the single-project resolution ls, err := s.session.GetLanguageService(ctx, codeLens.Data.Uri) if err != nil { return nil, err } - defer s.recover(reqMsg) return ls.ResolveCodeLens(ctx, codeLens, s.initializeParams.InitializationOptions.CodeLensShowLocationsCommandName) } +func (s *Server) resolveReferencesCodeLensAcrossProjects(ctx context.Context, codeLens *lsproto.CodeLens) (*lsproto.CodeLens, error) { + // Use multi-project search similar to handleReferences + uri := codeLens.Data.Uri + position := codeLens.Range.Start + + defaultProject, defaultLs, allProjects, err := s.session.GetLanguageServiceAndProjectsForFile(ctx, uri) + if err != nil { + return nil, err + } + + // Collect references from all relevant projects + var results collections.SyncMap[tspath.Path, *response[lsproto.ReferencesResponse]] + 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[lsproto.ReferencesResponse] + if _, loaded := results.LoadOrStore(item.project.Id(), &response); loaded { + return + } + wg.Queue(func() { + if ctx.Err() != nil { + return + } + // 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 + } + } + originalNode, symbolsAndEntries, ok := ls.ProvideSymbolsAndEntries(ctx, item.Uri, item.Position, false /*isRename*/) + if ok { + for _, entry := range symbolsAndEntries { + // Find the default definition that can be in another project + 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 references, errSearch := ls.ProvideReferencesFromSymbolAndEntries( + ctx, + &lsproto.ReferenceParams{ + TextDocument: lsproto.TextDocumentIdentifier{Uri: item.Uri}, + Position: item.Position, + Context: &lsproto.ReferenceContext{ + IncludeDeclaration: false, // Don't include the declaration in the references count + }, + }, + originalNode, + symbolsAndEntries, + ); errSearch == nil { + response.complete = true + response.result = references + 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: uri, + Position: position, + }) + for _, proj := range allProjects { + if proj != defaultProject { + enqueueItem(projectAndTextDocumentPosition{ + project: proj, + Uri: uri, + Position: position, + }) + } + } + + // Process existing known projects first + for { + wg.RunAndWait() + if ctx.Err() != nil { + return nil, ctx.Err() + } + if err != nil { + return nil, err + } + + wg = core.NewWorkGroup(false) + hasMoreWork := false + if defaultDefinition != nil { + requestedProjectTrees := make(map[tspath.Path]struct{}) + results.Range(func(key tspath.Path, response *response[lsproto.ReferencesResponse]) 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 nil, 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 + } + } + + // Combine all references from all projects + var combined []lsproto.Location + var seenLocations collections.Set[lsproto.Location] + var seenProjects collections.Set[tspath.Path] + + // Add default project results first + if response, loaded := results.Load(defaultProject.Id()); loaded && response.complete { + if response.result.Locations != nil { + for _, loc := range *response.result.Locations { + if !seenLocations.Has(loc) { + seenLocations.Add(loc) + combined = append(combined, loc) + } + } + } + } + seenProjects.Add(defaultProject.Id()) + + // Add other project results + for _, proj := range allProjects { + if seenProjects.AddIfAbsent(proj.Id()) { + if response, loaded := results.Load(proj.Id()); loaded && response.complete { + if response.result.Locations != nil { + for _, loc := range *response.result.Locations { + if !seenLocations.Has(loc) { + seenLocations.Add(loc) + combined = append(combined, loc) + } + } + } + } + } + } + + // Add remaining project results + results.Range(func(key tspath.Path, response *response[lsproto.ReferencesResponse]) bool { + if seenProjects.AddIfAbsent(key) && response.complete { + if response.result.Locations != nil { + for _, loc := range *response.result.Locations { + if !seenLocations.Has(loc) { + seenLocations.Add(loc) + combined = append(combined, loc) + } + } + } + } + return true + }) + + // Build the code lens with the combined references count + locale := locale.FromContext(ctx) + var lensTitle string + if len(combined) == 1 { + lensTitle = diagnostics.X_1_reference.Localize(locale) + } else { + lensTitle = diagnostics.X_0_references.Localize(locale, len(combined)) + } + + cmd := &lsproto.Command{ + Title: lensTitle, + } + if len(combined) > 0 && s.initializeParams.InitializationOptions.CodeLensShowLocationsCommandName != nil { + cmd.Command = *s.initializeParams.InitializationOptions.CodeLensShowLocationsCommandName + cmd.Arguments = &[]any{ + uri, + position, + combined, + } + } + + codeLens.Command = cmd + return codeLens, nil +} + func (s *Server) handlePrepareCallHierarchy( ctx context.Context, languageService *ls.LanguageService, 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..0fcab7f983 --- /dev/null +++ b/testdata/baselines/reference/fourslash/codeLenses/codeLensOnFunctionAcrossProjects1.baseline.jsonc @@ -0,0 +1,10 @@ +// === Code Lenses === +// === /a/src/foo.ts === +// 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 From 3078063416591d219999106d3b3bbeea0daa5dba Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 4 Dec 2025 20:35:22 +0000 Subject: [PATCH 4/5] Address code review feedback - Fix error assignment logic to only assign first error - Move defer s.recover() to only apply to single-project resolution path Co-authored-by: DanielRosenwasser <972891+DanielRosenwasser@users.noreply.github.com> --- internal/lsp/server.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/internal/lsp/server.go b/internal/lsp/server.go index c43c964731..82e167cbda 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -1347,14 +1347,13 @@ 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) { - defer s.recover(reqMsg) - // For references code lens, use multi-project search to find all references across projects if codeLens.Data.Kind == lsproto.CodeLensKindReferences { return s.resolveReferencesCodeLensAcrossProjects(ctx, codeLens) } // For other code lens kinds (like implementations), use the single-project resolution + defer s.recover(reqMsg) ls, err := s.session.GetLanguageService(ctx, codeLens.Data.Uri) if err != nil { return nil, err @@ -1447,7 +1446,7 @@ func (s *Server) resolveReferencesCodeLensAcrossProjects(ctx context.Context, co } else { errMu.Lock() defer errMu.Unlock() - if err != nil { + if err == nil { err = errSearch } } From 4beca99a898b9f803a6232e1124b16c4751fa014 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 5 Dec 2025 01:27:06 +0000 Subject: [PATCH 5/5] Refactor multi-project search into reusable helper Extract common multi-project search logic from registerMultiProjectReferenceRequestHandler into a new multiProjectSearchHelper function that can be used by handleReferences, handleRename, and handleCodeLensResolve. This eliminates code duplication and makes the multi-project search logic more maintainable. Co-authored-by: DanielRosenwasser <972891+DanielRosenwasser@users.noreply.github.com> --- internal/lsp/server.go | 637 ++++++++++++++++------------------------- 1 file changed, 245 insertions(+), 392 deletions(-) diff --git a/internal/lsp/server.go b/internal/lsp/server.go index 82e167cbda..9d1dcb2b56 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -653,222 +653,257 @@ type response[Resp any] struct { forOriginalLocation bool } -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), - 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()) - if err != nil { - return err - } - defer s.recover(req) +// multiProjectSearchHelper performs a multi-project search for symbols at a given position. +// It returns an iterator that yields results from all relevant projects. +func multiProjectSearchHelper[Resp any]( + s *Server, + ctx context.Context, + uri lsproto.DocumentUri, + position lsproto.Position, + isRename bool, + processFn func(context.Context, *ls.LanguageService, *ast.Node, []*ls.SymbolAndEntries) (Resp, error), +) (iter.Seq[Resp], error) { + defaultProject, defaultLs, allProjects, err := s.session.GetLanguageServiceAndProjectsForFile(ctx, uri) + if err != nil { + return nil, err + } - 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 { + // 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 } - 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 + } + originalNode, symbolsAndEntries, ok := ls.ProvideSymbolsAndEntries(ctx, item.Uri, item.Position, isRename) + 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) - 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 := processFn(ctx, ls, 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: uri, + Position: position, + }) + for _, project := range allProjects { + if project != defaultProject { + enqueueItem(projectAndTextDocumentPosition{ + project: project, + // TODO!! symlinks need to change the URI + Uri: uri, + Position: position, }) } + } + + // 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 nil, ctx.Err() + } + // No need to use mu here since we are not in parallel at this point + if err != nil { + return nil, err + } - // 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(), - }) + 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 nil, 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 + } + } - 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 - } - - 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 - }) + return getResultsIterator(), nil +} - // 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() - } +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), + 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) + } + defer s.recover(req) - // Can loop forever without this (enqueue here, dequeue above, repeat) - if !canSearchProject(loadedProject) || loadedProject.GetProgram() == nil { - continue - } + isRename := info.Method == lsproto.MethodTextDocumentRename + resultsIter, err := multiProjectSearchHelper( + s, + ctx, + params.TextDocumentURI(), + params.TextDocumentPosition(), + isRename, + func(ctx context.Context, ls *ls.LanguageService, originalNode *ast.Node, symbolsAndEntries []*ls.SymbolAndEntries) (Resp, error) { + return fn(s, ctx, ls, params, originalNode, symbolsAndEntries) + }, + ) + if err != nil { + return err + } - // 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 - } + // Collect results to determine if we need to combine + var results []Resp + for result := range resultsIter { + results = append(results, result) } var resp Resp - if results.Size() > 1 { - resp = combineResults(getResultsIterator()) - } else { - // Single result, return that directly - for value := range getResultsIterator() { - resp = value - break - } + if len(results) > 1 { + resp = combineResults(func(yield func(Resp) bool) { + for _, r := range results { + if !yield(r) { + return + } + } + }) + } else if len(results) == 1 { + resp = results[0] } s.sendResult(req.ID, resp) @@ -1363,242 +1398,60 @@ func (s *Server) handleCodeLensResolve(ctx context.Context, codeLens *lsproto.Co } func (s *Server) resolveReferencesCodeLensAcrossProjects(ctx context.Context, codeLens *lsproto.CodeLens) (*lsproto.CodeLens, error) { - // Use multi-project search similar to handleReferences uri := codeLens.Data.Uri position := codeLens.Range.Start - defaultProject, defaultLs, allProjects, err := s.session.GetLanguageServiceAndProjectsForFile(ctx, uri) - if err != nil { - return nil, err - } - - // Collect references from all relevant projects - var results collections.SyncMap[tspath.Path, *response[lsproto.ReferencesResponse]] - 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[lsproto.ReferencesResponse] - if _, loaded := results.LoadOrStore(item.project.Id(), &response); loaded { - return - } - wg.Queue(func() { - if ctx.Err() != nil { - return - } - // 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 - } - } - originalNode, symbolsAndEntries, ok := ls.ProvideSymbolsAndEntries(ctx, item.Uri, item.Position, false /*isRename*/) - if ok { - for _, entry := range symbolsAndEntries { - // Find the default definition that can be in another project - 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 references, errSearch := ls.ProvideReferencesFromSymbolAndEntries( + // Use the common multi-project search helper + resultsIter, err := multiProjectSearchHelper( + s, + ctx, + uri, + position, + false, // isRename + func(ctx context.Context, ls *ls.LanguageService, originalNode *ast.Node, symbolsAndEntries []*ls.SymbolAndEntries) (lsproto.ReferencesResponse, error) { + return ls.ProvideReferencesFromSymbolAndEntries( ctx, &lsproto.ReferenceParams{ - TextDocument: lsproto.TextDocumentIdentifier{Uri: item.Uri}, - Position: item.Position, + TextDocument: lsproto.TextDocumentIdentifier{Uri: uri}, + Position: position, Context: &lsproto.ReferenceContext{ IncludeDeclaration: false, // Don't include the declaration in the references count }, }, originalNode, symbolsAndEntries, - ); errSearch == nil { - response.complete = true - response.result = references - 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: uri, - Position: position, - }) - for _, proj := range allProjects { - if proj != defaultProject { - enqueueItem(projectAndTextDocumentPosition{ - project: proj, - Uri: uri, - Position: position, - }) - } - } - - // Process existing known projects first - for { - wg.RunAndWait() - if ctx.Err() != nil { - return nil, ctx.Err() - } - if err != nil { - return nil, err - } - - wg = core.NewWorkGroup(false) - hasMoreWork := false - if defaultDefinition != nil { - requestedProjectTrees := make(map[tspath.Path]struct{}) - results.Range(func(key tspath.Path, response *response[lsproto.ReferencesResponse]) 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 nil, 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 err != nil { + return nil, err } - // Combine all references from all projects - var combined []lsproto.Location - var seenLocations collections.Set[lsproto.Location] - var seenProjects collections.Set[tspath.Path] - - // Add default project results first - if response, loaded := results.Load(defaultProject.Id()); loaded && response.complete { - if response.result.Locations != nil { - for _, loc := range *response.result.Locations { - if !seenLocations.Has(loc) { - seenLocations.Add(loc) - combined = append(combined, loc) - } - } - } + // Combine all references from all projects using the same logic as combineReferences + combined := combineReferences(resultsIter) + var locs []lsproto.Location + if combined.Locations != nil { + locs = *combined.Locations } - seenProjects.Add(defaultProject.Id()) - - // Add other project results - for _, proj := range allProjects { - if seenProjects.AddIfAbsent(proj.Id()) { - if response, loaded := results.Load(proj.Id()); loaded && response.complete { - if response.result.Locations != nil { - for _, loc := range *response.result.Locations { - if !seenLocations.Has(loc) { - seenLocations.Add(loc) - combined = append(combined, loc) - } - } - } - } - } - } - - // Add remaining project results - results.Range(func(key tspath.Path, response *response[lsproto.ReferencesResponse]) bool { - if seenProjects.AddIfAbsent(key) && response.complete { - if response.result.Locations != nil { - for _, loc := range *response.result.Locations { - if !seenLocations.Has(loc) { - seenLocations.Add(loc) - combined = append(combined, loc) - } - } - } - } - return true - }) // Build the code lens with the combined references count locale := locale.FromContext(ctx) var lensTitle string - if len(combined) == 1 { + if len(locs) == 1 { lensTitle = diagnostics.X_1_reference.Localize(locale) } else { - lensTitle = diagnostics.X_0_references.Localize(locale, len(combined)) + lensTitle = diagnostics.X_0_references.Localize(locale, len(locs)) } cmd := &lsproto.Command{ Title: lensTitle, } - if len(combined) > 0 && s.initializeParams.InitializationOptions.CodeLensShowLocationsCommandName != nil { + if len(locs) > 0 && s.initializeParams.InitializationOptions.CodeLensShowLocationsCommandName != nil { cmd.Command = *s.initializeParams.InitializationOptions.CodeLensShowLocationsCommandName cmd.Arguments = &[]any{ uri, position, - combined, + locs, } }