package ls import ( "context" "fmt" "iter" "runtime/debug" "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] } 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) var panicsOccured []string var panicMu sync.Mutex 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 func() { if r := recover(); r != nil { stack := debug.Stack() panicOccured := fmt.Sprintf("panic handling request: %v\n%s", r, string(stack)) panicMu.Lock() panicsOccured = append(panicsOccured, panicOccured) panicMu.Unlock() } }() // 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() // No need to use mu here since we are not in parallel at this point if panicsOccured != nil { panic(fmt.Sprintf("Panics occurred during cross-project handling: %v", panicsOccured)) } if ctx.Err() != nil { return resp, ctx.Err() } 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 combineVSReferences(results iter.Seq[lsproto.VSReferencesResponse]) lsproto.VSReferencesResponse { var combined []*lsproto.VSReferenceItem // Re-number IDs across projects to maintain unique IDs and correct definition references nextId := int32(0) for resp := range results { if resp.VSReferenceItems == nil { continue } // Map old IDs to new IDs for this batch idMap := make(map[int32]int32) for _, item := range *resp.VSReferenceItems { oldId := item.VSId newId := nextId idMap[oldId] = newId nextId++ newItem := *item newItem.VSId = newId if item.VSDefinitionId != nil { newDefId := idMap[*item.VSDefinitionId] newItem.VSDefinitionId = &newDefId } combined = append(combined, &newItem) } } return lsproto.VSReferencesResponse{VSReferenceItems: &combined} } 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]) var documentChanges []lsproto.TextDocumentEditOrCreateFileOrRenameFileOrDeleteFile seenRenames := collections.Set[[2]lsproto.DocumentUri]{} for resp := range results { if resp.WorkspaceEdit != nil && resp.WorkspaceEdit.DocumentChanges != nil { for _, change := range *resp.WorkspaceEdit.DocumentChanges { switch { case change.RenameFile != nil: key := [2]lsproto.DocumentUri{change.RenameFile.OldUri, change.RenameFile.NewUri} if seenRenames.AddIfAbsent(key) { documentChanges = append(documentChanges, change) } default: documentChanges = append(documentChanges, change) } } } 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(documentChanges) > 0 || len(combined) > 0 { workspaceEdit := &lsproto.WorkspaceEdit{} if len(documentChanges) > 0 { workspaceEdit.DocumentChanges = &documentChanges } if len(combined) > 0 { workspaceEdit.Changes = &combined } return lsproto.RenameResponse{ WorkspaceEdit: workspaceEdit, } } 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} }