Skip to content

Commit 9fbec96

Browse files
xieyuschenfindleyr
authored andcommitted
gopls/internal/server: workspace: skip adding already added file
This CL adds a check to prevent re-adding a folder to the workspace if it has already been added. It resolves an issue where the 'loading packages...' status bar would get stuck in the editor. The problem occurs when the editor sends an initialization request with a rootURI but no workspaceFolders, followed by a DidChangeWorkspaceFolders request for the same folder. Re-adding the same folder is unnecessary and causes the status bar to hang. Additionally, this change cleans the provided URI to handle cases where the client appends a trailing slash to the same folder. An alternative approach is to reload the added folder again and end the process of loading package as expected so the status bar could end soon, which is unneccessary because there is no change in given file. Fixes golang/go#71967 Change-Id: Ib28c327a5a85f1fdd54e4facedf97d500b909fb2 Reviewed-on: https://go-review.googlesource.com/c/tools/+/663295 LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Robert Findley <[email protected]> Reviewed-by: Dmitri Shuralyov <[email protected]>
1 parent 456962e commit 9fbec96

File tree

6 files changed

+122
-9
lines changed

6 files changed

+122
-9
lines changed

gopls/internal/cache/session.go

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -139,11 +139,18 @@ func (s *Session) NewView(ctx context.Context, folder *Folder) (*View, *Snapshot
139139
}
140140
view, snapshot, release := s.createView(ctx, def)
141141
s.views = append(s.views, view)
142-
// we always need to drop the view map
143-
s.viewMap = make(map[protocol.DocumentURI]*View)
142+
s.viewMap[protocol.Clean(folder.Dir)] = view
144143
return view, snapshot, release, nil
145144
}
146145

146+
// HasView checks whether the uri's view exists.
147+
func (s *Session) HasView(uri protocol.DocumentURI) bool {
148+
s.viewMu.Lock()
149+
defer s.viewMu.Unlock()
150+
_, ok := s.viewMap[protocol.Clean(uri)]
151+
return ok
152+
}
153+
147154
// createView creates a new view, with an initial snapshot that retains the
148155
// supplied context, detached from events and cancelation.
149156
//
@@ -389,7 +396,7 @@ func (s *Session) SnapshotOf(ctx context.Context, uri protocol.DocumentURI) (*Sn
389396
// View is shut down. Forget this association.
390397
s.viewMu.Lock()
391398
if s.viewMap[uri] == v {
392-
delete(s.viewMap, uri)
399+
delete(s.viewMap, protocol.Clean(uri))
393400
}
394401
s.viewMu.Unlock()
395402
}
@@ -478,7 +485,7 @@ func (s *Session) viewOfLocked(ctx context.Context, uri protocol.DocumentURI) (*
478485
// (as in golang/go#60776).
479486
v = relevantViews[0]
480487
}
481-
s.viewMap[uri] = v // may be nil
488+
s.viewMap[protocol.Clean(uri)] = v // may be nil
482489
}
483490
return v, nil
484491
}

gopls/internal/protocol/uri.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,11 @@ func (uri *DocumentURI) UnmarshalText(data []byte) (err error) {
6767
return
6868
}
6969

70+
// Clean returns the cleaned uri by triggering filepath.Clean underlying.
71+
func Clean(uri DocumentURI) DocumentURI {
72+
return URIFromPath(filepath.Clean(uri.Path()))
73+
}
74+
7075
// Path returns the file path for the given URI.
7176
//
7277
// DocumentURI("").Path() returns the empty string.

