@@ -592,6 +592,11 @@ type projectAndTextDocumentPosition struct {
592592 Position lsproto.Position
593593}
594594
595+ type response [Resp any ] struct {
596+ complete bool
597+ result Resp
598+ }
599+
595600func registerMultiProjectReferenceRequestHandler [Req lsproto.HasTextDocumentPosition , Resp any ](
596601 handlers handlerMap ,
597602 info lsproto.RequestInfo [Req , Resp ],
@@ -611,51 +616,85 @@ func registerMultiProjectReferenceRequestHandler[Req lsproto.HasTextDocumentPosi
611616 }
612617 defer s .recover (req )
613618
614- var queue []projectAndTextDocumentPosition
615- var results collections.OrderedMap [tspath.Path , Resp ]
619+ var results collections.SyncMap [tspath.Path , * response [Resp ]]
616620 var defaultDefinition * ls.NonLocalDefinition
621+ canSearchProject := func (project * project.Project ) bool {
622+ _ , searched := results .Load (project .Id ())
623+ return ! searched
624+ }
625+ wg := core .NewWorkGroup (false )
626+ var errMu sync.Mutex
627+ var enqueueItem func (item projectAndTextDocumentPosition )
628+ enqueueItem = func (item projectAndTextDocumentPosition ) {
629+ var response response [Resp ]
630+ if _ , loaded := results .LoadOrStore (item .project .Id (), & response ); loaded {
631+ return
632+ }
633+ wg .Queue (func () {
634+ if ctx .Err () != nil {
635+ return
636+ }
617637
618- searchPosition := func (project * project.Project , ls * ls.LanguageService , uri lsproto.DocumentUri , position lsproto.Position ) (Resp , error ) {
619- originalNode , symbolsAndEntries , ok := ls .ProvideSymbolsAndEntries (ctx , uri , position , info .Method == lsproto .MethodTextDocumentRename )
620- if ok {
621- for _ , entry := range symbolsAndEntries {
622- // Find the default definition that can be in another project
623- // Later we will use this load ancestor tree that references this location and expand search
624- if project == defaultProject && defaultDefinition == nil {
625- defaultDefinition = ls .GetNonLocalDefinition (ctx , entry )
638+ // Process the item
639+ ls := item .ls
640+ if ls == nil {
641+ // Get it now
642+ ls = s .session .GetLanguageServiceForProjectWithFile (ctx , item .project , item .Uri )
643+ if ls == nil {
644+ return
626645 }
627- ls .ForEachOriginalDefinitionLocation (ctx , entry , func (uri lsproto.DocumentUri , position lsproto.Position ) {
628- // Get default configured project for this file
629- defProjects , errProjects := s .session .GetProjectsForFile (ctx , uri )
630- if errProjects != nil {
631- return
646+ }
647+ originalNode , symbolsAndEntries , ok := ls .ProvideSymbolsAndEntries (ctx , item .Uri , item .Position , info .Method == lsproto .MethodTextDocumentRename )
648+ if ok {
649+ for _ , entry := range symbolsAndEntries {
650+ // Find the default definition that can be in another project
651+ // Later we will use this load ancestor tree that references this location and expand search
652+ if item .project == defaultProject && defaultDefinition == nil {
653+ defaultDefinition = ls .GetNonLocalDefinition (ctx , entry )
632654 }
633- for _ , defProject := range defProjects {
634- // Optimization: don't enqueue if will be discarded
635- if ! results .Has (defProject .Id ()) {
636- queue = append (queue , projectAndTextDocumentPosition {
637- project : defProject ,
638- Uri : uri ,
639- Position : position ,
640- })
655+ ls .ForEachOriginalDefinitionLocation (ctx , entry , func (uri lsproto.DocumentUri , position lsproto.Position ) {
656+ // Get default configured project for this file
657+ defProjects , errProjects := s .session .GetProjectsForFile (ctx , uri )
658+ if errProjects != nil {
659+ return
641660 }
642- }
643- })
661+ for _ , defProject := range defProjects {
662+ // Optimization: don't enqueue if will be discarded
663+ if canSearchProject (defProject ) {
664+ enqueueItem (projectAndTextDocumentPosition {
665+ project : defProject ,
666+ Uri : uri ,
667+ Position : position ,
668+ })
669+ }
670+ }
671+ })
672+ }
644673 }
645- }
646- return fn (s , ctx , ls , params , originalNode , symbolsAndEntries )
674+
675+ if result , errSearch := fn (s , ctx , ls , params , originalNode , symbolsAndEntries ); errSearch == nil {
676+ response .complete = true
677+ response .result = result
678+ } else {
679+ errMu .Lock ()
680+ defer errMu .Unlock ()
681+ if err != nil {
682+ err = errSearch
683+ }
684+ }
685+ })
647686 }
648687
649688 // Initial set of projects and locations in the queue, starting with default project
650- queue = append ( queue , projectAndTextDocumentPosition {
689+ enqueueItem ( projectAndTextDocumentPosition {
651690 project : defaultProject ,
652691 ls : defaultLs ,
653692 Uri : params .TextDocumentURI (),
654693 Position : params .TextDocumentPosition (),
655694 })
656695 for _ , project := range allProjects {
657696 if project != defaultProject {
658- queue = append ( queue , projectAndTextDocumentPosition {
697+ enqueueItem ( projectAndTextDocumentPosition {
659698 project : project ,
660699 // TODO!! symlinks need to change the URI
661700 Uri : params .TextDocumentURI (),
@@ -664,69 +703,92 @@ func registerMultiProjectReferenceRequestHandler[Req lsproto.HasTextDocumentPosi
664703 }
665704 }
666705
667- // Outer loop - to complete work if more is added after completing existing queue
668- for len (queue ) > 0 {
669- // Process existing known projects first
670- for len (queue ) > 0 {
671- if ctx .Err () != nil {
672- return ctx .Err ()
673- }
674-
675- item := queue [0 ]
676- queue = queue [1 :]
677-
678- if results .Has (item .project .Id ()) {
679- continue
680- }
681-
682- // Process the item
683- ls := item .ls
684- if ls == nil {
685- // Get it now
686- ls = s .session .GetLanguageServiceForProjectWithFile (ctx , item .project , item .Uri )
687- if ls == nil {
688- continue
706+ getResultsIterator := func () iter.Seq [Resp ] {
707+ return func (yield func (Resp ) bool ) {
708+ var seenProjects collections.SyncSet [tspath.Path ]
709+ if response , loaded := results .Load (defaultProject .Id ()); loaded && response .complete {
710+ if ! yield (response .result ) {
711+ return
689712 }
690713 }
691- if result , err := searchPosition (item .project , ls , item .Uri , item .Position ); err == nil {
692- results .Set (item .project .Id (), result )
693- } else {
694- return err
714+ seenProjects .Add (defaultProject .Id ())
715+ for _ , project := range allProjects {
716+ if seenProjects .AddIfAbsent (project .Id ()) {
717+ if response , loaded := results .Load (project .Id ()); loaded && response .complete {
718+ if ! yield (response .result ) {
719+ return
720+ }
721+ }
722+ }
695723 }
724+ results .Range (func (key tspath.Path , response * response [Resp ]) bool {
725+ if seenProjects .AddIfAbsent (key ) && response .complete {
726+ return yield (response .result )
727+ }
728+ return true
729+ })
696730 }
731+ }
697732
733+ // Outer loop - to complete work if more is added after completing existing queue
734+ for {
735+ // Process existing known projects first
736+ wg .RunAndWait ()
737+ if ctx .Err () != nil {
738+ return ctx .Err ()
739+ }
740+ // No need to use mu here since we are not in parallel at this point
741+ if err != nil {
742+ return err
743+ }
744+
745+ wg = core .NewWorkGroup (false )
746+ hasMoreWork := false
698747 if defaultDefinition != nil {
699- if err := s .session .ForEachProjectLocationLoadingProjectTree (
748+ requestedProjectTrees := make (map [tspath.Path ]struct {})
749+ results .Range (func (key tspath.Path , response * response [Resp ]) bool {
750+ if response .complete {
751+ requestedProjectTrees [key ] = struct {}{}
752+ }
753+ return true
754+ })
755+
756+ // Load more projects based on default definition found
757+ if errFromLoadingTree := s .session .ForEachProjectLocationLoadingProjectTree (
700758 ctx ,
701- results . Keys () ,
759+ requestedProjectTrees ,
702760 defaultDefinition ,
703761 // Can loop forever without this (enqueue here, dequeue above, repeat)
704- func (projectFromSnapshot * project.Project ) bool {
705- return ! results .Has (projectFromSnapshot .Id ())
706- },
762+ canSearchProject ,
707763 func (project * project.Project , uri lsproto.DocumentUri , position lsproto.Position ) {
708764 // Enqueue the project and location for further processing
709- queue = append ( queue , projectAndTextDocumentPosition {
765+ enqueueItem ( projectAndTextDocumentPosition {
710766 project : project ,
711767 Uri : uri ,
712768 Position : position ,
713769 })
770+ hasMoreWork = true
714771 },
715- ); err != nil {
716- return err
772+ ); errFromLoadingTree != nil {
773+ return errFromLoadingTree
717774 }
718775 }
776+ if ! hasMoreWork {
777+ break
778+ }
719779 }
720780
721781 var resp Resp
722782 if results .Size () > 1 {
723- resp = combineResults (results . Values ())
783+ resp = combineResults (getResultsIterator ())
724784 } else {
725- for value := range results .Values () {
785+ // Single result, return that directly
786+ for value := range getResultsIterator () {
726787 resp = value
727788 break
728789 }
729790 }
791+
730792 s .sendResult (req .ID , resp )
731793 return nil
732794 }
0 commit comments