Skip to content

Commit a109a9a

Browse files
committed
Handle existing projects
1 parent fa53e0d commit a109a9a

File tree

9 files changed

+321
-21
lines changed

9 files changed

+321
-21
lines changed

internal/lsp/server.go

Lines changed: 135 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -505,8 +505,8 @@ var handlers = sync.OnceValue(func() handlerMap {
505505
registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentDocumentHighlightInfo, (*Server).handleDocumentHighlight)
506506
registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentSelectionRangeInfo, (*Server).handleSelectionRange)
507507

508-
registerMultiProjectDocumentRequestHandler(handlers, lsproto.TextDocumentReferencesInfo, (*Server).handleReferences)
509-
registerMultiProjectDocumentRequestHandler(handlers, lsproto.TextDocumentRenameInfo, (*Server).handleRename)
508+
registerMultiProjectDocumentRequestHandler(handlers, lsproto.TextDocumentReferencesInfo, (*Server).handleReferences, combineReferences)
509+
registerMultiProjectDocumentRequestHandler(handlers, lsproto.TextDocumentRenameInfo, (*Server).handleRename, combineRenameResponse)
510510

511511
registerRequestHandler(handlers, lsproto.WorkspaceSymbolInfo, (*Server).handleWorkspaceSymbol)
512512
registerRequestHandler(handlers, lsproto.CompletionItemResolveInfo, (*Server).handleCompletionItemResolve)
@@ -583,27 +583,69 @@ func registerLanguageServiceDocumentRequestHandler[Req lsproto.HasTextDocumentUR
583583
}
584584
}
585585