gopls/internal/server/general.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -312,10 +312,15 @@ func (s *server) addFolders(ctx context.Context, folders []protocol.WorkspaceFol
312312
// but the list can grow over time.
313313
var filtered []protocol.WorkspaceFolder
314314
for _, f := range folders {
315-
if _, err := protocol.ParseDocumentURI(f.URI); err != nil {
315+
uri, err := protocol.ParseDocumentURI(f.URI)
316+
if err != nil {
316317
debuglog.Warning.Logf(ctx, "skip adding virtual folder %q - invalid folder URI: %v", f.Name, err)
317318
continue
318319
}
320+
if s.session.HasView(uri) {
321+
debuglog.Warning.Logf(ctx, "skip adding the already added folder %q - its view has been created before", f.Name)
322+
continue
323+
}
319324
filtered = append(filtered, f)
320325
}
321326
folders = filtered

gopls/internal/test/integration/fake/editor.go

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,14 @@ type EditorConfig struct {
108108
// To explicitly send no workspace folders, use an empty (non-nil) slice.
109109
WorkspaceFolders []string
110110

111+
// NoDefaultWorkspaceFiles is used to specify whether the fake editor
112+
// should give a default workspace folder when WorkspaceFolders is nil.
113+
// When it's true, the editor will pass original WorkspaceFolders as is to the LSP server.
114+
NoDefaultWorkspaceFiles bool
115+
116+
// RelRootPath is the root path which will be converted to rootUri to configure on the LSP server.
117+
RelRootPath string
118+
111119
// Whether to edit files with windows line endings.
112120
WindowsLineEndings bool
113121

@@ -322,8 +330,9 @@ func (e *Editor) initialize(ctx context.Context) error {
322330
Version: "v1.0.0",
323331
}
324332
params.InitializationOptions = makeSettings(e.sandbox, config, nil)
325-
params.WorkspaceFolders = makeWorkspaceFolders(e.sandbox, config.WorkspaceFolders)
326333

334+
params.WorkspaceFolders = makeWorkspaceFolders(e.sandbox, config.WorkspaceFolders, config.NoDefaultWorkspaceFiles)
335+
params.RootURI = protocol.DocumentURI(makeRootURI(e.sandbox, config.RelRootPath))
327336
capabilities, err := clientCapabilities(config)
328337
if err != nil {
329338
return fmt.Errorf("unmarshalling EditorConfig.CapabilitiesJSON: %v", err)
@@ -447,7 +456,10 @@ var uriRE = regexp.MustCompile(`^[a-z][a-z0-9+\-.]*://\S+`)
447456

448457
// makeWorkspaceFolders creates a slice of workspace folders to use for
449458
// this editing session, based on the editor configuration.
450-
func makeWorkspaceFolders(sandbox *Sandbox, paths []string) (folders []protocol.WorkspaceFolder) {
459+
func makeWorkspaceFolders(sandbox *Sandbox, paths []string, useEmpty bool) (folders []protocol.WorkspaceFolder) {
460+
if len(paths) == 0 && useEmpty {
461+
return nil
462+
}
451463
if len(paths) == 0 {
452464
paths = []string{string(sandbox.Workdir.RelativeTo)}
453465
}
@@ -466,6 +478,14 @@ func makeWorkspaceFolders(sandbox *Sandbox, paths []string) (folders []protocol.
466478
return folders
467479
}
468480

481+
func makeRootURI(sandbox *Sandbox, path string) string {
482+
uri := path
483+
if !uriRE.MatchString(path) { // relative file path
484+
uri = string(sandbox.Workdir.URI(path))
485+
}
486+
return uri
487+
}
488+
469489
// onFileChanges is registered to be called by the Workdir on any writes that
470490
// go through the Workdir API. It is called synchronously by the Workdir.
471491
func (e *Editor) onFileChanges(ctx context.Context, evts []protocol.FileEvent) {
@@ -1645,8 +1665,8 @@ func (e *Editor) ChangeWorkspaceFolders(ctx context.Context, folders []string) e
16451665
config := e.Config()
16461666

16471667
// capture existing folders so that we can compute the change.
1648-
oldFolders := makeWorkspaceFolders(e.sandbox, config.WorkspaceFolders)
1649-
newFolders := makeWorkspaceFolders(e.sandbox, folders)
1668+
oldFolders := makeWorkspaceFolders(e.sandbox, config.WorkspaceFolders, config.NoDefaultWorkspaceFiles)
1669+
newFolders := makeWorkspaceFolders(e.sandbox, folders, config.NoDefaultWorkspaceFiles)
16501670
config.WorkspaceFolders = folders
16511671
e.SetConfig(config)
16521672

gopls/internal/test/integration/options.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,22 @@ func WorkspaceFolders(relFolders ...string) RunOption {
135135
})
136136
}
137137

138+
// NoDefaultWorkspaceFiles is used to specify whether the fake editor
139+
// should give a default workspace folder to the LSP server.
140+
// When it's true, the editor will pass original WorkspaceFolders to the LSP server.
141+
func NoDefaultWorkspaceFiles() RunOption {
142+
return optionSetter(func(opts *runConfig) {
143+
opts.editor.NoDefaultWorkspaceFiles = true
144+
})
145+
}
146+
147+
// RootPath configures the roo path which will be converted to rootUri and sent to the LSP server.
148+
func RootPath(relpath string) RunOption {
149+
return optionSetter(func(opts *runConfig) {
150+
opts.editor.RelRootPath = relpath
151+
})
152+
}
153+
138154
// FolderSettings defines per-folder workspace settings, keyed by relative path
139155
// to the folder.
140156
//

gopls/internal/test/integration/workspace/workspace_test.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1412,6 +1412,66 @@ func TestInitializeWithNonFileWorkspaceFolders(t *testing.T) {
14121412
}
14131413
}
14141414

1415+
// TestChangeAddedWorkspaceFolders tests issue71967 which an editor sends the following requests.
1416+
//
1417+
// 1. send an initialization request with rootURI but no workspaceFolders,
1418+
// which gopls helps to find a workspaceFolders for it.
1419+
// 2. send a DidChangeWorkspaceFolders request with the exact the same folder gopls helps to find.
1420+
//
1421+
// It uses the same approach to simulate the scenario, and ensure we can skip the already added file.
1422+
func TestChangeAddedWorkspaceFolders(t *testing.T) {
1423+
for _, tt := range []struct {
1424+
name string
1425+
after []string
1426+
wantViewRoots []string
1427+
}{
1428+
{
1429+
name: "add an already added file",
1430+
after: []string{"modb"},
1431+
wantViewRoots: []string{"./modb"},
1432+
},
1433+
{
1434+
name: "add an already added file but with an ending slash",
1435+
after: []string{"modb/"},
1436+
wantViewRoots: []string{"./modb"},
1437+
},
1438+
{
1439+
name: "add an already added file and a new file",
1440+
after: []string{"modb", "moda/a"},
1441+
wantViewRoots: []string{"./modb", "moda/a"},
1442+
},
1443+
} {
1444+
t.Run(tt.name, func(t *testing.T) {
1445+
opts := []RunOption{ProxyFiles(workspaceProxy), RootPath("modb"), NoDefaultWorkspaceFiles()}
1446+
WithOptions(opts...).Run(t, multiModule, func(t *testing.T, env *Env) {
1447+
summary := func(typ cache.ViewType, root, folder string) command.View {
1448+
return command.View{
1449+
Type: typ.String(),
1450+
Root: env.Sandbox.Workdir.URI(root),
1451+
Folder: env.Sandbox.Workdir.URI(folder),
1452+
}
1453+
}
1454+
checkViews := func(want ...command.View) {
1455+
got := env.Views()
1456+
if diff := cmp.Diff(want, got, cmpopts.IgnoreFields(command.View{}, "ID")); diff != "" {
1457+
t.Errorf("SummarizeViews() mismatch (-want +got):\n%s", diff)
1458+
}
1459+
}
1460+
var wantViews []command.View
1461+
for _, root := range tt.wantViewRoots {
1462+
wantViews = append(wantViews, summary(cache.GoModView, root, root))
1463+
}
1464+
env.ChangeWorkspaceFolders(tt.after...)
1465+
env.Await(
1466+
LogMatching(protocol.Warning, "skip adding the already added folder", 1, false),
1467+
NoOutstandingWork(IgnoreTelemetryPromptWork),
1468+
)
1469+
checkViews(wantViews...)
1470+
})
1471+
})
1472+
}
1473+
}
1474+
14151475
// Test that non-file scheme Document URIs in ChangeWorkspaceFolders
14161476
// notification does not produce errors.
14171477
func TestChangeNonFileWorkspaceFolders(t *testing.T) {

0 commit comments

Comments
 (0)