Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 5 additions & 6 deletions internal/collections/ordered_map.go
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,11 @@ func DiffOrderedMaps[K comparable, V comparable](m1 *OrderedMap[K, V], m2 *Order
}

func DiffOrderedMapsFunc[K comparable, V any](m1 *OrderedMap[K, V], m2 *OrderedMap[K, V], equalValues func(a, b V) bool, onAdded func(key K, value V), onRemoved func(key K, value V), onModified func(key K, oldValue V, newValue V)) {
for k, v2 := range m2.Entries() {
if _, ok := m1.Get(k); !ok {
onAdded(k, v2)
}
}
for k, v1 := range m1.Entries() {
if v2, ok := m2.Get(k); ok {
if !equalValues(v1, v2) {
Expand All @@ -309,10 +314,4 @@ func DiffOrderedMapsFunc[K comparable, V any](m1 *OrderedMap[K, V], m2 *OrderedM
onRemoved(k, v1)
}
}

for k, v2 := range m2.Entries() {
if _, ok := m1.Get(k); !ok {
onAdded(k, v2)
}
}
}
11 changes: 5 additions & 6 deletions internal/core/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -606,6 +606,11 @@ func DiffMaps[K comparable, V comparable](m1 map[K]V, m2 map[K]V, onAdded func(K
}

func DiffMapsFunc[K comparable, V any](m1 map[K]V, m2 map[K]V, equalValues func(V, V) bool, onAdded func(K, V), onRemoved func(K, V), onChanged func(K, V, V)) {
for k, v2 := range m2 {
if _, ok := m1[k]; !ok {
onAdded(k, v2)
}
}
for k, v1 := range m1 {
if v2, ok := m2[k]; ok {
if !equalValues(v1, v2) {
Expand All @@ -615,12 +620,6 @@ func DiffMapsFunc[K comparable, V any](m1 map[K]V, m2 map[K]V, equalValues func(
onRemoved(k, v1)
}
}

for k, v2 := range m2 {
if _, ok := m1[k]; !ok {
onAdded(k, v2)
}
}
}

// CopyMapInto is maps.Copy, unless dst is nil, in which case it clones and returns src.
Expand Down
21 changes: 20 additions & 1 deletion internal/lsp/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"github.com/microsoft/typescript-go/internal/project"
"github.com/microsoft/typescript-go/internal/project/ata"
"github.com/microsoft/typescript-go/internal/project/logging"
"github.com/microsoft/typescript-go/internal/tspath"
"github.com/microsoft/typescript-go/internal/vfs"
"golang.org/x/sync/errgroup"
"golang.org/x/text/language"
Expand Down Expand Up @@ -651,9 +652,27 @@ func (s *Server) handleInitialized(ctx context.Context, params *lsproto.Initiali
s.watchEnabled = true
}

cwd := s.cwd
if s.initializeParams.Capabilities != nil &&
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Boy do I want to add protobuf-style safe accessors for capabilities

s.initializeParams.Capabilities.Workspace != nil &&
s.initializeParams.Capabilities.Workspace.WorkspaceFolders != nil &&
ptrIsTrue(s.initializeParams.Capabilities.Workspace.WorkspaceFolders) &&
s.initializeParams.WorkspaceFolders != nil &&
s.initializeParams.WorkspaceFolders.WorkspaceFolders != nil &&
len(*s.initializeParams.WorkspaceFolders.WorkspaceFolders) == 1 {
cwd = lsproto.DocumentUri((*s.initializeParams.WorkspaceFolders.WorkspaceFolders)[0].Uri).FileName()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think we should use all the workspace folders here?
#1238 where i had the change to handle this where we could infer project and its setting based on what path it is.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And also need to handle : lsproto.DidChangeWorkspaceFoldersParams:

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it would also help with not looking past certain directory when finding tsconfig etc

Copy link
Member Author

@andrewbranch andrewbranch Oct 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this is definitely incomplete but I want to handle multi-root workspace support all at once in a future PR. In theory, this shouldn’t break watching in other workspace folders, because we always check if the directories we need to watch are inside this directory, and fall back to other locations if not. But I’ve literally never tried tsgo in a multi-root workspace so I have no idea if anything works at all, at the moment.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what i mean is instead of using "cwd" as the deciding factor for watching strategy it would be nice to instead have something else handle that. I would rather now change cwd as part of this and keep that in play when nothing else is there? You can handle single workspace for now (and add multiple places to consider later) off of that option rather than changing cwd.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do think this is kind of overloading the term "cwd" in an incorrect way, so I can rename/add a field to use instead. However, note that the actual process CWD was always / due to the way vscode-languageclient spawns the process. I thought about adding a CWD in the spawn options there, but I didn’t think there was an option that made sense, and we never use it for anything since every path that gets communicated over LSP is an absolute URI.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, the actual cwd of the process should never be used for any reason.

} else if s.initializeParams.RootUri.DocumentUri != nil {
cwd = s.initializeParams.RootUri.DocumentUri.FileName()
} else if s.initializeParams.RootPath != nil && s.initializeParams.RootPath.String != nil {
cwd = *s.initializeParams.RootPath.String
}
if !tspath.PathIsAbsolute(cwd) {
cwd = s.cwd
}

s.session = project.NewSession(&project.SessionInit{
Options: &project.SessionOptions{
CurrentDirectory: s.cwd,
CurrentDirectory: cwd,
DefaultLibraryPath: s.defaultLibraryPath,
TypingsLocation: s.typingsLocation,
PositionEncoding: s.positionEncoding,
Expand Down
6 changes: 5 additions & 1 deletion internal/module/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -1733,7 +1733,11 @@ func (r *resolutionState) readPackageJsonPeerDependencies(packageJsonInfo *packa
r.tracer.write(diagnostics.X_package_json_has_a_peerDependencies_field.Message())
}
packageDirectory := r.realPath(packageJsonInfo.PackageDirectory)
nodeModules := packageDirectory[:strings.LastIndex(packageDirectory, "/node_modules")+len("/node_modules")] + "/"
nodeModulesIndex := strings.LastIndex(packageDirectory, "/node_modules")
if nodeModulesIndex == -1 {
return ""
}
nodeModules := packageDirectory[:nodeModulesIndex+len("/node_modules")] + "/"
builder := strings.Builder{}
for name := range peerDependencies.Value {
peerPackageJson := r.getPackageJsonInfo(nodeModules+name /*onlyRecordFailures*/, false)
Expand Down
2 changes: 1 addition & 1 deletion internal/project/configfileregistry.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ type configFileEntry struct {
// when this is set, no other fields will be used.
retainingConfigs map[tspath.Path]struct{}
// rootFilesWatch is a watch for the root files of this config file.
rootFilesWatch *WatchedFiles[[]string]
rootFilesWatch *WatchedFiles[patternsAndIgnored]
}

func newConfigFileEntry(fileName string) *configFileEntry {
Expand Down
67 changes: 52 additions & 15 deletions internal/project/configfileregistrybuilder.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,21 +165,61 @@ func (c *configFileRegistryBuilder) updateRootFilesWatch(fileName string, entry
return
}

wildcardGlobs := entry.commandLine.WildcardDirectories()
rootFileGlobs := make([]string, 0, len(wildcardGlobs)+1+len(entry.commandLine.ExtendedSourceFiles()))
rootFileGlobs = append(rootFileGlobs, fileName)
for _, extendedConfig := range entry.commandLine.ExtendedSourceFiles() {
rootFileGlobs = append(rootFileGlobs, extendedConfig)
var ignored map[string]struct{}
var globs []string
var externalDirectories []string
var includeWorkspace bool
var includeTsconfigDir bool
tsconfigDir := tspath.GetDirectoryPath(fileName)
wildcardDirectories := entry.commandLine.WildcardDirectories()
comparePathsOptions := tspath.ComparePathsOptions{
CurrentDirectory: c.sessionOptions.CurrentDirectory,
UseCaseSensitiveFileNames: c.FS().UseCaseSensitiveFileNames(),
}
for dir, recursive := range wildcardGlobs {
rootFileGlobs = append(rootFileGlobs, fmt.Sprintf("%s/%s", tspath.NormalizePath(dir), core.IfElse(recursive, recursiveFileGlobPattern, fileGlobPattern)))
for dir := range wildcardDirectories {
if tspath.ContainsPath(c.sessionOptions.CurrentDirectory, dir, comparePathsOptions) {
includeWorkspace = true
} else if tspath.ContainsPath(tsconfigDir, dir, comparePathsOptions) {
includeTsconfigDir = true
} else {
externalDirectories = append(externalDirectories, dir)
}
}
for _, fileName := range entry.commandLine.LiteralFileNames() {
rootFileGlobs = append(rootFileGlobs, fileName)
if tspath.ContainsPath(c.sessionOptions.CurrentDirectory, fileName, comparePathsOptions) {
includeWorkspace = true
} else if tspath.ContainsPath(tsconfigDir, fileName, comparePathsOptions) {
includeTsconfigDir = true
} else {
externalDirectories = append(externalDirectories, tspath.GetDirectoryPath(fileName))
}
}

slices.Sort(rootFileGlobs)
entry.rootFilesWatch = entry.rootFilesWatch.Clone(rootFileGlobs)
if includeWorkspace {
globs = append(globs, getRecursiveGlobPattern(c.sessionOptions.CurrentDirectory))
}
if includeTsconfigDir {
globs = append(globs, getRecursiveGlobPattern(tsconfigDir))
}
for _, fileName := range entry.commandLine.ExtendedSourceFiles() {
if includeWorkspace && tspath.ContainsPath(c.sessionOptions.CurrentDirectory, fileName, comparePathsOptions) {
continue
}
globs = append(globs, fileName)
}
if len(externalDirectories) > 0 {
commonParents, ignoredExternalDirs := tspath.GetCommonParents(externalDirectories, minWatchLocationDepth, getPathComponentsForWatching, comparePathsOptions)
for _, parent := range commonParents {
globs = append(globs, getRecursiveGlobPattern(parent))
}
ignored = ignoredExternalDirs
}

slices.Sort(globs)
entry.rootFilesWatch = entry.rootFilesWatch.Clone(patternsAndIgnored{
patterns: globs,
ignored: ignored,
})
}

// acquireConfigForProject loads a config file entry from the cache, or parses it if not already
Expand Down Expand Up @@ -347,11 +387,8 @@ func (c *configFileRegistryBuilder) DidChangeFiles(summary FileChangeSummary, lo
}
logger.Logf("Checking if any of %d created files match root files for config %s", len(createdFiles), entry.Key())
for _, fileName := range createdFiles {
parsedGlobs := config.rootFilesWatch.ParsedGlobs()
for _, g := range parsedGlobs {
if g.Match(fileName) {
return true
}
if config.commandLine.PossiblyMatchesFileName(fileName) {
return true
}
}
return false
Expand Down
35 changes: 20 additions & 15 deletions internal/project/project.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,10 @@ type Project struct {
// The ID of the snapshot that created the program stored in this project.
ProgramLastUpdate uint64

programFilesWatch *WatchedFiles[patternsAndIgnored]
failedLookupsWatch *WatchedFiles[map[tspath.Path]string]
affectingLocationsWatch *WatchedFiles[map[tspath.Path]string]
typingsFilesWatch *WatchedFiles[map[tspath.Path]string]
typingsDirectoryWatch *WatchedFiles[map[tspath.Path]string]
typingsWatch *WatchedFiles[patternsAndIgnored]

checkerPool *checkerPool

Expand Down Expand Up @@ -146,26 +146,26 @@ func NewProject(

project.configFilePath = tspath.ToPath(configFileName, currentDirectory, builder.fs.fs.UseCaseSensitiveFileNames())
if builder.sessionOptions.WatchEnabled {
project.programFilesWatch = NewWatchedFiles(
"non-root program files for "+configFileName,
lsproto.WatchKindCreate|lsproto.WatchKindChange|lsproto.WatchKindDelete,
core.Identity,
)
project.failedLookupsWatch = NewWatchedFiles(
"failed lookups for "+configFileName,
lsproto.WatchKindCreate,
createResolutionLookupGlobMapper(project.currentDirectory, builder.fs.fs.UseCaseSensitiveFileNames()),
createResolutionLookupGlobMapper(builder.sessionOptions.CurrentDirectory, builder.sessionOptions.DefaultLibraryPath, project.currentDirectory, builder.fs.fs.UseCaseSensitiveFileNames()),
)
project.affectingLocationsWatch = NewWatchedFiles(
"affecting locations for "+configFileName,
lsproto.WatchKindCreate|lsproto.WatchKindChange|lsproto.WatchKindDelete,
createResolutionLookupGlobMapper(project.currentDirectory, builder.fs.fs.UseCaseSensitiveFileNames()),
createResolutionLookupGlobMapper(builder.sessionOptions.CurrentDirectory, builder.sessionOptions.DefaultLibraryPath, project.currentDirectory, builder.fs.fs.UseCaseSensitiveFileNames()),
)
if builder.sessionOptions.TypingsLocation != "" {
project.typingsFilesWatch = NewWatchedFiles(
project.typingsWatch = NewWatchedFiles(
"typings installer files",
lsproto.WatchKindCreate|lsproto.WatchKindChange|lsproto.WatchKindDelete,
globMapperForTypingsInstaller,
)
project.typingsDirectoryWatch = NewWatchedFiles(
"typings installer directories",
lsproto.WatchKindCreate|lsproto.WatchKindDelete,
globMapperForTypingsInstaller,
core.Identity,
)
}
}
Expand Down Expand Up @@ -221,10 +221,10 @@ func (p *Project) Clone() *Project {
ProgramUpdateKind: ProgramUpdateKindNone,
ProgramLastUpdate: p.ProgramLastUpdate,

programFilesWatch: p.programFilesWatch,
failedLookupsWatch: p.failedLookupsWatch,
affectingLocationsWatch: p.affectingLocationsWatch,
typingsFilesWatch: p.typingsFilesWatch,
typingsDirectoryWatch: p.typingsDirectoryWatch,
typingsWatch: p.typingsWatch,

checkerPool: p.checkerPool,

Expand Down Expand Up @@ -327,14 +327,19 @@ func (p *Project) CreateProgram() CreateProgramResult {
}
}

func (p *Project) CloneWatchers() (failedLookupsWatch *WatchedFiles[map[tspath.Path]string], affectingLocationsWatch *WatchedFiles[map[tspath.Path]string]) {
func (p *Project) CloneWatchers(workspaceDir string, libDir string) (programFilesWatch *WatchedFiles[patternsAndIgnored], failedLookupsWatch *WatchedFiles[map[tspath.Path]string], affectingLocationsWatch *WatchedFiles[map[tspath.Path]string]) {
failedLookups := make(map[tspath.Path]string)
affectingLocations := make(map[tspath.Path]string)
programFiles := getNonRootFileGlobs(workspaceDir, libDir, p.Program.GetSourceFiles(), p.CommandLine.FileNamesByPath(), tspath.ComparePathsOptions{
UseCaseSensitiveFileNames: p.host.FS().UseCaseSensitiveFileNames(),
CurrentDirectory: p.currentDirectory,
})
extractLookups(p.toPath, failedLookups, affectingLocations, p.Program.GetResolvedModules())
extractLookups(p.toPath, failedLookups, affectingLocations, p.Program.GetResolvedTypeReferenceDirectives())
programFilesWatch = p.programFilesWatch.Clone(programFiles)
failedLookupsWatch = p.failedLookupsWatch.Clone(failedLookups)
affectingLocationsWatch = p.affectingLocationsWatch.Clone(affectingLocations)
return failedLookupsWatch, affectingLocationsWatch
return programFilesWatch, failedLookupsWatch, affectingLocationsWatch
}

func (p *Project) log(msg string) {
Expand Down
11 changes: 6 additions & 5 deletions internal/project/projectcollectionbuilder.go
Original file line number Diff line number Diff line change
Expand Up @@ -343,14 +343,14 @@ func (b *projectCollectionBuilder) DidUpdateATAState(ataChanges map[tspath.Path]
// the set of typings files is actually different.
p.installedTypingsInfo = ataChange.TypingsInfo
p.typingsFiles = ataChange.TypingsFiles
fileWatchGlobs, directoryWatchGlobs := getTypingsLocationsGlobs(
typingsWatchGlobs := getTypingsLocationsGlobs(
ataChange.TypingsFilesToWatch,
b.sessionOptions.TypingsLocation,
b.sessionOptions.CurrentDirectory,
p.currentDirectory,
b.fs.fs.UseCaseSensitiveFileNames(),
)
p.typingsFilesWatch = p.typingsFilesWatch.Clone(fileWatchGlobs)
p.typingsDirectoryWatch = p.typingsDirectoryWatch.Clone(directoryWatchGlobs)
p.typingsWatch = p.typingsWatch.Clone(typingsWatchGlobs)
p.dirty = true
p.dirtyFilePath = ""
},
Expand Down Expand Up @@ -535,7 +535,7 @@ func (b *projectCollectionBuilder) findOrCreateDefaultConfiguredProjectWorker(
// For composite projects, we can get an early negative result.
// !!! what about declaration files in node_modules? wouldn't it be better to
// check project inclusion if the project is already loaded?
if !config.MatchesFileName(fileName) {
if _, ok := config.FileNamesByPath()[path]; !ok {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was a drive-by fix, but MatchesFileName is a more expensive function that speculates about files that may not exist; the usage here was for a file that already exists and is either properly included by the config or not.

node.logger.Log("Project does not contain file (by composite config inclusion)")
return false, false
}
Expand Down Expand Up @@ -793,7 +793,8 @@ func (b *projectCollectionBuilder) updateProgram(entry dirty.Value[*Project], lo
if result.UpdateKind == ProgramUpdateKindNewFiles {
filesChanged = true
if b.sessionOptions.WatchEnabled {
failedLookupsWatch, affectingLocationsWatch := project.CloneWatchers()
programFilesWatch, failedLookupsWatch, affectingLocationsWatch := project.CloneWatchers(b.sessionOptions.CurrentDirectory, b.sessionOptions.DefaultLibraryPath)
project.programFilesWatch = programFilesWatch
project.failedLookupsWatch = failedLookupsWatch
project.affectingLocationsWatch = affectingLocationsWatch
}
Expand Down
10 changes: 5 additions & 5 deletions internal/project/projectlifetime_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ func TestProjectLifetime(t *testing.T) {
assert.Equal(t, len(snapshot.ProjectCollection.Projects()), 2)
assert.Assert(t, snapshot.ProjectCollection.ConfiguredProject(tspath.Path("/home/projects/ts/p1/tsconfig.json")) != nil)
assert.Assert(t, snapshot.ProjectCollection.ConfiguredProject(tspath.Path("/home/projects/ts/p2/tsconfig.json")) != nil)
assert.Equal(t, len(utils.Client().WatchFilesCalls()), 2)
assert.Equal(t, len(utils.Client().WatchFilesCalls()), 1)
assert.Assert(t, snapshot.ConfigFileRegistry.GetConfig(tspath.Path("/home/projects/ts/p1/tsconfig.json")) != nil)
assert.Assert(t, snapshot.ConfigFileRegistry.GetConfig(tspath.Path("/home/projects/ts/p2/tsconfig.json")) != nil)

Expand All @@ -89,8 +89,8 @@ func TestProjectLifetime(t *testing.T) {
assert.Assert(t, snapshot.ConfigFileRegistry.GetConfig(tspath.Path("/home/projects/ts/p1/tsconfig.json")) == nil)
assert.Assert(t, snapshot.ConfigFileRegistry.GetConfig(tspath.Path("/home/projects/ts/p2/tsconfig.json")) != nil)
assert.Assert(t, snapshot.ConfigFileRegistry.GetConfig(tspath.Path("/home/projects/ts/p3/tsconfig.json")) != nil)
assert.Equal(t, len(utils.Client().WatchFilesCalls()), 3)
assert.Equal(t, len(utils.Client().UnwatchFilesCalls()), 1)
assert.Equal(t, len(utils.Client().WatchFilesCalls()), 1)
assert.Equal(t, len(utils.Client().UnwatchFilesCalls()), 0)

// Close p2 and p3 files, open p1 file again
session.DidCloseFile(context.Background(), uri2)
Expand All @@ -105,8 +105,8 @@ func TestProjectLifetime(t *testing.T) {
assert.Assert(t, snapshot.ConfigFileRegistry.GetConfig(tspath.Path("/home/projects/ts/p1/tsconfig.json")) != nil)
assert.Assert(t, snapshot.ConfigFileRegistry.GetConfig(tspath.Path("/home/projects/ts/p2/tsconfig.json")) == nil)
assert.Assert(t, snapshot.ConfigFileRegistry.GetConfig(tspath.Path("/home/projects/ts/p3/tsconfig.json")) == nil)
assert.Equal(t, len(utils.Client().WatchFilesCalls()), 4)
assert.Equal(t, len(utils.Client().UnwatchFilesCalls()), 3)
assert.Equal(t, len(utils.Client().WatchFilesCalls()), 1)
assert.Equal(t, len(utils.Client().UnwatchFilesCalls()), 0)
})

t.Run("unrooted inferred projects", func(t *testing.T) {
Expand Down
Loading