Skip to content

Commit f88b8f2

Browse files
committed
Handle existing projects
1 parent 9516c6f commit f88b8f2

File tree

9 files changed

+322
-21
lines changed

9 files changed

+322
-21
lines changed

internal/lsp/server.go

Lines changed: 136 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,70 @@ 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+
defer s.recover(req)
620+
ls := s.session.GetLanguageServiceForProjectWithFile(ctx, project, params.TextDocumentURI())
621+
if ls != nil {
622+
resp, err1 := fn(s, ctx, ls, params)
623+
if err1 != nil {
624+
return
625+
}
626+
results.Store(project.Id(), resp)
627+
}
628+
})
603629
}
630+
631+
// Other projects
632+
for _, project := range allProjects {
633+
enqueueWorkForNonDefaultProject(project)
634+
}
635+
636+
// Default projects
637+
wg.Queue(func() {
638+
resp, err1 := fn(s, ctx, defaultLs, params)
639+
if err1 != nil {
640+
return
641+
}
642+
results.Store(defaultProject.Id(), resp)
643+
// TODO:: if default location needs to be add more projects to request
644+
})
645+
wg.RunAndWait()
604646
if ctx.Err() != nil {
605647
return ctx.Err()
606648
}
649+
resp := combineResults(defaultProject, &results)
607650
s.sendResult(req.ID, resp)
608651
return nil
609652
}
@@ -880,9 +923,36 @@ func (s *Server) handleTypeDefinition(ctx context.Context, ls *ls.LanguageServic
880923
return ls.ProvideTypeDefinition(ctx, params.TextDocument.Uri, params.Position, getTypeDefinitionClientSupportsLink(s.initializeParams))
881924
}
882925

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

888958
func (s *Server) handleImplementations(ctx context.Context, ls *ls.LanguageService, params *lsproto.ImplementationParams) (lsproto.ImplementationResponse, error) {
@@ -962,6 +1032,62 @@ func (s *Server) handleRename(ctx context.Context, ls *ls.LanguageService, param
9621032
return ls.ProvideRename(ctx, params)
9631033
}
9641034

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

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)