586-
func registerMultiProjectDocumentRequestHandler[Req lsproto.HasTextDocumentURI, Resp any](handlers handlerMap, info lsproto.RequestInfo[Req, Resp], fn func(*Server, context.Context, *ls.LanguageService, Req) (Resp, error)) {
586+
func registerMultiProjectDocumentRequestHandler[Req lsproto.HasTextDocumentURI, Resp any](
587+
handlers handlerMap,
588+
info lsproto.RequestInfo[Req, Resp],
589+
fn func(*Server, context.Context, *ls.LanguageService, Req) (Resp, error),
590+
combineResults func(*project.Project, *collections.SyncMap[tspath.Path, Resp]) Resp,
591+
) {
587592
handlers[info.Method] = func(s *Server, ctx context.Context, req *lsproto.RequestMessage) error {
588593
var params Req
589594
// Ignore empty params.
590595
if req.Params != nil {
591596
params = req.Params.(Req)
592597
}
593598
// !!! sheetal: multiple projects that contain the file through symlinks
594-
// !!! multiple projects that contain the file directly
595-
ls, err := s.session.GetLanguageService(ctx, params.TextDocumentURI())
599+
defaultProject, defaultLs, allProjects, err := s.session.GetProjectsForFile(ctx, params.TextDocumentURI())
596600
if err != nil {
597601
return err
598602
}
599603
defer s.recover(req)
600-
resp, err := fn(s, ctx, ls, params)
601-
if err != nil {
602-
return err
604+
var results collections.SyncMap[tspath.Path, Resp]
605+
var workInProgress collections.SyncMap[tspath.Path, bool]
606+
workInProgress.Store(defaultProject.Id(), true)
607+
wg := core.NewWorkGroup(false)
608+
enqueueWorkForNonDefaultProject := func(project *project.Project) {
609+
if project == defaultProject {
610+
return
611+
}
612+
wg.Queue(func() {
613+
if ctx.Err() != nil {
614+
return
615+
}
616+
if _, loaded := workInProgress.LoadOrStore(project.Id(), true); loaded {
617+
return
618+
}
619+
ls := s.session.GetLanguageServiceForProjectWithFile(ctx, project, params.TextDocumentURI())
620+
if ls != nil {
621+
resp, err1 := fn(s, ctx, ls, params)
622+
if err1 != nil {
623+
return
624+
}
625+
results.Store(project.Id(), resp)
626+
}
627+
})
603628
}
629+
630+
// Other projects
631+
for _, project := range allProjects {
632+
enqueueWorkForNonDefaultProject(project)
633+
}
634+
635+
// Default projects
636+
wg.Queue(func() {
637+
resp, err1 := fn(s, ctx, defaultLs, params)
638+
if err1 != nil {
639+
return
640+
}
641+
results.Store(defaultProject.Id(), resp)
642+
// TODO:: if default location needs to be add more projects to request
643+
})
644+
wg.RunAndWait()
604645
if ctx.Err() != nil {
605646
return ctx.Err()
606647
}
648+
resp := combineResults(defaultProject, &results)
607649
s.sendResult(req.ID, resp)
608650
return nil
609651
}
@@ -879,9 +921,36 @@ func (s *Server) handleTypeDefinition(ctx context.Context, ls *ls.LanguageServic
879921
return ls.ProvideTypeDefinition(ctx, params.TextDocument.Uri, params.Position, getTypeDefinitionClientSupportsLink(s.initializeParams))
880922
}
881923

882-
func (s *Server) handleReferences(ctx context.Context, ls *ls.LanguageService, params *lsproto.ReferenceParams) (lsproto.ReferencesResponse, error) {
924+
func (s *Server) handleReferences(ctx context.Context, defaultLs *ls.LanguageService, params *lsproto.ReferenceParams) (lsproto.ReferencesResponse, error) {
883925
// findAllReferences
884-
return ls.ProvideReferences(ctx, params)
926+
return defaultLs.ProvideReferences(ctx, params)
927+
}
928+
929+
func combineReferences(defaultProject *project.Project, results *collections.SyncMap[tspath.Path, lsproto.ReferencesResponse]) lsproto.ReferencesResponse {
930+
var combined []lsproto.Location
931+
var seenLocations collections.Set[lsproto.Location]
932+
if resp, ok := results.Load(defaultProject.Id()); ok {
933+
if resp.Locations != nil {
934+
for _, loc := range *resp.Locations {
935+
seenLocations.Add(loc)
936+
combined = append(combined, loc)
937+
}
938+
}
939+
}
940+
results.Range(func(projectID tspath.Path, resp lsproto.ReferencesResponse) bool {
941+
if projectID != defaultProject.Id() {
942+
if resp.Locations != nil {
943+
for _, loc := range *resp.Locations {
944+
if !seenLocations.Has(loc) {
945+
seenLocations.Add(loc)
946+
combined = append(combined, loc)
947+
}
948+
}
949+
}
950+
}
951+
return true
952+
})
953+
return lsproto.LocationsOrNull{Locations: &combined}
885954
}
886955

887956
func (s *Server) handleImplementations(ctx context.Context, ls *ls.LanguageService, params *lsproto.ImplementationParams) (lsproto.ImplementationResponse, error) {
@@ -961,6 +1030,62 @@ func (s *Server) handleRename(ctx context.Context, ls *ls.LanguageService, param
9611030
return ls.ProvideRename(ctx, params)
9621031
}
9631032

1033+
func combineRenameResponse(defaultProject *project.Project, results *collections.SyncMap[tspath.Path, lsproto.RenameResponse]) lsproto.RenameResponse {
1034+
combined := make(map[lsproto.DocumentUri][]*lsproto.TextEdit)
1035+
seenChanges := make(map[lsproto.DocumentUri]*collections.Set[lsproto.TextEdit])
1036+
// !!! this is not used any more so we will skip this part of deduplication and combining
1037+
// DocumentChanges *[]TextDocumentEditOrCreateFileOrRenameFileOrDeleteFile `json:"documentChanges,omitzero"`
1038+
// ChangeAnnotations *map[string]*ChangeAnnotation `json:"changeAnnotations,omitzero"`
1039+
1040+
if resp, ok := results.Load(defaultProject.Id()); ok {
1041+
if resp.WorkspaceEdit != nil {
1042+
for doc, changes := range *resp.WorkspaceEdit.Changes {
1043+
seenSet := collections.Set[lsproto.TextEdit]{}
1044+
seenChanges[doc] = &seenSet
1045+
var changesForDoc []*lsproto.TextEdit
1046+
for _, change := range changes {
1047+
seenSet.Add(*change)
1048+
changesForDoc = append(changesForDoc, change)
1049+
}
1050+
combined[doc] = changesForDoc
1051+
}
1052+
}
1053+
}
1054+
results.Range(func(projectID tspath.Path, resp lsproto.RenameResponse) bool {
1055+
if projectID != defaultProject.Id() {
1056+
if resp.WorkspaceEdit != nil {
1057+
for doc, changes := range *resp.WorkspaceEdit.Changes {
1058+
seenSet, ok := seenChanges[doc]
1059+
if !ok {
1060+
seenSet = &collections.Set[lsproto.TextEdit]{}
1061+
seenChanges[doc] = seenSet
1062+
}
1063+
changesForDoc, exists := combined[doc]
1064+
if !exists {
1065+
changesForDoc = []*lsproto.TextEdit{}
1066+
}
1067+
for _, change := range changes {
1068+
if !seenSet.Has(*change) {
1069+
seenSet.Add(*change)
1070+
changesForDoc = append(changesForDoc, change)
1071+
}
1072+
}
1073+
combined[doc] = changesForDoc
1074+
}
1075+
}
1076+
}
1077+
return true
1078+
})
1079+
if len(combined) > 0 {
1080+
return lsproto.RenameResponse{
1081+
WorkspaceEdit: &lsproto.WorkspaceEdit{
1082+
Changes: &combined,
1083+
},
1084+
}
1085+
}
1086+
return lsproto.RenameResponse{}
1087+
}
1088+
9641089
func (s *Server) handleDocumentHighlight(ctx context.Context, ls *ls.LanguageService, params *lsproto.DocumentHighlightParams) (lsproto.DocumentHighlightResponse, error) {
9651090
return ls.ProvideDocumentHighlights(ctx, params.TextDocument.Uri, params.Position)
9661091
}

internal/project/project.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,10 @@ func (p *Project) ConfigFilePath() tspath.Path {
192192
return p.configFilePath
193193
}
194194

195+
func (p *Project) Id() tspath.Path {
196+
return p.configFilePath
197+
}
198+
195199
func (p *Project) GetProgram() *compiler.Program {
196200
return p.Program
197201
}

internal/project/projectcollection.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,19 @@ func (c *ProjectCollection) InferredProject() *Project {
9393
return c.inferredProject
9494
}
9595

96+
func (c *ProjectCollection) GetProjectsContainingFile(path tspath.Path) []*Project {
97+
var projects []*Project
98+
for _, project := range c.ConfiguredProjects() {
99+
if project.containsFile(path) {
100+
projects = append(projects, project)
101+
}
102+
}
103+
if c.inferredProject != nil && c.inferredProject.containsFile(path) {
104+
projects = append(projects, c.inferredProject)
105+
}
106+
return projects
107+
}
108+
96109
// !!! result could be cached
97110
func (c *ProjectCollection) GetDefaultProject(fileName string, path tspath.Path) *Project {
98111
if result, ok := c.fileDefaultProjects[path]; ok {

internal/project/projectcollectionbuilder.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,25 @@ func (b *ProjectCollectionBuilder) DidRequestFile(uri lsproto.DocumentUri, logge
338338
}
339339
}
340340

341+
func (b *ProjectCollectionBuilder) DidRequestProject(projectId tspath.Path, logger *logging.LogTree) {
342+
startTime := time.Now()
343+
if projectId == inferredProjectName {
344+
// Update inferred project
345+
if b.inferredProject.Value() != nil {
346+
b.updateProgram(b.inferredProject, logger)
347+
}
348+
} else {
349+
if entry, ok := b.configuredProjects.Load(projectId); ok {
350+
b.updateProgram(entry, logger)
351+
}
352+
}
353+
354+
if logger != nil {
355+
elapsed := time.Since(startTime)
356+
logger.Log(fmt.Sprintf("Completed project update request for %s in %v", projectId, elapsed))
357+
}
358+
}
359+
341360
func (b *ProjectCollectionBuilder) DidUpdateATAState(ataChanges map[tspath.Path]*ATAStateChange, logger *logging.LogTree) {
342361
updateProject := func(project dirty.Value[*Project], ataChange *ATAStateChange) {
343362
project.ChangeIf(

internal/project/session.go

Lines changed: 56 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -371,29 +371,34 @@ func (s *Session) Snapshot() (*Snapshot, func()) {
371371
}
372372
}
373373

374-
func (s *Session) GetLanguageService(ctx context.Context, uri lsproto.DocumentUri) (*ls.LanguageService, error) {
374+
func (s *Session) getSnapshot(ctx context.Context, uri lsproto.DocumentUri, projectId tspath.Path) (*Snapshot, map[tspath.Path]*Overlay, bool) {
375375
var snapshot *Snapshot
376376
fileChanges, overlays, ataChanges, newConfig := s.flushChanges(ctx)
377377
updateSnapshot := !fileChanges.IsEmpty() || len(ataChanges) > 0 || newConfig != nil
378378
if updateSnapshot {
379379
// If there are pending file changes, we need to update the snapshot.
380380
// Sending the requested URI ensures that the project for this URI is loaded.
381381
snapshot = s.UpdateSnapshot(ctx, overlays, SnapshotChange{
382-
reason: UpdateReasonRequestedLanguageServicePendingChanges,
383-
fileChanges: fileChanges,
384-
ataChanges: ataChanges,
385-
newConfig: newConfig,
386-
requestedURIs: []lsproto.DocumentUri{uri},
382+
reason: UpdateReasonRequestedLanguageServicePendingChanges,
383+
fileChanges: fileChanges,
384+
ataChanges: ataChanges,
385+
newConfig: newConfig,
386+
requestedURIs: core.IfElse(uri != "", []lsproto.DocumentUri{uri}, nil),
387+
requestedProjectUpdates: core.IfElse(projectId != "", []tspath.Path{projectId}, nil),
387388
})
388389
} else {
389390
// If there are no pending file changes, we can try to use the current snapshot.
390391
s.snapshotMu.RLock()
391392
snapshot = s.snapshot
392393
s.snapshotMu.RUnlock()
393394
}
395+
return snapshot, overlays, updateSnapshot
396+
}
394397

398+
func (s *Session) getSnapshotAndDefaultProject(ctx context.Context, uri lsproto.DocumentUri) (*Snapshot, *Project, *ls.LanguageService, error) {
399+
snapshot, overlays, updatedSnapshot := s.getSnapshot(ctx, uri, "")
395400
project := snapshot.GetDefaultProject(uri)
396-
if project == nil && !updateSnapshot || project != nil && project.dirty {
401+
if project == nil && !updatedSnapshot || project != nil && project.dirty {
397402
// The current snapshot does not have an up to date project for the URI,
398403
// so we need to update the snapshot to ensure the project is loaded.
399404
// !!! Allow multiple projects to update in parallel
@@ -404,9 +409,51 @@ func (s *Session) GetLanguageService(ctx context.Context, uri lsproto.DocumentUr
404409
project = snapshot.GetDefaultProject(uri)
405410
}
406411
if project == nil {
407-
return nil, fmt.Errorf("no project found for URI %s", uri)
412+
return nil, nil, nil, fmt.Errorf("no project found for URI %s", uri)
413+
}
414+
return snapshot, project, ls.NewLanguageService(project.GetProgram(), snapshot), nil
415+
}
416+
417+
func (s *Session) GetLanguageService(ctx context.Context, uri lsproto.DocumentUri) (*ls.LanguageService, error) {
418+
_, _, languageService, err := s.getSnapshotAndDefaultProject(ctx, uri)
419+
if err != nil {
420+
return nil, err
421+
}
422+
return languageService, nil
423+
}
424+
425+
func (s *Session) GetProjectsForFile(ctx context.Context, uri lsproto.DocumentUri) (*Project, *ls.LanguageService, []*Project, error) {
426+
snapshot, project, defaultLs, err := s.getSnapshotAndDefaultProject(ctx, uri)
427+
if err != nil {
428+
return nil, nil, nil, err
429+
}
430+
// !!! TODO: sheetal: Get other projects that contain the file with symlink
431+
allProjects := snapshot.GetProjectsContainingFile(uri)
432+
return project, defaultLs, allProjects, nil
433+
}
434+
435+
func (s *Session) GetLanguageServiceForProjectWithFile(ctx context.Context, project *Project, uri lsproto.DocumentUri) *ls.LanguageService {
436+
snapshot, overlays, updatedSnapshot := s.getSnapshot(ctx, "", project.Id())
437+
// update the snapshot
438+
if !updatedSnapshot && project.dirty {
439+
// The current snapshot does not have an up to date project for the URI,
440+
// so we need to update the snapshot to ensure the project is loaded.
441+
// !!! Allow multiple projects to update in parallel
442+
snapshot = s.UpdateSnapshot(ctx, overlays, SnapshotChange{
443+
reason: UpdateReasonRequestedLanguageServiceProjectDirty,
444+
requestedProjectUpdates: []tspath.Path{project.Id()},
445+
})
446+
}
447+
// Ensure we have updated project
448+
project = snapshot.ProjectCollection.GetProjectByPath(project.Id())
449+
if project == nil {
450+
return nil
451+
}
452+
// if program doesnt contain this file any more ignore it
453+
if path := s.toPath(uri.FileName()); !project.containsFile(path) {
454+
return nil
408455
}
409-
return ls.NewLanguageService(project.GetProgram(), snapshot), nil
456+
return ls.NewLanguageService(project.GetProgram(), snapshot)
410457
}
411458

412459
func (s *Session) UpdateSnapshot(ctx context.Context, overlays map[tspath.Path]*Overlay, change SnapshotChange) *Snapshot {

internal/project/snapshot.go

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,13 @@ func (s *Snapshot) GetDefaultProject(uri lsproto.DocumentUri) *Project {
7676
return s.ProjectCollection.GetDefaultProject(fileName, path)
7777
}
7878

79+
func (s *Snapshot) GetProjectsContainingFile(uri lsproto.DocumentUri) []*Project {
80+
fileName := uri.FileName()
81+
path := s.toPath(fileName)
82+
// TODO!! sheetal may be change this to handle symlinks!!
83+
return s.ProjectCollection.GetProjectsContainingFile(path)
84+
}
85+
7986
func (s *Snapshot) GetFile(fileName string) FileHandle {
8087
return s.fs.GetFile(fileName)
8188
}
@@ -135,6 +142,9 @@ type SnapshotChange struct {
135142
// requestedURIs are URIs that were requested by the client.
136143
// The new snapshot should ensure projects for these URIs have loaded programs.
137144
requestedURIs []lsproto.DocumentUri
145+
// Update requested projects.
146+
// this is used when we want to get LS and from all the projects the file can be part of
147+
requestedProjectUpdates []tspath.Path
138148
// compilerOptionsForInferredProjects is the compiler options to use for inferred projects.
139149
// It should only be set the value in the next snapshot should be changed. If nil, the
140150
// value from the previous snapshot will be copied to the new snapshot.
@@ -185,11 +195,11 @@ func (s *Snapshot) Clone(ctx context.Context, change SnapshotChange, overlays ma
185195
case UpdateReasonDidChangeCompilerOptionsForInferredProjects:
186196
logger.Logf("Reason: DidChangeCompilerOptionsForInferredProjects")
187197
case UpdateReasonRequestedLanguageServicePendingChanges:
188-
logger.Logf("Reason: RequestedLanguageService (pending file changes) - %v", change.requestedURIs)
198+
logger.Logf("Reason: RequestedLanguageService (pending file changes) - %v, %v", change.requestedURIs, change.requestedProjectUpdates)
189199
case UpdateReasonRequestedLanguageServiceProjectNotLoaded:
190200
logger.Logf("Reason: RequestedLanguageService (project not loaded) - %v", change.requestedURIs)
191201
case UpdateReasonRequestedLanguageServiceProjectDirty:
192-
logger.Logf("Reason: RequestedLanguageService (project dirty) - %v", change.requestedURIs)
202+
logger.Logf("Reason: RequestedLanguageService (project dirty) - %v, %v", change.requestedURIs, change.requestedProjectUpdates)
193203
}
194204
}
195205

@@ -248,6 +258,10 @@ func (s *Snapshot) Clone(ctx context.Context, change SnapshotChange, overlays ma
248258
projectCollectionBuilder.DidRequestFile(uri, logger.Fork("DidRequestFile"))
249259
}
250260

261+
for _, projectId := range change.requestedProjectUpdates {
262+
projectCollectionBuilder.DidRequestProject(projectId, logger.Fork("DidRequestProject"))
263+
}
264+
251265
projectCollection, configFileRegistry := projectCollectionBuilder.Finalize(logger)
252266

253267
// Clean cached disk files not touched by any open project. It's not important that we do this on

0 commit comments

Comments
 (0)