diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go index b691ab3063..d916de3c35 100644 --- a/internal/lsp/cache/snapshot.go +++ b/internal/lsp/cache/snapshot.go @@ -694,7 +694,7 @@ func (s *snapshot) CachedImportPaths(ctx context.Context) (map[string]source.Pac func (s *snapshot) GoModForFile(ctx context.Context, uri span.URI) span.URI { var match span.URI for modURI := range s.workspace.activeModFiles() { - if !isSubdirectory(dirURI(modURI).Filename(), uri.Filename()) { + if !inDir(dirURI(modURI).Filename(), uri.Filename()) { continue } if len(modURI) > len(match) { @@ -1377,7 +1377,7 @@ func (s *snapshot) shouldInvalidateMetadata(ctx context.Context, newSnapshot *sn } // If a go.mod in the workspace has been changed, invalidate metadata. if kind := originalFH.Kind(); kind == source.Mod { - return isSubdirectory(filepath.Dir(s.view.rootURI.Filename()), filepath.Dir(originalFH.URI().Filename())) + return inDir(filepath.Dir(s.view.rootURI.Filename()), filepath.Dir(originalFH.URI().Filename())) } // Get the original and current parsed files in order to check package name // and imports. Use the new snapshot to parse to avoid modifying the diff --git a/internal/lsp/cache/view.go b/internal/lsp/cache/view.go index 84a5499efb..f27d8ca1af 100644 --- a/internal/lsp/cache/view.go +++ b/internal/lsp/cache/view.go @@ -698,16 +698,85 @@ func validBuildConfiguration(folder span.URI, ws *workspaceInformation, modFiles // The user may have a multiple directories in their GOPATH. // Check if the workspace is within any of them. for _, gp := range filepath.SplitList(ws.gopath) { - if isSubdirectory(filepath.Join(gp, "src"), folder.Filename()) { + if inDir(filepath.Join(gp, "src"), folder.Filename()) { return true } } return false } -func isSubdirectory(root, leaf string) bool { - rel, err := filepath.Rel(root, leaf) - return err == nil && !strings.HasPrefix(rel, "..") +// Copied and slightly adjusted from go/src/cmd/go/internal/search/search.go. +// +// inDir checks whether path is in the file tree rooted at dir. +// If so, InDir returns an equivalent path relative to dir. +// If not, InDir returns an empty string. +// InDir makes some effort to succeed even in the presence of symbolic links. +func inDir(dir, path string) bool { + if rel := inDirLex(path, dir); rel != "" { + return true + } + xpath, err := filepath.EvalSymlinks(path) + if err != nil || xpath == path { + xpath = "" + } else { + if rel := inDirLex(xpath, dir); rel != "" { + return true + } + } + + xdir, err := filepath.EvalSymlinks(dir) + if err == nil && xdir != dir { + if rel := inDirLex(path, xdir); rel != "" { + return true + } + if xpath != "" { + if rel := inDirLex(xpath, xdir); rel != "" { + return true + } + } + } + return false +} + +// Copied from go/src/cmd/go/internal/search/search.go. +// +// inDirLex is like inDir but only checks the lexical form of the file names. +// It does not consider symbolic links. +// TODO(rsc): This is a copy of str.HasFilePathPrefix, modified to +// return the suffix. Most uses of str.HasFilePathPrefix should probably +// be calling InDir instead. +func inDirLex(path, dir string) string { + pv := strings.ToUpper(filepath.VolumeName(path)) + dv := strings.ToUpper(filepath.VolumeName(dir)) + path = path[len(pv):] + dir = dir[len(dv):] + switch { + default: + return "" + case pv != dv: + return "" + case len(path) == len(dir): + if path == dir { + return "." + } + return "" + case dir == "": + return path + case len(path) > len(dir): + if dir[len(dir)-1] == filepath.Separator { + if path[:len(dir)] == dir { + return path[len(dir):] + } + return "" + } + if path[len(dir)] == filepath.Separator && path[:len(dir)] == dir { + if len(path) == len(dir)+1 { + return "." + } + return path[len(dir)+1:] + } + return "" + } } // getGoEnv gets the view's various GO* values. diff --git a/internal/lsp/cache/workspace.go b/internal/lsp/cache/workspace.go index 71d3842329..673843a57a 100644 --- a/internal/lsp/cache/workspace.go +++ b/internal/lsp/cache/workspace.go @@ -258,7 +258,7 @@ func (wm *workspace) invalidate(ctx context.Context, changes map[span.URI]*fileC // Legacy mode only considers a module a workspace root. continue } - if !isSubdirectory(wm.root.Filename(), uri.Filename()) { + if !inDir(wm.root.Filename(), uri.Filename()) { // Otherwise, the module must be contained within the workspace root. continue }