-
Notifications
You must be signed in to change notification settings - Fork 640
Watch with way fewer globs in LSP #971
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
b2e8c39
e575309
e81acb6
a796802
b980896
c713119
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,9 +2,13 @@ package project | |
|
||
import ( | ||
"context" | ||
"fmt" | ||
"slices" | ||
"strings" | ||
"time" | ||
|
||
"github.com/microsoft/typescript-go/internal/lsp/lsproto" | ||
"github.com/microsoft/typescript-go/internal/tspath" | ||
) | ||
|
||
const ( | ||
|
@@ -60,3 +64,263 @@ func (w *watchedFiles[T]) update(ctx context.Context, newData T) (updated bool, | |
w.watcherID = watcherID | ||
return true, nil | ||
} | ||
|
||
func createGlobMapper(host ProjectHost) func(data map[tspath.Path]string) []string { | ||
rootDir := host.GetCurrentDirectory() | ||
rootPath := tspath.ToPath(rootDir, "", host.FS().UseCaseSensitiveFileNames()) | ||
rootPathComponents := tspath.GetPathComponents(string(rootPath), "") | ||
isRootWatchable := canWatchDirectoryOrFile(rootPathComponents) | ||
|
||
return func(data map[tspath.Path]string) []string { | ||
start := time.Now() | ||
|
||
// dir -> recursive | ||
globSet := make(map[string]bool) | ||
|
||
for path, fileName := range data { | ||
w := getDirectoryToWatchFailedLookupLocation( | ||
fileName, | ||
path, | ||
rootDir, | ||
rootPath, | ||
rootPathComponents, | ||
isRootWatchable, | ||
rootDir, | ||
true, | ||
) | ||
if w == nil { | ||
continue | ||
} | ||
globSet[w.dir] = globSet[w.dir] || !w.nonRecursive | ||
} | ||
|
||
globs := make([]string, 0, len(globSet)) | ||
for dir, recursive := range globSet { | ||
if recursive { | ||
globs = append(globs, dir+"/"+recursiveFileGlobPattern) | ||
} else { | ||
globs = append(globs, dir+"/"+fileGlobPattern) | ||
} | ||
} | ||
slices.Sort(globs) | ||
|
||
timeTaken := time.Since(start) | ||
host.Log(fmt.Sprintf("createGlobMapper took %s to create %d globs for %d failed lookups", timeTaken, len(globs), len(data))) | ||
return globs | ||
} | ||
} | ||
|
||
type directoryOfFailedLookupWatch struct { | ||
dir string | ||
dirPath tspath.Path | ||
nonRecursive bool | ||
packageDir *string | ||
packageDirPath *tspath.Path | ||
} | ||
|
||
func getDirectoryToWatchFailedLookupLocation( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a pure port, basically line for line. I made no attempts to make it go any faster. |
||
failedLookupLocation string, | ||
failedLookupLocationPath tspath.Path, | ||
rootDir string, | ||
rootPath tspath.Path, | ||
rootPathComponents []string, | ||
isRootWatchable bool, | ||
currentDirectory string, | ||
preferNonRecursiveWatch bool, | ||
) *directoryOfFailedLookupWatch { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Strada has baselines on what to watch : https://github.com/microsoft/TypeScript/blob/main/tests/baselines/reference/canWatch/getDirectoryToWatchFailedLookupLocationIndirDos.baseline.md like these. do we want these atleast till we have this logic and dont make this any faster. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can do that, though I'm wary of continuing to have these sorts of super large baselines. |
||
failedLookupPathComponents := tspath.GetPathComponents(string(failedLookupLocationPath), "") | ||
// Ensure failed look up is normalized path | ||
// !!! needed? | ||
if tspath.IsRootedDiskPath(failedLookupLocation) { | ||
failedLookupLocation = tspath.NormalizePath(failedLookupLocation) | ||
} else { | ||
failedLookupLocation = tspath.GetNormalizedAbsolutePath(failedLookupLocation, currentDirectory) | ||
} | ||
failedLookupComponents := tspath.GetPathComponents(failedLookupLocation, "") | ||
perceivedOsRootLength := perceivedOsRootLengthForWatching(failedLookupPathComponents, len(failedLookupPathComponents)) | ||
if len(failedLookupPathComponents) <= perceivedOsRootLength+1 { | ||
return nil | ||
} | ||
// If directory path contains node module, get the most parent node_modules directory for watching | ||
nodeModulesIndex := slices.Index(failedLookupPathComponents, "node_modules") | ||
if nodeModulesIndex != -1 && nodeModulesIndex+1 <= perceivedOsRootLength+1 { | ||
return nil | ||
} | ||
lastNodeModulesIndex := lastIndex(failedLookupPathComponents, "node_modules") | ||
if isRootWatchable && isInDirectoryPath(rootPathComponents, failedLookupPathComponents) { | ||
if len(failedLookupPathComponents) > len(rootPathComponents)+1 { | ||
// Instead of watching root, watch directory in root to avoid watching excluded directories not needed for module resolution | ||
return getDirectoryOfFailedLookupWatch( | ||
failedLookupComponents, | ||
failedLookupPathComponents, | ||
max(len(rootPathComponents)+1, perceivedOsRootLength+1), | ||
lastNodeModulesIndex, | ||
false, | ||
) | ||
} else { | ||
// Always watch root directory non recursively | ||
return &directoryOfFailedLookupWatch{ | ||
dir: rootDir, | ||
dirPath: rootPath, | ||
nonRecursive: true, | ||
} | ||
} | ||
} | ||
|
||
return getDirectoryToWatchFromFailedLookupLocationDirectory( | ||
failedLookupComponents, | ||
failedLookupPathComponents, | ||
len(failedLookupPathComponents)-1, | ||
perceivedOsRootLength, | ||
nodeModulesIndex, | ||
rootPathComponents, | ||
lastNodeModulesIndex, | ||
preferNonRecursiveWatch, | ||
) | ||
} | ||
|
||
func getDirectoryToWatchFromFailedLookupLocationDirectory( | ||
dirComponents []string, | ||
dirPathComponents []string, | ||
dirPathComponentsLength int, | ||
perceivedOsRootLength int, | ||
nodeModulesIndex int, | ||
rootPathComponents []string, | ||
lastNodeModulesIndex int, | ||
preferNonRecursiveWatch bool, | ||
) *directoryOfFailedLookupWatch { | ||
// If directory path contains node module, get the most parent node_modules directory for watching | ||
if nodeModulesIndex != -1 { | ||
// If the directory is node_modules use it to watch, always watch it recursively | ||
return getDirectoryOfFailedLookupWatch( | ||
dirComponents, | ||
dirPathComponents, | ||
nodeModulesIndex+1, | ||
lastNodeModulesIndex, | ||
false, | ||
) | ||
} | ||
|
||
// Use some ancestor of the root directory | ||
nonRecursive := true | ||
length := dirPathComponentsLength | ||
if !preferNonRecursiveWatch { | ||
for i := range dirPathComponentsLength { | ||
jakebailey marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if dirPathComponents[i] != rootPathComponents[i] { | ||
nonRecursive = false | ||
length = max(i+1, perceivedOsRootLength+1) | ||
break | ||
} | ||
} | ||
} | ||
return getDirectoryOfFailedLookupWatch( | ||
dirComponents, | ||
dirPathComponents, | ||
length, | ||
lastNodeModulesIndex, | ||
nonRecursive, | ||
) | ||
} | ||
|
||
func getDirectoryOfFailedLookupWatch( | ||
dirComponents []string, | ||
dirPathComponents []string, | ||
length int, | ||
lastNodeModulesIndex int, | ||
nonRecursive bool, | ||
) *directoryOfFailedLookupWatch { | ||
packageDirLength := -1 | ||
if lastNodeModulesIndex != -1 && lastNodeModulesIndex+1 >= length && lastNodeModulesIndex+2 < len(dirPathComponents) { | ||
if !strings.HasPrefix(dirPathComponents[lastNodeModulesIndex+1], "@") { | ||
packageDirLength = lastNodeModulesIndex + 2 | ||
} else if lastNodeModulesIndex+3 < len(dirPathComponents) { | ||
packageDirLength = lastNodeModulesIndex + 3 | ||
} | ||
} | ||
var packageDir *string | ||
var packageDirPath *tspath.Path | ||
if packageDirLength != -1 { | ||
packageDir = ptrTo(tspath.GetPathFromPathComponents(dirPathComponents[:packageDirLength])) | ||
packageDirPath = ptrTo(tspath.Path(tspath.GetPathFromPathComponents(dirComponents[:packageDirLength]))) | ||
} | ||
|
||
return &directoryOfFailedLookupWatch{ | ||
dir: tspath.GetPathFromPathComponents(dirComponents[:length]), | ||
dirPath: tspath.Path(tspath.GetPathFromPathComponents(dirPathComponents[:length])), | ||
nonRecursive: nonRecursive, | ||
packageDir: packageDir, | ||
packageDirPath: packageDirPath, | ||
} | ||
} | ||
|
||
func perceivedOsRootLengthForWatching(pathComponents []string, length int) int { | ||
// Ignore "/", "c:/" | ||
if length <= 1 { | ||
return 1 | ||
} | ||
indexAfterOsRoot := 1 | ||
firstComponent := pathComponents[0] | ||
isDosStyle := len(firstComponent) >= 2 && tspath.IsVolumeCharacter(firstComponent[0]) && firstComponent[1] == ':' | ||
if firstComponent != "/" && !isDosStyle && isDosStyleNextPart(pathComponents[1]) { | ||
// ignore "//vda1cs4850/c$/folderAtRoot" | ||
if length == 2 { | ||
return 2 | ||
} | ||
indexAfterOsRoot = 2 | ||
isDosStyle = true | ||
} | ||
|
||
afterOsRoot := pathComponents[indexAfterOsRoot] | ||
if isDosStyle && !strings.EqualFold(afterOsRoot, "users") { | ||
// Paths like c:/notUsers | ||
return indexAfterOsRoot | ||
} | ||
|
||
if strings.EqualFold(afterOsRoot, "workspaces") { | ||
// Paths like: /workspaces as codespaces hoist the repos in /workspaces so we have to exempt these from "2" level from root rule | ||
return indexAfterOsRoot + 1 | ||
} | ||
|
||
// Paths like: c:/users/username or /home/username | ||
return indexAfterOsRoot + 2 | ||
} | ||
|
||
func canWatchDirectoryOrFile(pathComponents []string) bool { | ||
length := len(pathComponents) | ||
// Ignore "/", "c:/" | ||
// ignore "/user", "c:/users" or "c:/folderAtRoot" | ||
if length < 2 { | ||
return false | ||
} | ||
perceivedOsRootLength := perceivedOsRootLengthForWatching(pathComponents, length) | ||
return length > perceivedOsRootLength+1 | ||
} | ||
|
||
func isDosStyleNextPart(part string) bool { | ||
return len(part) == 2 && tspath.IsVolumeCharacter(part[0]) && part[1] == '$' | ||
} | ||
|
||
func lastIndex[T comparable](s []T, v T) int { | ||
for i := len(s) - 1; i >= 0; i-- { | ||
if s[i] == v { | ||
return i | ||
} | ||
} | ||
return -1 | ||
} | ||
|
||
func isInDirectoryPath(dirComponents []string, fileOrDirComponents []string) bool { | ||
if len(fileOrDirComponents) < len(dirComponents) { | ||
return false | ||
} | ||
for i := range dirComponents { | ||
if dirComponents[i] != fileOrDirComponents[i] { | ||
return false | ||
} | ||
} | ||
return true | ||
} | ||
|
||
func ptrTo[T any](v T) *T { | ||
return &v | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In strada there is code to watch "packageDir" if it exits and dir otherwise so we can handle the symlinks .. a note here to handle package symlinks later would be good
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I should have added that, yeah. Will follow up with that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was dogfooding
tsgo
so couldn't find all refs on it; is it justpackageDir ?? dir
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
its packageDir if packageDir exists and is symlink otherwise dir. Its in
createDirectoryWatcherForPackageDir
in strada. Its little complicated with having to manage state but since you dont need to, it should make it easier.