From cc9597ad66b8fedc7e04ed4d7776879b2d3acb3d Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Tue, 8 Apr 2025 17:01:34 -0700 Subject: [PATCH 01/16] Refresh open file diagnostics on config file change --- internal/api/api.go | 5 + internal/lsp/lsproto/jsonrpc.go | 22 +++- internal/lsp/server.go | 115 +++++++++++++++++- internal/project/host.go | 15 ++- internal/project/project.go | 61 +++++++++- internal/project/service.go | 85 ++++++++++++- .../projecttestutil/projecttestutil.go | 5 + 7 files changed, 296 insertions(+), 12 deletions(-) diff --git a/internal/api/api.go b/internal/api/api.go index 6b04c741aa..9e1391e8d6 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -119,6 +119,11 @@ func (api *API) PositionEncoding() lsproto.PositionEncodingKind { return lsproto.PositionEncodingKindUTF8 } +// Client implements ProjectHost. +func (api *API) Client() project.Client { + return nil +} + func (api *API) HandleRequest(id int, method string, payload []byte) ([]byte, error) { params, err := unmarshalPayload(method, payload) if err != nil { diff --git a/internal/lsp/lsproto/jsonrpc.go b/internal/lsp/lsproto/jsonrpc.go index 90763c4991..af9626082c 100644 --- a/internal/lsp/lsproto/jsonrpc.go +++ b/internal/lsp/lsproto/jsonrpc.go @@ -28,6 +28,10 @@ type ID struct { int int32 } +func NewIDString(str string) *ID { + return &ID{str: str} +} + func (id *ID) MarshalJSON() ([]byte, error) { if id.str != "" { return json.Marshal(id.str) @@ -43,6 +47,13 @@ func (id *ID) UnmarshalJSON(data []byte) error { return json.Unmarshal(data, &id.int) } +func (id *ID) TryInt() (int32, bool) { + if id == nil || id.str != "" { + return 0, false + } + return id.int, true +} + func (id *ID) MustInt() int32 { if id.str != "" { panic("ID is not an integer") @@ -54,11 +65,20 @@ func (id *ID) MustInt() int32 { type RequestMessage struct { JSONRPC JSONRPCVersion `json:"jsonrpc"` - ID *ID `json:"id"` + ID *ID `json:"id,omitempty"` Method Method `json:"method"` Params any `json:"params"` } +func NewRequestMessage(method Method, id *ID, params any) *RequestMessage { + return &RequestMessage{ + JSONRPC: JSONRPCVersion{}, + ID: id, + Method: method, + Params: params, + } +} + func (r *RequestMessage) UnmarshalJSON(data []byte) error { var raw struct { JSONRPC JSONRPCVersion `json:"jsonrpc"` diff --git a/internal/lsp/server.go b/internal/lsp/server.go index 14f2957cbc..c633b013bf 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -40,10 +40,12 @@ func NewServer(opts *ServerOptions) *Server { newLine: opts.NewLine, fs: opts.FS, defaultLibraryPath: opts.DefaultLibraryPath, + watchers: make(map[project.WatcherHandle]struct{}), } } var _ project.ServiceHost = (*Server)(nil) +var _ project.Client = (*Server)(nil) type Server struct { r *lsproto.BaseReader @@ -51,6 +53,7 @@ type Server struct { stderr io.Writer + clientSeq int32 requestMethod string requestTime time.Time @@ -62,36 +65,95 @@ type Server struct { initializeParams *lsproto.InitializeParams positionEncoding lsproto.PositionEncodingKind + watcheEnabled bool + watcherID int + watchers map[project.WatcherHandle]struct{} logger *project.Logger projectService *project.Service converters *ls.Converters } -// FS implements project.ProjectServiceHost. +// FS implements project.ServiceHost. func (s *Server) FS() vfs.FS { return s.fs } -// DefaultLibraryPath implements project.ProjectServiceHost. +// DefaultLibraryPath implements project.ServiceHost. func (s *Server) DefaultLibraryPath() string { return s.defaultLibraryPath } -// GetCurrentDirectory implements project.ProjectServiceHost. +// GetCurrentDirectory implements project.ServiceHost. func (s *Server) GetCurrentDirectory() string { return s.cwd } -// NewLine implements project.ProjectServiceHost. +// NewLine implements project.ServiceHost. func (s *Server) NewLine() string { return s.newLine.GetNewLineCharacter() } -// Trace implements project.ProjectServiceHost. +// Trace implements project.ServiceHost. func (s *Server) Trace(msg string) { s.Log(msg) } +// Client implements project.ServiceHost. +func (s *Server) Client() project.Client { + if !s.watcheEnabled { + return nil + } + return s +} + +// WatchFiles implements project.Client. +func (s *Server) WatchFiles(watchers []lsproto.FileSystemWatcher) (project.WatcherHandle, error) { + watcherId := fmt.Sprintf("watcher-%d", s.watcherID) + if err := s.sendRequest(lsproto.MethodClientRegisterCapability, &lsproto.RegistrationParams{ + Registrations: []lsproto.Registration{ + { + Id: watcherId, + Method: string(lsproto.MethodWorkspaceDidChangeWatchedFiles), + RegisterOptions: ptrTo(lsproto.LSPAny(lsproto.DidChangeWatchedFilesRegistrationOptions{ + Watchers: watchers, + })), + }, + }, + }); err != nil { + return "", fmt.Errorf("failed to register file watcher: %w", err) + } + + handle := project.WatcherHandle(watcherId) + s.watchers[handle] = struct{}{} + s.watcherID++ + return handle, nil +} + +// UnwatchFiles implements project.Client. +func (s *Server) UnwatchFiles(handle project.WatcherHandle) error { + if _, ok := s.watchers[handle]; ok { + if err := s.sendRequest(lsproto.MethodClientUnregisterCapability, &lsproto.UnregistrationParams{ + Unregisterations: []lsproto.Unregistration{ + { + Id: string(handle), + Method: string(lsproto.MethodWorkspaceDidChangeWatchedFiles), + }, + }, + }); err != nil { + return fmt.Errorf("failed to unregister file watcher: %w", err) + } + delete(s.watchers, handle) + return nil + } + + return fmt.Errorf("no file watcher exists with ID %s", handle) +} + +// PublishDiagnostics implements project.Client. +func (s *Server) PublishDiagnostics(params *lsproto.PublishDiagnosticsParams) error { + return s.sendNotification(lsproto.MethodTextDocumentPublishDiagnostics, params) +} + func (s *Server) Run() error { for { req, err := s.read() @@ -105,6 +167,11 @@ func (s *Server) Run() error { return err } + // TODO: handle response messages + if req == nil { + continue + } + if s.initializeParams == nil { if req.Method == lsproto.MethodInitialize { if err := s.handleInitialize(req); err != nil { @@ -132,12 +199,37 @@ func (s *Server) read() (*lsproto.RequestMessage, error) { req := &lsproto.RequestMessage{} if err := json.Unmarshal(data, req); err != nil { + res := &lsproto.ResponseMessage{} + if err := json.Unmarshal(data, res); err == nil { + // !!! TODO: handle response + return nil, nil + } return nil, fmt.Errorf("%w: %w", lsproto.ErrInvalidRequest, err) } return req, nil } +func (s *Server) sendRequest(method lsproto.Method, params any) error { + s.clientSeq++ + id := lsproto.NewIDString(fmt.Sprintf("ts%d", s.clientSeq)) + req := lsproto.NewRequestMessage(method, id, params) + data, err := json.Marshal(req) + if err != nil { + return err + } + return s.w.Write(data) +} + +func (s *Server) sendNotification(method lsproto.Method, params any) error { + req := lsproto.NewRequestMessage(method, nil /*id*/, params) + data, err := json.Marshal(req) + if err != nil { + return err + } + return s.w.Write(data) +} + func (s *Server) sendResult(id *lsproto.ID, result any) error { return s.sendResponse(&lsproto.ResponseMessage{ ID: id, @@ -189,6 +281,8 @@ func (s *Server) handleMessage(req *lsproto.RequestMessage) error { return s.handleDidSave(req) case *lsproto.DidCloseTextDocumentParams: return s.handleDidClose(req) + case *lsproto.DidChangeWatchedFilesParams: + return s.handleDidChangeWatchedFiles(req) case *lsproto.DocumentDiagnosticParams: return s.handleDocumentDiagnostic(req) case *lsproto.HoverParams: @@ -262,9 +356,14 @@ func (s *Server) handleInitialize(req *lsproto.RequestMessage) error { } func (s *Server) handleInitialized(req *lsproto.RequestMessage) error { + if s.initializeParams.Capabilities.Workspace.DidChangeWatchedFiles != nil && *s.initializeParams.Capabilities.Workspace.DidChangeWatchedFiles.DynamicRegistration { + s.watcheEnabled = true + } + s.logger = project.NewLogger([]io.Writer{s.stderr}, "" /*file*/, project.LogLevelVerbose) s.projectService = project.NewService(s, project.ServiceOptions{ Logger: s.logger, + WatchEnabled: s.watcheEnabled, PositionEncoding: s.positionEncoding, }) @@ -322,6 +421,12 @@ func (s *Server) handleDidClose(req *lsproto.RequestMessage) error { return nil } +func (s *Server) handleDidChangeWatchedFiles(req *lsproto.RequestMessage) error { + params := req.Params.(*lsproto.DidChangeWatchedFilesParams) + s.projectService.OnWatchedFilesChanged(params.Changes) + return nil +} + func (s *Server) handleDocumentDiagnostic(req *lsproto.RequestMessage) error { params := req.Params.(*lsproto.DocumentDiagnosticParams) file, project := s.getFileAndProject(params.TextDocument.Uri) diff --git a/internal/project/host.go b/internal/project/host.go index 9c8843c0e3..baf8b512f5 100644 --- a/internal/project/host.go +++ b/internal/project/host.go @@ -1,10 +1,23 @@ package project -import "github.com/microsoft/typescript-go/internal/vfs" +import ( + "github.com/microsoft/typescript-go/internal/lsp/lsproto" + "github.com/microsoft/typescript-go/internal/vfs" +) + +type WatcherHandle string + +type Client interface { + WatchFiles(watchers []lsproto.FileSystemWatcher) (WatcherHandle, error) + UnwatchFiles(handle WatcherHandle) error + PublishDiagnostics(params *lsproto.PublishDiagnosticsParams) error +} type ServiceHost interface { FS() vfs.FS DefaultLibraryPath() string GetCurrentDirectory() string NewLine() string + + Client() Client } diff --git a/internal/project/project.go b/internal/project/project.go index 3aa4e4751e..fae794661c 100644 --- a/internal/project/project.go +++ b/internal/project/project.go @@ -5,6 +5,8 @@ import ( "strings" "sync" + "slices" + "github.com/microsoft/typescript-go/internal/ast" "github.com/microsoft/typescript-go/internal/collections" "github.com/microsoft/typescript-go/internal/compiler" @@ -41,6 +43,8 @@ type ProjectHost interface { OnDiscoveredSymlink(info *ScriptInfo) Log(s string) PositionEncoding() lsproto.PositionEncodingKind + + Client() Client } type Project struct { @@ -56,7 +60,7 @@ type Project struct { hasAddedOrRemovedFiles bool hasAddedOrRemovedSymlinks bool deferredClose bool - reloadConfig bool + pendingConfigReload bool currentDirectory string // Inferred projects only @@ -70,6 +74,9 @@ type Project struct { compilerOptions *core.CompilerOptions languageService *ls.LanguageService program *compiler.Program + + watchedGlobs []string + watcherID WatcherHandle } func NewConfiguredProject(configFileName string, configFilePath tspath.Path, host ProjectHost) *Project { @@ -205,6 +212,51 @@ func (p *Project) LanguageService() *ls.LanguageService { return p.languageService } +func (p *Project) getWatchGlobs() []string { + // !!! + if p.kind == KindConfigured { + return []string{ + p.configFileName, + } + } + return nil +} + +func (p *Project) updateWatchers() { + watchHost := p.host.Client() + if watchHost == nil { + return + } + + globs := p.getWatchGlobs() + if !slices.Equal(p.watchedGlobs, globs) { + if p.watcherID != "" { + if err := watchHost.UnwatchFiles(p.watcherID); err != nil { + p.log(fmt.Sprintf("Failed to unwatch files: %v", err)) + } + } + + p.watchedGlobs = globs + if len(globs) > 0 { + watchers := make([]lsproto.FileSystemWatcher, len(globs)) + kind := lsproto.WatchKindChange | lsproto.WatchKindDelete | lsproto.WatchKindCreate + for i, glob := range globs { + watchers[i] = lsproto.FileSystemWatcher{ + GlobPattern: lsproto.GlobPattern{ + Pattern: &glob, + }, + Kind: &kind, + } + } + if watcherID, err := watchHost.WatchFiles(watchers); err != nil { + p.log(fmt.Sprintf("Failed to watch files: %v", err)) + } else { + p.watcherID = watcherID + } + } + } +} + func (p *Project) getOrCreateScriptInfoAndAttachToProject(fileName string, scriptKind core.ScriptKind) *ScriptInfo { if scriptInfo := p.host.GetOrCreateScriptInfoForFile(fileName, p.toPath(fileName), scriptKind); scriptInfo != nil { scriptInfo.attachToProject(p) @@ -257,11 +309,11 @@ func (p *Project) updateGraph() bool { hasAddedOrRemovedFiles := p.hasAddedOrRemovedFiles p.initialLoadPending = false - if p.kind == KindConfigured && p.reloadConfig { + if p.kind == KindConfigured && p.pendingConfigReload { if err := p.LoadConfig(); err != nil { panic(fmt.Sprintf("failed to reload config: %v", err)) } - p.reloadConfig = false + p.pendingConfigReload = false } p.hasAddedOrRemovedFiles = false @@ -283,6 +335,7 @@ func (p *Project) updateGraph() bool { } } + p.updateWatchers() return true } @@ -324,7 +377,7 @@ func (p *Project) removeFile(info *ScriptInfo, fileExists bool, detachFromProjec case KindInferred: p.rootFileNames.Delete(info.path) case KindConfigured: - p.reloadConfig = true + p.pendingConfigReload = true } } diff --git a/internal/project/service.go b/internal/project/service.go index 4311a8cf87..a169e97330 100644 --- a/internal/project/service.go +++ b/internal/project/service.go @@ -2,6 +2,7 @@ package project import ( "fmt" + "slices" "strings" "sync" @@ -30,6 +31,7 @@ type assignProjectResult struct { type ServiceOptions struct { Logger *Logger PositionEncoding lsproto.PositionEncodingKind + WatchEnabled bool } var _ ProjectHost = (*Service)(nil) @@ -38,6 +40,7 @@ type Service struct { host ServiceHost options ServiceOptions comparePathsOptions tspath.ComparePathsOptions + converters *ls.Converters configuredProjects map[tspath.Path]*Project // unrootedInferredProject is the inferred project for files opened without a projectRootDirectory @@ -61,7 +64,7 @@ type Service struct { func NewService(host ServiceHost, options ServiceOptions) *Service { options.Logger.Info(fmt.Sprintf("currentDirectory:: %s useCaseSensitiveFileNames:: %t", host.GetCurrentDirectory(), host.FS().UseCaseSensitiveFileNames())) options.Logger.Info("libs Location:: " + host.DefaultLibraryPath()) - return &Service{ + service := &Service{ host: host, options: options, comparePathsOptions: tspath.ComparePathsOptions{ @@ -82,6 +85,12 @@ func NewService(host ServiceHost, options ServiceOptions) *Service { filenameToScriptInfoVersion: make(map[tspath.Path]int), realpathToScriptInfos: make(map[tspath.Path]map[*ScriptInfo]struct{}), } + + service.converters = ls.NewConverters(options.PositionEncoding, func(fileName string) ls.ScriptInfo { + return service.GetScriptInfo(fileName) + }) + + return service } // GetCurrentDirectory implements ProjectHost. @@ -124,6 +133,14 @@ func (s *Service) PositionEncoding() lsproto.PositionEncodingKind { return s.options.PositionEncoding } +// Client implements ProjectHost. +func (s *Service) Client() Client { + if s.options.WatchEnabled { + return s.host.Client() + } + return nil +} + func (s *Service) Projects() []*Project { projects := make([]*Project, 0, len(s.configuredProjects)+len(s.inferredProjects)) for _, project := range s.configuredProjects { @@ -215,6 +232,72 @@ func (s *Service) SourceFileCount() int { return s.documentRegistry.size() } +func (s *Service) OnWatchedFilesChanged(changes []lsproto.FileEvent) error { + for _, change := range changes { + fileName := ls.DocumentURIToFileName(change.Uri) + path := s.toPath(fileName) + if project, ok := s.configuredProjects[path]; ok { + if err := s.onConfigFileChanged(project, change.Type); err != nil { + return fmt.Errorf("error handling config file change: %w", err) + } + } else { + // !!! + } + } + return nil +} + +func (s *Service) onConfigFileChanged(project *Project, changeKind lsproto.FileChangeType) error { + wasDeferredClose := project.deferredClose + switch changeKind { + case lsproto.FileChangeTypeCreated: + if wasDeferredClose { + project.deferredClose = false + } + case lsproto.FileChangeTypeDeleted: + project.deferredClose = true + } + + s.delayUpdateProjectGraph(project) + if !project.deferredClose { + project.pendingConfigReload = true + project.markAsDirty() + project.updateIfDirty() + return s.publishDiagnosticsForOpenFiles(project) + } + return nil +} + +func (s *Service) publishDiagnosticsForOpenFiles(project *Project) error { + client := s.host.Client() + if client == nil { + return nil + } + + for path := range s.openFiles { + info := s.GetScriptInfoByPath(path) + if slices.Contains(info.containingProjects, project) { + diagnostics := project.LanguageService().GetDocumentDiagnostics(info.fileName) + lspDiagnostics := make([]lsproto.Diagnostic, len(diagnostics)) + for i, diagnostic := range diagnostics { + if diag, err := s.converters.ToLSPDiagnostic(diagnostic); err != nil { + return fmt.Errorf("error converting diagnostic: %w", err) + } else { + lspDiagnostics[i] = diag + } + } + + if err := client.PublishDiagnostics(&lsproto.PublishDiagnosticsParams{ + Uri: ls.FileNameToDocumentURI(info.fileName), + Diagnostics: lspDiagnostics, + }); err != nil { + return fmt.Errorf("error publishing diagnostics: %w", err) + } + } + } + return nil +} + func (s *Service) ensureProjectStructureUpToDate() { var hasChanges bool for _, project := range s.configuredProjects { diff --git a/internal/testutil/projecttestutil/projecttestutil.go b/internal/testutil/projecttestutil/projecttestutil.go index c2e324ee4d..d7898ea82a 100644 --- a/internal/testutil/projecttestutil/projecttestutil.go +++ b/internal/testutil/projecttestutil/projecttestutil.go @@ -47,6 +47,11 @@ func (p *ProjectServiceHost) NewLine() string { return "\n" } +// Client implements project.ProjectServiceHost. +func (p *ProjectServiceHost) Client() project.Client { + return nil +} + func (p *ProjectServiceHost) ReplaceFS(files map[string]string) { p.fs = bundled.WrapFS(vfstest.FromMap(files, false /*useCaseSensitiveFileNames*/)) } From d8ad00890d508b8c3f532dd8787a011f3d6b8357 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Thu, 10 Apr 2025 15:24:59 -0700 Subject: [PATCH 02/16] Handle closed files part of configured projects --- internal/api/api.go | 2 +- internal/project/documentregistry.go | 6 +- internal/project/project.go | 39 ++++-- internal/project/scriptinfo.go | 23 +++- internal/project/service.go | 26 +++- internal/tsoptions/commandlineparser.go | 6 + internal/tsoptions/parsedcommandline.go | 57 ++++++++- internal/tsoptions/tsconfigparsing.go | 41 +++++++ internal/tsoptions/utilities.go | 13 ++ internal/tsoptions/wildcarddirectories.go | 143 ++++++++++++++++++++++ 10 files changed, 328 insertions(+), 28 deletions(-) create mode 100644 internal/tsoptions/wildcarddirectories.go diff --git a/internal/api/api.go b/internal/api/api.go index 9e1391e8d6..8279e2481d 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -356,7 +356,7 @@ func (api *API) getOrCreateScriptInfo(fileName string, path tspath.Path, scriptK if !ok { return nil } - info = project.NewScriptInfo(fileName, path, scriptKind) + info = project.NewScriptInfo(fileName, path, scriptKind, api.host.FS()) info.SetTextFromDisk(content) api.scriptInfosMu.Lock() defer api.scriptInfosMu.Unlock() diff --git a/internal/project/documentregistry.go b/internal/project/documentregistry.go index f4a651e48c..d10786ba58 100644 --- a/internal/project/documentregistry.go +++ b/internal/project/documentregistry.go @@ -92,9 +92,9 @@ func (r *DocumentRegistry) getDocumentWorker( if entry, ok := r.documents.Load(key); ok { // We have an entry for this file. However, it may be for a different version of // the script snapshot. If so, update it appropriately. - if entry.sourceFile.Version != scriptInfo.version { + if entry.sourceFile.Version != scriptInfo.Version() { sourceFile := parser.ParseSourceFile(scriptInfo.fileName, scriptInfo.path, scriptInfo.text, scriptTarget, scanner.JSDocParsingModeParseAll) - sourceFile.Version = scriptInfo.version + sourceFile.Version = scriptInfo.Version() entry.mu.Lock() defer entry.mu.Unlock() entry.sourceFile = sourceFile @@ -104,7 +104,7 @@ func (r *DocumentRegistry) getDocumentWorker( } else { // Have never seen this file with these settings. Create a new source file for it. sourceFile := parser.ParseSourceFile(scriptInfo.fileName, scriptInfo.path, scriptInfo.text, scriptTarget, scanner.JSDocParsingModeParseAll) - sourceFile.Version = scriptInfo.version + sourceFile.Version = scriptInfo.Version() entry, _ := r.documents.LoadOrStore(key, ®istryEntry{ sourceFile: sourceFile, refCount: 0, diff --git a/internal/project/project.go b/internal/project/project.go index fae794661c..40a591f419 100644 --- a/internal/project/project.go +++ b/internal/project/project.go @@ -19,6 +19,10 @@ import ( ) //go:generate go tool golang.org/x/tools/cmd/stringer -type=Kind -output=project_stringer_generated.go +const ( + fileGlobPattern = "*.{js,jsx,mjs,cjs,ts,tsx,mts,cts}" + recursiveFileGlobPattern = "**/*.{js,jsx,mjs,cjs,ts,tsx,mts,cts}" +) var projectNamer = &namer{} @@ -62,7 +66,8 @@ type Project struct { deferredClose bool pendingConfigReload bool - currentDirectory string + comparePathsOptions tspath.ComparePathsOptions + currentDirectory string // Inferred projects only rootPath tspath.Path @@ -70,10 +75,11 @@ type Project struct { configFilePath tspath.Path // rootFileNames was a map from Path to { NormalizedPath, ScriptInfo? } in the original code. // But the ProjectService owns script infos, so it's not clear why there was an extra pointer. - rootFileNames *collections.OrderedMap[tspath.Path, string] - compilerOptions *core.CompilerOptions - languageService *ls.LanguageService - program *compiler.Program + rootFileNames *collections.OrderedMap[tspath.Path, string] + compilerOptions *core.CompilerOptions + parsedCommandLine *tsoptions.ParsedCommandLine + languageService *ls.LanguageService + program *compiler.Program watchedGlobs []string watcherID WatcherHandle @@ -103,6 +109,10 @@ func NewProject(name string, kind Kind, currentDirectory string, host ProjectHos currentDirectory: currentDirectory, rootFileNames: &collections.OrderedMap[tspath.Path, string]{}, } + project.comparePathsOptions = tspath.ComparePathsOptions{ + CurrentDirectory: currentDirectory, + UseCaseSensitiveFileNames: host.FS().UseCaseSensitiveFileNames(), + } project.languageService = ls.NewLanguageService(project) project.markAsDirty() return project @@ -213,25 +223,28 @@ func (p *Project) LanguageService() *ls.LanguageService { } func (p *Project) getWatchGlobs() []string { - // !!! if p.kind == KindConfigured { - return []string{ - p.configFileName, + wildcardDirectories := p.parsedCommandLine.WildcardDirectories() + result := make([]string, 0, len(wildcardDirectories)+1) + result = append(result, p.configFileName) + for dir, recursive := range wildcardDirectories { + result = append(result, fmt.Sprintf("%s/%s", dir, core.IfElse(recursive, recursiveFileGlobPattern, fileGlobPattern))) } + return result } return nil } func (p *Project) updateWatchers() { - watchHost := p.host.Client() - if watchHost == nil { + client := p.host.Client() + if client == nil { return } globs := p.getWatchGlobs() if !slices.Equal(p.watchedGlobs, globs) { if p.watcherID != "" { - if err := watchHost.UnwatchFiles(p.watcherID); err != nil { + if err := client.UnwatchFiles(p.watcherID); err != nil { p.log(fmt.Sprintf("Failed to unwatch files: %v", err)) } } @@ -248,7 +261,7 @@ func (p *Project) updateWatchers() { Kind: &kind, } } - if watcherID, err := watchHost.WatchFiles(watchers); err != nil { + if watcherID, err := client.WatchFiles(watchers); err != nil { p.log(fmt.Sprintf("Failed to watch files: %v", err)) } else { p.watcherID = watcherID @@ -284,6 +297,7 @@ func (p *Project) markAsDirty() { } } +// updateIfDirty returns true if the project was updated. func (p *Project) updateIfDirty() bool { // !!! p.invalidateResolutionsOfFailedLookupLocations() return p.dirty && p.updateGraph() @@ -437,6 +451,7 @@ func (p *Project) LoadConfig() error { }, " ", " ")), ) + p.parsedCommandLine = parsedCommandLine p.compilerOptions = parsedCommandLine.CompilerOptions() p.setRootFiles(parsedCommandLine.FileNames()) } else { diff --git a/internal/project/scriptinfo.go b/internal/project/scriptinfo.go index f97f15ad13..dfa18a67af 100644 --- a/internal/project/scriptinfo.go +++ b/internal/project/scriptinfo.go @@ -27,9 +27,11 @@ type ScriptInfo struct { deferredDelete bool containingProjects []*Project + + fs vfs.FS } -func NewScriptInfo(fileName string, path tspath.Path, scriptKind core.ScriptKind) *ScriptInfo { +func NewScriptInfo(fileName string, path tspath.Path, scriptKind core.ScriptKind, fs vfs.FS) *ScriptInfo { isDynamic := isDynamicFileName(fileName) realpath := core.IfElse(isDynamic, path, "") return &ScriptInfo{ @@ -38,6 +40,7 @@ func NewScriptInfo(fileName string, path tspath.Path, scriptKind core.ScriptKind realpath: realpath, isDynamic: isDynamic, scriptKind: scriptKind, + fs: fs, } } @@ -51,15 +54,29 @@ func (s *ScriptInfo) Path() tspath.Path { func (s *ScriptInfo) LineMap() *ls.LineMap { if s.lineMap == nil { - s.lineMap = ls.ComputeLineStarts(s.text) + s.lineMap = ls.ComputeLineStarts(s.Text()) } return s.lineMap } func (s *ScriptInfo) Text() string { + s.reloadIfNeeded() return s.text } +func (s *ScriptInfo) Version() int { + s.reloadIfNeeded() + return s.version +} + +func (s *ScriptInfo) reloadIfNeeded() { + if s.pendingReloadFromDisk { + if newText, ok := s.fs.ReadFile(s.fileName); ok { + s.SetTextFromDisk(newText) + } + } +} + func (s *ScriptInfo) open(newText string) { s.isOpen = true s.pendingReloadFromDisk = false @@ -133,7 +150,7 @@ func (s *ScriptInfo) isOrphan() bool { } func (s *ScriptInfo) editContent(change ls.TextChange) { - s.setText(change.ApplyTo(s.text)) + s.setText(change.ApplyTo(s.Text())) s.markContainingProjectsAsDirty() } diff --git a/internal/project/service.go b/internal/project/service.go index a169e97330..68bae99603 100644 --- a/internal/project/service.go +++ b/internal/project/service.go @@ -237,11 +237,31 @@ func (s *Service) OnWatchedFilesChanged(changes []lsproto.FileEvent) error { fileName := ls.DocumentURIToFileName(change.Uri) path := s.toPath(fileName) if project, ok := s.configuredProjects[path]; ok { + // tsconfig of project if err := s.onConfigFileChanged(project, change.Type); err != nil { return fmt.Errorf("error handling config file change: %w", err) } + } else if _, ok := s.openFiles[path]; ok { + // open file + continue + } else if info := s.GetScriptInfoByPath(path); info != nil { + // closed existing file + if change.Type == lsproto.FileChangeTypeDeleted { + s.handleDeletedFile(info, true /*deferredDelete*/) + } else { + info.deferredDelete = false + info.delayReloadNonMixedContentFile() + // !!! s.delayUpdateProjectGraphs(info.containingProjects, false /*clearSourceMapperCache*/) + // !!! s.handleSourceMapProjects(info) + } } else { - // !!! + // must be wildcard watcher? + } + } + + for _, project := range s.configuredProjects { + if project.updateIfDirty() { + s.publishDiagnosticsForOpenFiles(project) } } return nil @@ -262,8 +282,6 @@ func (s *Service) onConfigFileChanged(project *Project, changeKind lsproto.FileC if !project.deferredClose { project.pendingConfigReload = true project.markAsDirty() - project.updateIfDirty() - return s.publishDiagnosticsForOpenFiles(project) } return nil } @@ -434,7 +452,7 @@ func (s *Service) getOrCreateScriptInfoWorker(fileName string, path tspath.Path, } } - info = NewScriptInfo(fileName, path, scriptKind) + info = NewScriptInfo(fileName, path, scriptKind, s.host.FS()) if fromDisk { info.SetTextFromDisk(fileContent) } diff --git a/internal/tsoptions/commandlineparser.go b/internal/tsoptions/commandlineparser.go index a9916cfe4b..4d70a00981 100644 --- a/internal/tsoptions/commandlineparser.go +++ b/internal/tsoptions/commandlineparser.go @@ -9,6 +9,7 @@ import ( "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/diagnostics" "github.com/microsoft/typescript-go/internal/stringutil" + "github.com/microsoft/typescript-go/internal/tspath" "github.com/microsoft/typescript-go/internal/vfs" ) @@ -58,6 +59,11 @@ func ParseCommandLine( Errors: parser.errors, Raw: parser.options, // !!! keep optionsBase incase needed later. todo: figure out if this is still needed CompileOnSave: nil, + + comparePathsOptions: tspath.ComparePathsOptions{ + UseCaseSensitiveFileNames: host.FS().UseCaseSensitiveFileNames(), + CurrentDirectory: host.GetCurrentDirectory(), + }, } } diff --git a/internal/tsoptions/parsedcommandline.go b/internal/tsoptions/parsedcommandline.go index 8ddec3c0ef..66d708f977 100644 --- a/internal/tsoptions/parsedcommandline.go +++ b/internal/tsoptions/parsedcommandline.go @@ -2,20 +2,38 @@ package tsoptions import ( "slices" + "sync" "github.com/microsoft/typescript-go/internal/ast" "github.com/microsoft/typescript-go/internal/core" + "github.com/microsoft/typescript-go/internal/tspath" ) type ParsedCommandLine struct { ParsedConfig *core.ParsedOptions `json:"parsedConfig"` - ConfigFile *TsConfigSourceFile `json:"configFile"` // TsConfigSourceFile, used in Program and ExecuteCommandLine - Errors []*ast.Diagnostic `json:"errors"` - Raw any `json:"raw"` - // WildcardDirectories map[string]watchDirectoryFlags - CompileOnSave *bool `json:"compileOnSave"` + ConfigFile *TsConfigSourceFile `json:"configFile"` // TsConfigSourceFile, used in Program and ExecuteCommandLine + Errors []*ast.Diagnostic `json:"errors"` + Raw any `json:"raw"` + CompileOnSave *bool `json:"compileOnSave"` // TypeAquisition *core.TypeAcquisition + + comparePathsOptions tspath.ComparePathsOptions + wildcardDirectoriesOnce sync.Once + wildcardDirectories map[string]bool +} + +// WildcardDirectories returns the cached wildcard directories, initializing them if needed +func (p *ParsedCommandLine) WildcardDirectories() map[string]bool { + p.wildcardDirectoriesOnce.Do(func() { + p.wildcardDirectories = getWildcardDirectories( + p.ConfigFile.configFileSpecs.validatedIncludeSpecs, + p.ConfigFile.configFileSpecs.validatedExcludeSpecs, + p.comparePathsOptions, + ) + }) + + return p.wildcardDirectories } func (p *ParsedCommandLine) SetParsedOptions(o *core.ParsedOptions) { @@ -45,3 +63,32 @@ func (p *ParsedCommandLine) GetConfigFileParsingDiagnostics() []*ast.Diagnostic } return p.Errors } + +func (p *ParsedCommandLine) MatchesFileName(fileName string, comparePathsOptions tspath.ComparePathsOptions) bool { + path := tspath.ToPath(fileName, comparePathsOptions.CurrentDirectory, comparePathsOptions.UseCaseSensitiveFileNames) + if slices.ContainsFunc(p.FileNames(), func(f string) bool { + return path == tspath.ToPath(f, comparePathsOptions.CurrentDirectory, comparePathsOptions.UseCaseSensitiveFileNames) + }) { + return true + } + + if p.ConfigFile == nil { + return false + } + + if slices.ContainsFunc(p.ConfigFile.configFileSpecs.validatedFilesSpec, func(f string) bool { + return path == tspath.ToPath(f, comparePathsOptions.CurrentDirectory, comparePathsOptions.UseCaseSensitiveFileNames) + }) { + return true + } + + if len(p.ConfigFile.configFileSpecs.validatedIncludeSpecs) == 0 { + return false + } + + if p.ConfigFile.configFileSpecs.matchesExclude(fileName, comparePathsOptions) { + return false + } + + return p.ConfigFile.configFileSpecs.matchesInclude(fileName, comparePathsOptions) +} diff --git a/internal/tsoptions/tsconfigparsing.go b/internal/tsoptions/tsconfigparsing.go index 99a3897865..386ecaa6e4 100644 --- a/internal/tsoptions/tsconfigparsing.go +++ b/internal/tsoptions/tsconfigparsing.go @@ -97,15 +97,51 @@ type configFileSpecs struct { validatedExcludeSpecs []string isDefaultIncludeSpec bool } + +func (c *configFileSpecs) matchesExclude(fileName string, comparePathsOptions tspath.ComparePathsOptions) bool { + if len(c.validatedExcludeSpecs) == 0 { + return false + } + excludePattern := getRegularExpressionForWildcard(c.validatedExcludeSpecs, comparePathsOptions.CurrentDirectory, "exclude") + excludeRegex := getRegexFromPattern(excludePattern, comparePathsOptions.UseCaseSensitiveFileNames) + if match, err := excludeRegex.MatchString(fileName); err == nil && match { + return true + } + if !tspath.HasExtension(fileName) { + if match, err := excludeRegex.MatchString(tspath.EnsureTrailingDirectorySeparator(fileName)); err == nil && match { + return true + } + } + return false +} + +func (c *configFileSpecs) matchesInclude(fileName string, comparePathsOptions tspath.ComparePathsOptions) bool { + if len(c.validatedIncludeSpecs) == 0 { + return false + } + for _, spec := range c.validatedIncludeSpecs { + includePattern := getPatternFromSpec(spec, comparePathsOptions.CurrentDirectory, "files") + if includePattern != "" { + includeRegex := getRegexFromPattern(includePattern, comparePathsOptions.UseCaseSensitiveFileNames) + if match, err := includeRegex.MatchString(fileName); err == nil && match { + return true + } + } + } + return false +} + type fileExtensionInfo struct { extension string isMixedContent bool scriptKind core.ScriptKind } + type ExtendedConfigCacheEntry struct { extendedResult *TsConfigSourceFile extendedConfig *parsedTsconfig } + type parsedTsconfig struct { raw any options *core.CompilerOptions @@ -1209,6 +1245,11 @@ func parseJsonConfigFileContentWorker( ConfigFile: sourceFile, Raw: parsedConfig.raw, Errors: errors, + + comparePathsOptions: tspath.ComparePathsOptions{ + UseCaseSensitiveFileNames: host.FS().UseCaseSensitiveFileNames(), + CurrentDirectory: basePathForFileNames, + }, } } diff --git a/internal/tsoptions/utilities.go b/internal/tsoptions/utilities.go index b5889636ff..8810b6bbd0 100644 --- a/internal/tsoptions/utilities.go +++ b/internal/tsoptions/utilities.go @@ -150,6 +150,19 @@ var wildcardMatchers = map[usage]WildcardMatcher{ usageExclude: excludeMatcher, } +func getPatternFromSpec( + spec string, + basePath string, + usage usage, +) string { + pattern := getSubPatternFromSpec(spec, basePath, usage, wildcardMatchers[usage]) + if pattern == "" { + return "" + } + ending := core.IfElse(usage == "exclude", "($|/)", "$") + return fmt.Sprintf("^(%s)%s", pattern, ending) +} + func getSubPatternFromSpec( spec string, basePath string, diff --git a/internal/tsoptions/wildcarddirectories.go b/internal/tsoptions/wildcarddirectories.go new file mode 100644 index 0000000000..c906d14bf2 --- /dev/null +++ b/internal/tsoptions/wildcarddirectories.go @@ -0,0 +1,143 @@ +package tsoptions + +import ( + "regexp" + "strings" + + "github.com/dlclark/regexp2" + "github.com/microsoft/typescript-go/internal/tspath" +) + +func getWildcardDirectories(include []string, exclude []string, comparePathsOptions tspath.ComparePathsOptions) map[string]bool { + // We watch a directory recursively if it contains a wildcard anywhere in a directory segment + // of the pattern: + // + // /a/b/**/d - Watch /a/b recursively to catch changes to any d in any subfolder recursively + // /a/b/*/d - Watch /a/b recursively to catch any d in any immediate subfolder, even if a new subfolder is added + // /a/b - Watch /a/b recursively to catch changes to anything in any recursive subfoler + // + // We watch a directory without recursion if it contains a wildcard in the file segment of + // the pattern: + // + // /a/b/* - Watch /a/b directly to catch any new file + // /a/b/a?z - Watch /a/b directly to catch any new file matching a?z + + if len(include) == 0 { + return nil + } + + rawExcludeRegex := getRegularExpressionForWildcard(exclude, comparePathsOptions.CurrentDirectory, "exclude") + var excludeRegex *regexp.Regexp + if rawExcludeRegex != "" { + options := "" + if !comparePathsOptions.UseCaseSensitiveFileNames { + options = "(?i)" + } + excludeRegex = regexp.MustCompile(options + rawExcludeRegex) + } + + wildcardDirectories := make(map[string]bool) + wildCardKeyToPath := make(map[string]string) + + var recursiveKeys []string + + for _, file := range include { + spec := tspath.NormalizeSlashes(tspath.CombinePaths(comparePathsOptions.CurrentDirectory, file)) + if excludeRegex != nil && excludeRegex.MatchString(spec) { + continue + } + + match := getWildcardDirectoryFromSpec(spec, comparePathsOptions.UseCaseSensitiveFileNames) + if match != nil { + key := match.Key + path := match.Path + recursive := match.Recursive + + existingPath, existsPath := wildCardKeyToPath[key] + var existingRecursive bool + + if existsPath { + existingRecursive = wildcardDirectories[existingPath] + } + + if !existsPath || (!existingRecursive && recursive) { + pathToUse := path + if existsPath { + pathToUse = existingPath + } + wildcardDirectories[pathToUse] = recursive + + if !existsPath { + wildCardKeyToPath[key] = path + } + + if recursive { + recursiveKeys = append(recursiveKeys, key) + } + } + } + + // Remove any subpaths under an existing recursively watched directory + for path := range wildcardDirectories { + for _, recursiveKey := range recursiveKeys { + key := toCanonicalKey(path, comparePathsOptions.UseCaseSensitiveFileNames) + if key != recursiveKey && tspath.ContainsPath(recursiveKey, key, comparePathsOptions) { + delete(wildcardDirectories, path) + } + } + } + } + + return wildcardDirectories +} + +func toCanonicalKey(path string, useCaseSensitiveFileNames bool) string { + if useCaseSensitiveFileNames { + return path + } + return strings.ToLower(path) +} + +// wildcardDirectoryPattern matches paths with wildcard characters +var wildcardDirectoryPattern = regexp2.MustCompile(`^[^*?]*(?=\/[^/]*[*?])`, 0) + +// wildcardDirectoryMatch represents the result of a wildcard directory match +type wildcardDirectoryMatch struct { + Key string + Path string + Recursive bool +} + +func getWildcardDirectoryFromSpec(spec string, useCaseSensitiveFileNames bool) *wildcardDirectoryMatch { + match, _ := wildcardDirectoryPattern.FindStringMatch(spec) + if match != nil { + // We check this with a few `Index` calls because it's more efficient than complex regex + questionWildcardIndex := strings.Index(spec, "?") + starWildcardIndex := strings.Index(spec, "*") + lastDirectorySeparatorIndex := strings.LastIndexByte(spec, tspath.DirectorySeparator) + + // Determine if this should be watched recursively + recursive := (questionWildcardIndex != -1 && questionWildcardIndex < lastDirectorySeparatorIndex) || + (starWildcardIndex != -1 && starWildcardIndex < lastDirectorySeparatorIndex) + + return &wildcardDirectoryMatch{ + Key: toCanonicalKey(match.String(), useCaseSensitiveFileNames), + Path: match.String(), + Recursive: recursive, + } + } + + if lastSepIndex := strings.LastIndexByte(spec, tspath.DirectorySeparator); lastSepIndex != -1 { + lastSegment := spec[lastSepIndex+1:] + if isImplicitGlob(lastSegment) { + path := tspath.RemoveTrailingDirectorySeparator(spec) + return &wildcardDirectoryMatch{ + Key: toCanonicalKey(path, useCaseSensitiveFileNames), + Path: path, + Recursive: true, + } + } + } + + return nil +} From 361b2fa212562f4846535fb58054871a84de00cf Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Fri, 11 Apr 2025 11:22:40 -0700 Subject: [PATCH 03/16] Add tests --- internal/project/project.go | 24 +++- internal/project/service.go | 7 +- internal/project/service_test.go | 223 +++++++++++++++++++++++++++++++ 3 files changed, 248 insertions(+), 6 deletions(-) diff --git a/internal/project/project.go b/internal/project/project.go index 40a591f419..cb522daf4c 100644 --- a/internal/project/project.go +++ b/internal/project/project.go @@ -270,6 +270,15 @@ func (p *Project) updateWatchers() { } } +func (p *Project) onWatchedFileCreated(fileName string) { + if p.kind == KindConfigured { + if p.rootFileNames.Has(p.toPath(fileName)) || p.parsedCommandLine.MatchesFileName(fileName, p.comparePathsOptions) { + p.pendingConfigReload = true + p.markAsDirty() + } + } +} + func (p *Project) getOrCreateScriptInfoAndAttachToProject(fileName string, scriptKind core.ScriptKind) *ScriptInfo { if scriptInfo := p.host.GetOrCreateScriptInfoForFile(fileName, p.toPath(fileName), scriptKind); scriptInfo != nil { scriptInfo.attachToProject(p) @@ -467,16 +476,21 @@ func (p *Project) setRootFiles(rootFileNames []string) { newRootScriptInfos := make(map[tspath.Path]struct{}, len(rootFileNames)) for _, file := range rootFileNames { scriptKind := p.getScriptKind(file) - scriptInfo := p.host.GetOrCreateScriptInfoForFile(file, p.toPath(file), scriptKind) - newRootScriptInfos[scriptInfo.path] = struct{}{} - if _, isRoot := p.rootFileNames.Get(scriptInfo.path); !isRoot { + path := p.toPath(file) + // !!! updateNonInferredProjectFiles uses a fileExists check, which I guess + // could be needed if a watcher fails? + scriptInfo := p.host.GetOrCreateScriptInfoForFile(file, path, scriptKind) + newRootScriptInfos[path] = struct{}{} + isAlreadyRoot := p.rootFileNames.Has(path) + + if !isAlreadyRoot && scriptInfo != nil { p.addRoot(scriptInfo) if scriptInfo.isOpen { // !!! // s.removeRootOfInferredProjectIfNowPartOfOtherProject(scriptInfo) } - } else { - p.rootFileNames.Set(scriptInfo.path, file) + } else if !isAlreadyRoot { + p.rootFileNames.Set(path, file) } } diff --git a/internal/project/service.go b/internal/project/service.go index 68bae99603..6a29c44329 100644 --- a/internal/project/service.go +++ b/internal/project/service.go @@ -255,7 +255,12 @@ func (s *Service) OnWatchedFilesChanged(changes []lsproto.FileEvent) error { // !!! s.handleSourceMapProjects(info) } } else { - // must be wildcard watcher? + if change.Type != lsproto.FileChangeTypeCreated { + panic("unexpected file change type") + } + for _, project := range s.configuredProjects { + project.onWatchedFileCreated(fileName) + } } } diff --git a/internal/project/service_test.go b/internal/project/service_test.go index a283641174..3b6ef60e4c 100644 --- a/internal/project/service_test.go +++ b/internal/project/service_test.go @@ -7,6 +7,7 @@ import ( "github.com/microsoft/typescript-go/internal/bundled" "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/ls" + "github.com/microsoft/typescript-go/internal/lsp/lsproto" "github.com/microsoft/typescript-go/internal/project" "github.com/microsoft/typescript-go/internal/testutil/projecttestutil" "gotest.tools/v3/assert" @@ -228,4 +229,226 @@ func TestService(t *testing.T) { assert.Assert(t, x1 != x2) }) }) + + t.Run("Watch", func(t *testing.T) { + t.Parallel() + + t.Run("change open file", func(t *testing.T) { + t.Parallel() + service, host := setup(files) + service.OpenFile("/home/projects/TS/p1/src/x.ts", files["/home/projects/TS/p1/src/x.ts"], core.ScriptKindTS, "") + service.OpenFile("/home/projects/TS/p1/src/index.ts", files["/home/projects/TS/p1/src/index.ts"], core.ScriptKindTS, "") + _, project := service.EnsureDefaultProjectForFile("/home/projects/TS/p1/src/index.ts") + programBefore := project.GetProgram() + + filesCopy := maps.Clone(files) + filesCopy["/home/projects/TS/p1/src/x.ts"] = `export const x = 2;` + host.replaceFS(filesCopy) + service.OnWatchedFilesChanged([]lsproto.FileEvent{ + { + Type: lsproto.FileChangeTypeChanged, + Uri: "file:///home/projects/TS/p1/src/x.ts", + }, + }) + + assert.Equal(t, programBefore, project.GetProgram()) + }) + + t.Run("change closed program file", func(t *testing.T) { + t.Parallel() + service, host := setup(files) + service.OpenFile("/home/projects/TS/p1/src/index.ts", files["/home/projects/TS/p1/src/index.ts"], core.ScriptKindTS, "") + _, project := service.EnsureDefaultProjectForFile("/home/projects/TS/p1/src/index.ts") + programBefore := project.GetProgram() + + filesCopy := maps.Clone(files) + filesCopy["/home/projects/TS/p1/src/x.ts"] = `export const x = 2;` + host.replaceFS(filesCopy) + service.OnWatchedFilesChanged([]lsproto.FileEvent{ + { + Type: lsproto.FileChangeTypeChanged, + Uri: "file:///home/projects/TS/p1/src/x.ts", + }, + }) + + assert.Check(t, project.GetProgram() != programBefore) + }) + + t.Run("change config file", func(t *testing.T) { + t.Parallel() + files := map[string]string{ + "/home/projects/TS/p1/tsconfig.json": `{ + "compilerOptions": { + "noLib": true, + "strict": false + } + }`, + "/home/projects/TS/p1/src/x.ts": `export declare const x: number | undefined;`, + "/home/projects/TS/p1/src/index.ts": ` + import { x } from "./x"; + let y: number = x;`, + } + + service, host := setup(files) + service.OpenFile("/home/projects/TS/p1/src/index.ts", files["/home/projects/TS/p1/src/index.ts"], core.ScriptKindTS, "") + _, project := service.EnsureDefaultProjectForFile("/home/projects/TS/p1/src/index.ts") + program := project.GetProgram() + assert.Equal(t, len(program.GetSemanticDiagnostics(program.GetSourceFile("/home/projects/TS/p1/src/index.ts"))), 0) + + filesCopy := maps.Clone(files) + filesCopy["/home/projects/TS/p1/tsconfig.json"] = `{ + "compilerOptions": { + "noLib": false, + "strict": true + } + }` + host.replaceFS(filesCopy) + service.OnWatchedFilesChanged([]lsproto.FileEvent{ + { + Type: lsproto.FileChangeTypeChanged, + Uri: "file:///home/projects/TS/p1/tsconfig.json", + }, + }) + + program = project.GetProgram() + assert.Equal(t, len(program.GetSemanticDiagnostics(program.GetSourceFile("/home/projects/TS/p1/src/index.ts"))), 1) + }) + + t.Run("delete explicitly included file", func(t *testing.T) { + t.Parallel() + files := map[string]string{ + "/home/projects/TS/p1/tsconfig.json": `{ + "compilerOptions": { + "noLib": true, + }, + "files": ["src/index.ts", "src/x.ts"] + }`, + "/home/projects/TS/p1/src/x.ts": `export declare const x: number | undefined;`, + "/home/projects/TS/p1/src/index.ts": `import { x } from "./x";`, + } + service, host := setup(files) + service.OpenFile("/home/projects/TS/p1/src/index.ts", files["/home/projects/TS/p1/src/index.ts"], core.ScriptKindTS, "") + _, project := service.EnsureDefaultProjectForFile("/home/projects/TS/p1/src/index.ts") + program := project.GetProgram() + assert.Equal(t, len(program.GetSemanticDiagnostics(program.GetSourceFile("/home/projects/TS/p1/src/index.ts"))), 0) + + filesCopy := maps.Clone(files) + delete(filesCopy, "/home/projects/TS/p1/src/x.ts") + host.replaceFS(filesCopy) + service.OnWatchedFilesChanged([]lsproto.FileEvent{ + { + Type: lsproto.FileChangeTypeDeleted, + Uri: "file:///home/projects/TS/p1/src/x.ts", + }, + }) + + program = project.GetProgram() + assert.Equal(t, len(program.GetSemanticDiagnostics(program.GetSourceFile("/home/projects/TS/p1/src/index.ts"))), 1) + assert.Check(t, program.GetSourceFile("/home/projects/TS/p1/src/x.ts") == nil) + }) + + t.Run("delete wildcard included file", func(t *testing.T) { + t.Parallel() + files := map[string]string{ + "/home/projects/TS/p1/tsconfig.json": `{ + "compilerOptions": { + "noLib": true + }, + "include": ["src"] + }`, + "/home/projects/TS/p1/src/index.ts": `let x = 2;`, + "/home/projects/TS/p1/src/x.ts": `let y = x;`, + } + service, host := setup(files) + service.OpenFile("/home/projects/TS/p1/src/x.ts", files["/home/projects/TS/p1/src/x.ts"], core.ScriptKindTS, "") + _, project := service.EnsureDefaultProjectForFile("/home/projects/TS/p1/src/x.ts") + program := project.GetProgram() + assert.Equal(t, len(program.GetSemanticDiagnostics(program.GetSourceFile("/home/projects/TS/p1/src/x.ts"))), 0) + + filesCopy := maps.Clone(files) + delete(filesCopy, "/home/projects/TS/p1/src/index.ts") + host.replaceFS(filesCopy) + service.OnWatchedFilesChanged([]lsproto.FileEvent{ + { + Type: lsproto.FileChangeTypeDeleted, + Uri: "file:///home/projects/TS/p1/src/index.ts", + }, + }) + + program = project.GetProgram() + assert.Equal(t, len(program.GetSemanticDiagnostics(program.GetSourceFile("/home/projects/TS/p1/src/x.ts"))), 1) + }) + + t.Run("create explicitly included file", func(t *testing.T) { + t.Parallel() + files := map[string]string{ + "/home/projects/TS/p1/tsconfig.json": `{ + "compilerOptions": { + "noLib": true + }, + "files": ["src/index.ts", "src/y.ts"] + }`, + "/home/projects/TS/p1/src/index.ts": `import { y } from "./y";`, + } + service, host := setup(files) + service.OpenFile("/home/projects/TS/p1/src/index.ts", files["/home/projects/TS/p1/src/index.ts"], core.ScriptKindTS, "") + _, project := service.EnsureDefaultProjectForFile("/home/projects/TS/p1/src/index.ts") + program := project.GetProgram() + + // Initially should have an error because y.ts is missing + assert.Equal(t, len(program.GetSemanticDiagnostics(program.GetSourceFile("/home/projects/TS/p1/src/index.ts"))), 1) + + // Add the missing file + filesCopy := maps.Clone(files) + filesCopy["/home/projects/TS/p1/src/y.ts"] = `export const y = 1;` + host.replaceFS(filesCopy) + service.OnWatchedFilesChanged([]lsproto.FileEvent{ + { + Type: lsproto.FileChangeTypeCreated, + Uri: "file:///home/projects/TS/p1/src/y.ts", + }, + }) + + // Error should be resolved + program = project.GetProgram() + assert.Equal(t, len(program.GetSemanticDiagnostics(program.GetSourceFile("/home/projects/TS/p1/src/index.ts"))), 0) + assert.Check(t, program.GetSourceFile("/home/projects/TS/p1/src/y.ts") != nil) + }) + + t.Run("create wildcard included file", func(t *testing.T) { + t.Parallel() + files := map[string]string{ + "/home/projects/TS/p1/tsconfig.json": `{ + "compilerOptions": { + "noLib": true + }, + "include": ["src"] + }`, + "/home/projects/TS/p1/src/index.ts": `import { z } from "./z";`, + } + service, host := setup(files) + service.OpenFile("/home/projects/TS/p1/src/index.ts", files["/home/projects/TS/p1/src/index.ts"], core.ScriptKindTS, "") + _, project := service.EnsureDefaultProjectForFile("/home/projects/TS/p1/src/index.ts") + program := project.GetProgram() + + // Initially should have an error because z.ts is missing + assert.Equal(t, len(program.GetSemanticDiagnostics(program.GetSourceFile("/home/projects/TS/p1/src/index.ts"))), 1) + + // Add a new file through wildcard inclusion + filesCopy := maps.Clone(files) + filesCopy["/home/projects/TS/p1/src/z.ts"] = `export const z = 1;` + host.replaceFS(filesCopy) + service.OnWatchedFilesChanged([]lsproto.FileEvent{ + { + Type: lsproto.FileChangeTypeCreated, + Uri: "file:///home/projects/TS/p1/src/z.ts", + }, + }) + + // Error should be resolved and the new file should be included in the program + program = project.GetProgram() + assert.Equal(t, len(program.GetSemanticDiagnostics(program.GetSourceFile("/home/projects/TS/p1/src/index.ts"))), 0) + assert.Check(t, program.GetSourceFile("/home/projects/TS/p1/src/z.ts") != nil) + }) + }) } From c7b7ef3153bffebee9320f7f60cff10bbdfa575a Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Wed, 16 Apr 2025 12:34:43 -0700 Subject: [PATCH 04/16] Trigger update on failed/affecting locations watch --- internal/project/project.go | 129 ++++++++++++++++++++++++++---------- internal/project/service.go | 5 +- internal/project/watch.go | 61 +++++++++++++++++ 3 files changed, 156 insertions(+), 39 deletions(-) create mode 100644 internal/project/watch.go diff --git a/internal/project/project.go b/internal/project/project.go index cb522daf4c..5e3652ea9f 100644 --- a/internal/project/project.go +++ b/internal/project/project.go @@ -2,6 +2,7 @@ package project import ( "fmt" + "maps" "strings" "sync" @@ -19,10 +20,7 @@ import ( ) //go:generate go tool golang.org/x/tools/cmd/stringer -type=Kind -output=project_stringer_generated.go -const ( - fileGlobPattern = "*.{js,jsx,mjs,cjs,ts,tsx,mts,cts}" - recursiveFileGlobPattern = "**/*.{js,jsx,mjs,cjs,ts,tsx,mts,cts}" -) +const hr = "-----------------------------------------------" var projectNamer = &namer{} @@ -81,8 +79,10 @@ type Project struct { languageService *ls.LanguageService program *compiler.Program - watchedGlobs []string - watcherID WatcherHandle + // Watchers + rootFilesWatch *watchedFiles[[]string] + failedLookupsWatch *watchedFiles[map[tspath.Path]string] + affectingLocationsWatch *watchedFiles[map[tspath.Path]string] } func NewConfiguredProject(configFileName string, configFilePath tspath.Path, host ProjectHost) *Project { @@ -90,6 +90,10 @@ func NewConfiguredProject(configFileName string, configFilePath tspath.Path, hos project.configFileName = configFileName project.configFilePath = configFilePath project.initialLoadPending = true + client := host.Client() + if client != nil { + project.rootFilesWatch = newWatchedFiles(client, lsproto.WatchKindChange|lsproto.WatchKindCreate|lsproto.WatchKindDelete, core.Identity) + } return project } @@ -113,6 +117,15 @@ func NewProject(name string, kind Kind, currentDirectory string, host ProjectHos CurrentDirectory: currentDirectory, UseCaseSensitiveFileNames: host.FS().UseCaseSensitiveFileNames(), } + client := host.Client() + if client != nil { + project.failedLookupsWatch = newWatchedFiles(client, lsproto.WatchKindCreate, func(data map[tspath.Path]string) []string { + return slices.Sorted(maps.Values(data)) + }) + project.affectingLocationsWatch = newWatchedFiles(client, lsproto.WatchKindChange|lsproto.WatchKindCreate|lsproto.WatchKindDelete, func(data map[tspath.Path]string) []string { + return slices.Sorted(maps.Values(data)) + }) + } project.languageService = ls.NewLanguageService(project) project.markAsDirty() return project @@ -222,7 +235,7 @@ func (p *Project) LanguageService() *ls.LanguageService { return p.languageService } -func (p *Project) getWatchGlobs() []string { +func (p *Project) getRootFileWatchGlobs() []string { if p.kind == KindConfigured { wildcardDirectories := p.parsedCommandLine.WildcardDirectories() result := make([]string, 0, len(wildcardDirectories)+1) @@ -235,48 +248,78 @@ func (p *Project) getWatchGlobs() []string { return nil } +func (p *Project) getModuleResolutionWatchGlobs() (failedLookups map[tspath.Path]string, affectingLocaions map[tspath.Path]string) { + failedLookups = make(map[tspath.Path]string) + affectingLocaions = make(map[tspath.Path]string) + for _, resolvedModulesInFile := range p.program.GetResolvedModules() { + for _, resolvedModule := range resolvedModulesInFile { + for _, failedLookupLocation := range resolvedModule.FailedLookupLocations { + path := p.toPath(failedLookupLocation) + if _, ok := failedLookups[path]; !ok { + failedLookups[path] = failedLookupLocation + } + } + for _, affectingLocation := range resolvedModule.AffectingLocations { + path := p.toPath(affectingLocation) + if _, ok := affectingLocaions[path]; !ok { + affectingLocaions[path] = affectingLocation + } + } + } + } + return failedLookups, affectingLocaions +} + func (p *Project) updateWatchers() { client := p.host.Client() if client == nil { return } - globs := p.getWatchGlobs() - if !slices.Equal(p.watchedGlobs, globs) { - if p.watcherID != "" { - if err := client.UnwatchFiles(p.watcherID); err != nil { - p.log(fmt.Sprintf("Failed to unwatch files: %v", err)) - } - } + rootFileGlobs := p.getRootFileWatchGlobs() + failedLookupGlobs, affectingLocationGlobs := p.getModuleResolutionWatchGlobs() - p.watchedGlobs = globs - if len(globs) > 0 { - watchers := make([]lsproto.FileSystemWatcher, len(globs)) - kind := lsproto.WatchKindChange | lsproto.WatchKindDelete | lsproto.WatchKindCreate - for i, glob := range globs { - watchers[i] = lsproto.FileSystemWatcher{ - GlobPattern: lsproto.GlobPattern{ - Pattern: &glob, - }, - Kind: &kind, - } - } - if watcherID, err := client.WatchFiles(watchers); err != nil { - p.log(fmt.Sprintf("Failed to watch files: %v", err)) - } else { - p.watcherID = watcherID - } - } + if updated, err := p.rootFilesWatch.update(rootFileGlobs); err != nil { + p.log(fmt.Sprintf("Failed to update root file watch: %v", err)) + } else if updated { + p.log("Root file watches updated:\n" + formatFileList(rootFileGlobs, "\t", hr)) + } + + if updated, err := p.failedLookupsWatch.update(failedLookupGlobs); err != nil { + p.log(fmt.Sprintf("Failed to update failed lookup watch: %v", err)) + } else if updated { + p.log("Failed lookup watches updated:\n" + formatFileList(p.failedLookupsWatch.globs, "\t", hr)) + } + + if updated, err := p.affectingLocationsWatch.update(affectingLocationGlobs); err != nil { + p.log(fmt.Sprintf("Failed to update affecting location watch: %v", err)) + } else if updated { + p.log("Affecting location watches updated:\n" + formatFileList(p.affectingLocationsWatch.globs, "\t", hr)) } } -func (p *Project) onWatchedFileCreated(fileName string) { +// onWatchEventForNilScriptInfo is fired for watch events that are not the +// project tsconfig, and do not have a ScriptInfo for the associated file. +// This could be a case of one of the following: +// - A file is being created that will be added to the project. +// - An affecting location was changed. +// - A file is being created that matches a watch glob, but is not actually +// part of the project, e.g., a .js file in a project without --allowJs. +func (p *Project) onWatchEventForNilScriptInfo(fileName string) { + path := p.toPath(fileName) if p.kind == KindConfigured { - if p.rootFileNames.Has(p.toPath(fileName)) || p.parsedCommandLine.MatchesFileName(fileName, p.comparePathsOptions) { + if p.rootFileNames.Has(path) || p.parsedCommandLine.MatchesFileName(fileName, p.comparePathsOptions) { p.pendingConfigReload = true p.markAsDirty() + return } } + + if _, ok := p.failedLookupsWatch.data[path]; ok { + p.markAsDirty() + } else if _, ok := p.affectingLocationsWatch.data[path]; ok { + p.markAsDirty() + } } func (p *Project) getOrCreateScriptInfoAndAttachToProject(fileName string, scriptKind core.ScriptKind) *ScriptInfo { @@ -533,7 +576,7 @@ func (p *Project) print(writeFileNames bool, writeFileExplanation bool, writeFil // if writeFileExplanation {} } } - builder.WriteString("-----------------------------------------------") + builder.WriteString(hr) return builder.String() } @@ -548,3 +591,19 @@ func (p *Project) logf(format string, args ...interface{}) { func (p *Project) Close() { // !!! } + +func formatFileList(files []string, linePrefix string, groupSuffix string) string { + var builder strings.Builder + length := len(groupSuffix) + for _, file := range files { + length += len(file) + len(linePrefix) + 1 + } + builder.Grow(length) + for _, file := range files { + builder.WriteString(linePrefix) + builder.WriteString(file) + builder.WriteRune('\n') + } + builder.WriteString(groupSuffix) + return builder.String() +} diff --git a/internal/project/service.go b/internal/project/service.go index 6a29c44329..b9da2946ba 100644 --- a/internal/project/service.go +++ b/internal/project/service.go @@ -255,11 +255,8 @@ func (s *Service) OnWatchedFilesChanged(changes []lsproto.FileEvent) error { // !!! s.handleSourceMapProjects(info) } } else { - if change.Type != lsproto.FileChangeTypeCreated { - panic("unexpected file change type") - } for _, project := range s.configuredProjects { - project.onWatchedFileCreated(fileName) + project.onWatchEventForNilScriptInfo(fileName) } } } diff --git a/internal/project/watch.go b/internal/project/watch.go new file mode 100644 index 0000000000..21f519607a --- /dev/null +++ b/internal/project/watch.go @@ -0,0 +1,61 @@ +package project + +import ( + "slices" + + "github.com/microsoft/typescript-go/internal/lsp/lsproto" +) + +const ( + fileGlobPattern = "*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,json}" + recursiveFileGlobPattern = "**/*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,json}" +) + +type watchedFiles[T any] struct { + client Client + getGlobs func(data T) []string + watchKind lsproto.WatchKind + + data T + globs []string + watcherID WatcherHandle +} + +func newWatchedFiles[T any](client Client, watchKind lsproto.WatchKind, getGlobs func(data T) []string) *watchedFiles[T] { + return &watchedFiles[T]{ + client: client, + watchKind: watchKind, + getGlobs: getGlobs, + } +} + +func (w *watchedFiles[T]) update(newData T) (updated bool, err error) { + newGlobs := w.getGlobs(newData) + w.data = newData + if slices.Equal(w.globs, newGlobs) { + return false, nil + } + + w.globs = newGlobs + if w.watcherID != "" { + if err := w.client.UnwatchFiles(w.watcherID); err != nil { + return false, err + } + } + + watchers := make([]lsproto.FileSystemWatcher, 0, len(newGlobs)) + for _, glob := range newGlobs { + watchers = append(watchers, lsproto.FileSystemWatcher{ + GlobPattern: lsproto.PatternOrRelativePattern{ + Pattern: &glob, + }, + Kind: &w.watchKind, + }) + } + watcherID, err := w.client.WatchFiles(watchers) + if err != nil { + return false, err + } + w.watcherID = watcherID + return true, nil +} From de902c50d7e794d279b37b9c68b4ec636e7f72fb Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Thu, 17 Apr 2025 14:01:19 -0700 Subject: [PATCH 05/16] Fix nil exception on inferred projects --- internal/project/service_test.go | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/internal/project/service_test.go b/internal/project/service_test.go index 3b6ef60e4c..8f0b044dfa 100644 --- a/internal/project/service_test.go +++ b/internal/project/service_test.go @@ -235,7 +235,7 @@ func TestService(t *testing.T) { t.Run("change open file", func(t *testing.T) { t.Parallel() - service, host := setup(files) + service, host := projecttestutil.Setup(files) service.OpenFile("/home/projects/TS/p1/src/x.ts", files["/home/projects/TS/p1/src/x.ts"], core.ScriptKindTS, "") service.OpenFile("/home/projects/TS/p1/src/index.ts", files["/home/projects/TS/p1/src/index.ts"], core.ScriptKindTS, "") _, project := service.EnsureDefaultProjectForFile("/home/projects/TS/p1/src/index.ts") @@ -243,7 +243,7 @@ func TestService(t *testing.T) { filesCopy := maps.Clone(files) filesCopy["/home/projects/TS/p1/src/x.ts"] = `export const x = 2;` - host.replaceFS(filesCopy) + host.ReplaceFS(filesCopy) service.OnWatchedFilesChanged([]lsproto.FileEvent{ { Type: lsproto.FileChangeTypeChanged, @@ -256,14 +256,14 @@ func TestService(t *testing.T) { t.Run("change closed program file", func(t *testing.T) { t.Parallel() - service, host := setup(files) + service, host := projecttestutil.Setup(files) service.OpenFile("/home/projects/TS/p1/src/index.ts", files["/home/projects/TS/p1/src/index.ts"], core.ScriptKindTS, "") _, project := service.EnsureDefaultProjectForFile("/home/projects/TS/p1/src/index.ts") programBefore := project.GetProgram() filesCopy := maps.Clone(files) filesCopy["/home/projects/TS/p1/src/x.ts"] = `export const x = 2;` - host.replaceFS(filesCopy) + host.ReplaceFS(filesCopy) service.OnWatchedFilesChanged([]lsproto.FileEvent{ { Type: lsproto.FileChangeTypeChanged, @@ -289,7 +289,7 @@ func TestService(t *testing.T) { let y: number = x;`, } - service, host := setup(files) + service, host := projecttestutil.Setup(files) service.OpenFile("/home/projects/TS/p1/src/index.ts", files["/home/projects/TS/p1/src/index.ts"], core.ScriptKindTS, "") _, project := service.EnsureDefaultProjectForFile("/home/projects/TS/p1/src/index.ts") program := project.GetProgram() @@ -302,7 +302,7 @@ func TestService(t *testing.T) { "strict": true } }` - host.replaceFS(filesCopy) + host.ReplaceFS(filesCopy) service.OnWatchedFilesChanged([]lsproto.FileEvent{ { Type: lsproto.FileChangeTypeChanged, @@ -326,7 +326,7 @@ func TestService(t *testing.T) { "/home/projects/TS/p1/src/x.ts": `export declare const x: number | undefined;`, "/home/projects/TS/p1/src/index.ts": `import { x } from "./x";`, } - service, host := setup(files) + service, host := projecttestutil.Setup(files) service.OpenFile("/home/projects/TS/p1/src/index.ts", files["/home/projects/TS/p1/src/index.ts"], core.ScriptKindTS, "") _, project := service.EnsureDefaultProjectForFile("/home/projects/TS/p1/src/index.ts") program := project.GetProgram() @@ -334,7 +334,7 @@ func TestService(t *testing.T) { filesCopy := maps.Clone(files) delete(filesCopy, "/home/projects/TS/p1/src/x.ts") - host.replaceFS(filesCopy) + host.ReplaceFS(filesCopy) service.OnWatchedFilesChanged([]lsproto.FileEvent{ { Type: lsproto.FileChangeTypeDeleted, @@ -359,7 +359,7 @@ func TestService(t *testing.T) { "/home/projects/TS/p1/src/index.ts": `let x = 2;`, "/home/projects/TS/p1/src/x.ts": `let y = x;`, } - service, host := setup(files) + service, host := projecttestutil.Setup(files) service.OpenFile("/home/projects/TS/p1/src/x.ts", files["/home/projects/TS/p1/src/x.ts"], core.ScriptKindTS, "") _, project := service.EnsureDefaultProjectForFile("/home/projects/TS/p1/src/x.ts") program := project.GetProgram() @@ -367,7 +367,7 @@ func TestService(t *testing.T) { filesCopy := maps.Clone(files) delete(filesCopy, "/home/projects/TS/p1/src/index.ts") - host.replaceFS(filesCopy) + host.ReplaceFS(filesCopy) service.OnWatchedFilesChanged([]lsproto.FileEvent{ { Type: lsproto.FileChangeTypeDeleted, @@ -390,7 +390,7 @@ func TestService(t *testing.T) { }`, "/home/projects/TS/p1/src/index.ts": `import { y } from "./y";`, } - service, host := setup(files) + service, host := projecttestutil.Setup(files) service.OpenFile("/home/projects/TS/p1/src/index.ts", files["/home/projects/TS/p1/src/index.ts"], core.ScriptKindTS, "") _, project := service.EnsureDefaultProjectForFile("/home/projects/TS/p1/src/index.ts") program := project.GetProgram() @@ -401,7 +401,7 @@ func TestService(t *testing.T) { // Add the missing file filesCopy := maps.Clone(files) filesCopy["/home/projects/TS/p1/src/y.ts"] = `export const y = 1;` - host.replaceFS(filesCopy) + host.ReplaceFS(filesCopy) service.OnWatchedFilesChanged([]lsproto.FileEvent{ { Type: lsproto.FileChangeTypeCreated, @@ -426,7 +426,7 @@ func TestService(t *testing.T) { }`, "/home/projects/TS/p1/src/index.ts": `import { z } from "./z";`, } - service, host := setup(files) + service, host := projecttestutil.Setup(files) service.OpenFile("/home/projects/TS/p1/src/index.ts", files["/home/projects/TS/p1/src/index.ts"], core.ScriptKindTS, "") _, project := service.EnsureDefaultProjectForFile("/home/projects/TS/p1/src/index.ts") program := project.GetProgram() @@ -437,7 +437,7 @@ func TestService(t *testing.T) { // Add a new file through wildcard inclusion filesCopy := maps.Clone(files) filesCopy["/home/projects/TS/p1/src/z.ts"] = `export const z = 1;` - host.replaceFS(filesCopy) + host.ReplaceFS(filesCopy) service.OnWatchedFilesChanged([]lsproto.FileEvent{ { Type: lsproto.FileChangeTypeCreated, From 1a821711fdea8695a3e0f2139683d39ae47dade2 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Thu, 17 Apr 2025 14:01:19 -0700 Subject: [PATCH 06/16] Fix nil exception on inferred projects --- internal/project/project.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/internal/project/project.go b/internal/project/project.go index 5e3652ea9f..14b56590c5 100644 --- a/internal/project/project.go +++ b/internal/project/project.go @@ -279,10 +279,12 @@ func (p *Project) updateWatchers() { rootFileGlobs := p.getRootFileWatchGlobs() failedLookupGlobs, affectingLocationGlobs := p.getModuleResolutionWatchGlobs() - if updated, err := p.rootFilesWatch.update(rootFileGlobs); err != nil { - p.log(fmt.Sprintf("Failed to update root file watch: %v", err)) - } else if updated { - p.log("Root file watches updated:\n" + formatFileList(rootFileGlobs, "\t", hr)) + if rootFileGlobs != nil { + if updated, err := p.rootFilesWatch.update(rootFileGlobs); err != nil { + p.log(fmt.Sprintf("Failed to update root file watch: %v", err)) + } else if updated { + p.log("Root file watches updated:\n" + formatFileList(rootFileGlobs, "\t", hr)) + } } if updated, err := p.failedLookupsWatch.update(failedLookupGlobs); err != nil { From 0d6ece0ec3b4b0e9bc19a205905cd64b1b4ec1f2 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Mon, 28 Apr 2025 15:50:38 -0700 Subject: [PATCH 07/16] Update for lsproto changes --- internal/lsp/server.go | 8 ++++---- internal/project/host.go | 2 +- internal/project/service.go | 4 ++-- internal/project/service_test.go | 14 +++++++------- internal/project/watch.go | 4 ++-- 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/internal/lsp/server.go b/internal/lsp/server.go index c633b013bf..06d9d1bc12 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -107,14 +107,14 @@ func (s *Server) Client() project.Client { } // WatchFiles implements project.Client. -func (s *Server) WatchFiles(watchers []lsproto.FileSystemWatcher) (project.WatcherHandle, error) { +func (s *Server) WatchFiles(watchers []*lsproto.FileSystemWatcher) (project.WatcherHandle, error) { watcherId := fmt.Sprintf("watcher-%d", s.watcherID) if err := s.sendRequest(lsproto.MethodClientRegisterCapability, &lsproto.RegistrationParams{ - Registrations: []lsproto.Registration{ + Registrations: []*lsproto.Registration{ { Id: watcherId, Method: string(lsproto.MethodWorkspaceDidChangeWatchedFiles), - RegisterOptions: ptrTo(lsproto.LSPAny(lsproto.DidChangeWatchedFilesRegistrationOptions{ + RegisterOptions: ptrTo(any(lsproto.DidChangeWatchedFilesRegistrationOptions{ Watchers: watchers, })), }, @@ -133,7 +133,7 @@ func (s *Server) WatchFiles(watchers []lsproto.FileSystemWatcher) (project.Watch func (s *Server) UnwatchFiles(handle project.WatcherHandle) error { if _, ok := s.watchers[handle]; ok { if err := s.sendRequest(lsproto.MethodClientUnregisterCapability, &lsproto.UnregistrationParams{ - Unregisterations: []lsproto.Unregistration{ + Unregisterations: []*lsproto.Unregistration{ { Id: string(handle), Method: string(lsproto.MethodWorkspaceDidChangeWatchedFiles), diff --git a/internal/project/host.go b/internal/project/host.go index baf8b512f5..b4dbb9dafa 100644 --- a/internal/project/host.go +++ b/internal/project/host.go @@ -8,7 +8,7 @@ import ( type WatcherHandle string type Client interface { - WatchFiles(watchers []lsproto.FileSystemWatcher) (WatcherHandle, error) + WatchFiles(watchers []*lsproto.FileSystemWatcher) (WatcherHandle, error) UnwatchFiles(handle WatcherHandle) error PublishDiagnostics(params *lsproto.PublishDiagnosticsParams) error } diff --git a/internal/project/service.go b/internal/project/service.go index b9da2946ba..05f887cfa5 100644 --- a/internal/project/service.go +++ b/internal/project/service.go @@ -232,7 +232,7 @@ func (s *Service) SourceFileCount() int { return s.documentRegistry.size() } -func (s *Service) OnWatchedFilesChanged(changes []lsproto.FileEvent) error { +func (s *Service) OnWatchedFilesChanged(changes []*lsproto.FileEvent) error { for _, change := range changes { fileName := ls.DocumentURIToFileName(change.Uri) path := s.toPath(fileName) @@ -298,7 +298,7 @@ func (s *Service) publishDiagnosticsForOpenFiles(project *Project) error { info := s.GetScriptInfoByPath(path) if slices.Contains(info.containingProjects, project) { diagnostics := project.LanguageService().GetDocumentDiagnostics(info.fileName) - lspDiagnostics := make([]lsproto.Diagnostic, len(diagnostics)) + lspDiagnostics := make([]*lsproto.Diagnostic, len(diagnostics)) for i, diagnostic := range diagnostics { if diag, err := s.converters.ToLSPDiagnostic(diagnostic); err != nil { return fmt.Errorf("error converting diagnostic: %w", err) diff --git a/internal/project/service_test.go b/internal/project/service_test.go index 8f0b044dfa..132f1220c7 100644 --- a/internal/project/service_test.go +++ b/internal/project/service_test.go @@ -244,7 +244,7 @@ func TestService(t *testing.T) { filesCopy := maps.Clone(files) filesCopy["/home/projects/TS/p1/src/x.ts"] = `export const x = 2;` host.ReplaceFS(filesCopy) - service.OnWatchedFilesChanged([]lsproto.FileEvent{ + service.OnWatchedFilesChanged([]*lsproto.FileEvent{ { Type: lsproto.FileChangeTypeChanged, Uri: "file:///home/projects/TS/p1/src/x.ts", @@ -264,7 +264,7 @@ func TestService(t *testing.T) { filesCopy := maps.Clone(files) filesCopy["/home/projects/TS/p1/src/x.ts"] = `export const x = 2;` host.ReplaceFS(filesCopy) - service.OnWatchedFilesChanged([]lsproto.FileEvent{ + service.OnWatchedFilesChanged([]*lsproto.FileEvent{ { Type: lsproto.FileChangeTypeChanged, Uri: "file:///home/projects/TS/p1/src/x.ts", @@ -303,7 +303,7 @@ func TestService(t *testing.T) { } }` host.ReplaceFS(filesCopy) - service.OnWatchedFilesChanged([]lsproto.FileEvent{ + service.OnWatchedFilesChanged([]*lsproto.FileEvent{ { Type: lsproto.FileChangeTypeChanged, Uri: "file:///home/projects/TS/p1/tsconfig.json", @@ -335,7 +335,7 @@ func TestService(t *testing.T) { filesCopy := maps.Clone(files) delete(filesCopy, "/home/projects/TS/p1/src/x.ts") host.ReplaceFS(filesCopy) - service.OnWatchedFilesChanged([]lsproto.FileEvent{ + service.OnWatchedFilesChanged([]*lsproto.FileEvent{ { Type: lsproto.FileChangeTypeDeleted, Uri: "file:///home/projects/TS/p1/src/x.ts", @@ -368,7 +368,7 @@ func TestService(t *testing.T) { filesCopy := maps.Clone(files) delete(filesCopy, "/home/projects/TS/p1/src/index.ts") host.ReplaceFS(filesCopy) - service.OnWatchedFilesChanged([]lsproto.FileEvent{ + service.OnWatchedFilesChanged([]*lsproto.FileEvent{ { Type: lsproto.FileChangeTypeDeleted, Uri: "file:///home/projects/TS/p1/src/index.ts", @@ -402,7 +402,7 @@ func TestService(t *testing.T) { filesCopy := maps.Clone(files) filesCopy["/home/projects/TS/p1/src/y.ts"] = `export const y = 1;` host.ReplaceFS(filesCopy) - service.OnWatchedFilesChanged([]lsproto.FileEvent{ + service.OnWatchedFilesChanged([]*lsproto.FileEvent{ { Type: lsproto.FileChangeTypeCreated, Uri: "file:///home/projects/TS/p1/src/y.ts", @@ -438,7 +438,7 @@ func TestService(t *testing.T) { filesCopy := maps.Clone(files) filesCopy["/home/projects/TS/p1/src/z.ts"] = `export const z = 1;` host.ReplaceFS(filesCopy) - service.OnWatchedFilesChanged([]lsproto.FileEvent{ + service.OnWatchedFilesChanged([]*lsproto.FileEvent{ { Type: lsproto.FileChangeTypeCreated, Uri: "file:///home/projects/TS/p1/src/z.ts", diff --git a/internal/project/watch.go b/internal/project/watch.go index 21f519607a..208898f7c6 100644 --- a/internal/project/watch.go +++ b/internal/project/watch.go @@ -43,9 +43,9 @@ func (w *watchedFiles[T]) update(newData T) (updated bool, err error) { } } - watchers := make([]lsproto.FileSystemWatcher, 0, len(newGlobs)) + watchers := make([]*lsproto.FileSystemWatcher, 0, len(newGlobs)) for _, glob := range newGlobs { - watchers = append(watchers, lsproto.FileSystemWatcher{ + watchers = append(watchers, &lsproto.FileSystemWatcher{ GlobPattern: lsproto.PatternOrRelativePattern{ Pattern: &glob, }, From 1dbb03b82bf10907aa1e0c3af624051585a49366 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Mon, 28 Apr 2025 15:55:29 -0700 Subject: [PATCH 08/16] Format --- internal/lsp/server.go | 6 ++++-- internal/project/project.go | 3 +-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/internal/lsp/server.go b/internal/lsp/server.go index 06d9d1bc12..9f5f709dc3 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -44,8 +44,10 @@ func NewServer(opts *ServerOptions) *Server { } } -var _ project.ServiceHost = (*Server)(nil) -var _ project.Client = (*Server)(nil) +var ( + _ project.ServiceHost = (*Server)(nil) + _ project.Client = (*Server)(nil) +) type Server struct { r *lsproto.BaseReader diff --git a/internal/project/project.go b/internal/project/project.go index 14b56590c5..1ac6c64b55 100644 --- a/internal/project/project.go +++ b/internal/project/project.go @@ -3,11 +3,10 @@ package project import ( "fmt" "maps" + "slices" "strings" "sync" - "slices" - "github.com/microsoft/typescript-go/internal/ast" "github.com/microsoft/typescript-go/internal/collections" "github.com/microsoft/typescript-go/internal/compiler" From 5785405422b04ade847f94f50c7107772ed382c4 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Mon, 28 Apr 2025 16:02:34 -0700 Subject: [PATCH 09/16] Lint --- internal/lsp/server.go | 5 +- internal/project/service.go | 4 +- internal/project/service_test.go | 142 +++++++++++++++---------------- internal/project/watch.go | 2 +- 4 files changed, 77 insertions(+), 76 deletions(-) diff --git a/internal/lsp/server.go b/internal/lsp/server.go index 9f5f709dc3..1da871f573 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -202,7 +202,7 @@ func (s *Server) read() (*lsproto.RequestMessage, error) { req := &lsproto.RequestMessage{} if err := json.Unmarshal(data, req); err != nil { res := &lsproto.ResponseMessage{} - if err := json.Unmarshal(data, res); err == nil { + if err = json.Unmarshal(data, res); err == nil { // !!! TODO: handle response return nil, nil } @@ -425,8 +425,7 @@ func (s *Server) handleDidClose(req *lsproto.RequestMessage) error { func (s *Server) handleDidChangeWatchedFiles(req *lsproto.RequestMessage) error { params := req.Params.(*lsproto.DidChangeWatchedFilesParams) - s.projectService.OnWatchedFilesChanged(params.Changes) - return nil + return s.projectService.OnWatchedFilesChanged(params.Changes) } func (s *Server) handleDocumentDiagnostic(req *lsproto.RequestMessage) error { diff --git a/internal/project/service.go b/internal/project/service.go index 05f887cfa5..59759de23f 100644 --- a/internal/project/service.go +++ b/internal/project/service.go @@ -263,7 +263,9 @@ func (s *Service) OnWatchedFilesChanged(changes []*lsproto.FileEvent) error { for _, project := range s.configuredProjects { if project.updateIfDirty() { - s.publishDiagnosticsForOpenFiles(project) + if err := s.publishDiagnosticsForOpenFiles(project); err != nil { + return err + } } } return nil diff --git a/internal/project/service_test.go b/internal/project/service_test.go index 132f1220c7..22e9b0e816 100644 --- a/internal/project/service_test.go +++ b/internal/project/service_test.go @@ -19,7 +19,7 @@ func TestService(t *testing.T) { t.Skip("bundled files are not embedded") } - files := map[string]string{ + defaultFiles := map[string]string{ "/home/projects/TS/p1/tsconfig.json": `{ "compilerOptions": { "noLib": true, @@ -37,9 +37,9 @@ func TestService(t *testing.T) { t.Parallel() t.Run("create configured project", func(t *testing.T) { t.Parallel() - service, _ := projecttestutil.Setup(files) + service, _ := projecttestutil.Setup(defaultFiles) assert.Equal(t, len(service.Projects()), 0) - service.OpenFile("/home/projects/TS/p1/src/index.ts", files["/home/projects/TS/p1/src/index.ts"], core.ScriptKindTS, "") + service.OpenFile("/home/projects/TS/p1/src/index.ts", defaultFiles["/home/projects/TS/p1/src/index.ts"], core.ScriptKindTS, "") assert.Equal(t, len(service.Projects()), 1) p := service.Projects()[0] assert.Equal(t, p.Kind(), project.KindConfigured) @@ -50,8 +50,8 @@ func TestService(t *testing.T) { t.Run("create inferred project", func(t *testing.T) { t.Parallel() - service, _ := projecttestutil.Setup(files) - service.OpenFile("/home/projects/TS/p1/config.ts", files["/home/projects/TS/p1/config.ts"], core.ScriptKindTS, "") + service, _ := projecttestutil.Setup(defaultFiles) + service.OpenFile("/home/projects/TS/p1/config.ts", defaultFiles["/home/projects/TS/p1/config.ts"], core.ScriptKindTS, "") // Find tsconfig, load, notice config.ts is not included, create inferred project assert.Equal(t, len(service.Projects()), 2) _, proj := service.EnsureDefaultProjectForFile("/home/projects/TS/p1/config.ts") @@ -60,8 +60,8 @@ func TestService(t *testing.T) { t.Run("inferred project for in-memory files", func(t *testing.T) { t.Parallel() - service, _ := projecttestutil.Setup(files) - service.OpenFile("/home/projects/TS/p1/config.ts", files["/home/projects/TS/p1/config.ts"], core.ScriptKindTS, "") + service, _ := projecttestutil.Setup(defaultFiles) + service.OpenFile("/home/projects/TS/p1/config.ts", defaultFiles["/home/projects/TS/p1/config.ts"], core.ScriptKindTS, "") service.OpenFile("^/untitled/ts-nul-authority/Untitled-1", "x", core.ScriptKindTS, "") service.OpenFile("^/untitled/ts-nul-authority/Untitled-2", "y", core.ScriptKindTS, "") assert.Equal(t, len(service.Projects()), 2) @@ -89,8 +89,8 @@ func TestService(t *testing.T) { t.Parallel() t.Run("update script info eagerly and program lazily", func(t *testing.T) { t.Parallel() - service, _ := projecttestutil.Setup(files) - service.OpenFile("/home/projects/TS/p1/src/x.ts", files["/home/projects/TS/p1/src/x.ts"], core.ScriptKindTS, "") + service, _ := projecttestutil.Setup(defaultFiles) + service.OpenFile("/home/projects/TS/p1/src/x.ts", defaultFiles["/home/projects/TS/p1/src/x.ts"], core.ScriptKindTS, "") info, proj := service.EnsureDefaultProjectForFile("/home/projects/TS/p1/src/x.ts") programBefore := proj.GetProgram() service.ChangeFile("/home/projects/TS/p1/src/x.ts", []ls.TextChange{{TextRange: core.NewTextRange(17, 18), NewText: "2"}}) @@ -102,8 +102,8 @@ func TestService(t *testing.T) { t.Run("unchanged source files are reused", func(t *testing.T) { t.Parallel() - service, _ := projecttestutil.Setup(files) - service.OpenFile("/home/projects/TS/p1/src/x.ts", files["/home/projects/TS/p1/src/x.ts"], core.ScriptKindTS, "") + service, _ := projecttestutil.Setup(defaultFiles) + service.OpenFile("/home/projects/TS/p1/src/x.ts", defaultFiles["/home/projects/TS/p1/src/x.ts"], core.ScriptKindTS, "") _, proj := service.EnsureDefaultProjectForFile("/home/projects/TS/p1/src/x.ts") programBefore := proj.GetProgram() indexFileBefore := programBefore.GetSourceFile("/home/projects/TS/p1/src/index.ts") @@ -113,10 +113,10 @@ func TestService(t *testing.T) { t.Run("change can pull in new files", func(t *testing.T) { t.Parallel() - filesCopy := maps.Clone(files) - filesCopy["/home/projects/TS/p1/y.ts"] = `export const y = 2;` - service, _ := projecttestutil.Setup(filesCopy) - service.OpenFile("/home/projects/TS/p1/src/index.ts", filesCopy["/home/projects/TS/p1/src/index.ts"], core.ScriptKindTS, "") + files := maps.Clone(defaultFiles) + files["/home/projects/TS/p1/y.ts"] = `export const y = 2;` + service, _ := projecttestutil.Setup(files) + service.OpenFile("/home/projects/TS/p1/src/index.ts", files["/home/projects/TS/p1/src/index.ts"], core.ScriptKindTS, "") assert.Check(t, service.GetScriptInfo("/home/projects/TS/p1/y.ts") == nil) service.ChangeFile("/home/projects/TS/p1/src/index.ts", []ls.TextChange{{TextRange: core.NewTextRange(0, 0), NewText: `import { y } from "../y";\n`}}) @@ -130,23 +130,23 @@ func TestService(t *testing.T) { t.Parallel() t.Run("delete a file, close it, recreate it", func(t *testing.T) { t.Parallel() - service, host := projecttestutil.Setup(files) - service.OpenFile("/home/projects/TS/p1/src/x.ts", files["/home/projects/TS/p1/src/x.ts"], core.ScriptKindTS, "") - service.OpenFile("/home/projects/TS/p1/src/index.ts", files["/home/projects/TS/p1/src/index.ts"], core.ScriptKindTS, "") + service, host := projecttestutil.Setup(defaultFiles) + service.OpenFile("/home/projects/TS/p1/src/x.ts", defaultFiles["/home/projects/TS/p1/src/x.ts"], core.ScriptKindTS, "") + service.OpenFile("/home/projects/TS/p1/src/index.ts", defaultFiles["/home/projects/TS/p1/src/index.ts"], core.ScriptKindTS, "") assert.Equal(t, service.SourceFileCount(), 2) - filesCopy := maps.Clone(files) - delete(filesCopy, "/home/projects/TS/p1/src/x.ts") - host.ReplaceFS(filesCopy) + files := maps.Clone(defaultFiles) + delete(files, "/home/projects/TS/p1/src/x.ts") + host.ReplaceFS(files) service.CloseFile("/home/projects/TS/p1/src/x.ts") assert.Check(t, service.GetScriptInfo("/home/projects/TS/p1/src/x.ts") == nil) assert.Check(t, service.Projects()[0].GetProgram().GetSourceFile("/home/projects/TS/p1/src/x.ts") == nil) assert.Equal(t, service.SourceFileCount(), 1) - filesCopy["/home/projects/TS/p1/src/x.ts"] = `` - host.ReplaceFS(filesCopy) - service.OpenFile("/home/projects/TS/p1/src/x.ts", filesCopy["/home/projects/TS/p1/src/x.ts"], core.ScriptKindTS, "") + files["/home/projects/TS/p1/src/x.ts"] = `` + host.ReplaceFS(files) + service.OpenFile("/home/projects/TS/p1/src/x.ts", files["/home/projects/TS/p1/src/x.ts"], core.ScriptKindTS, "") assert.Equal(t, service.GetScriptInfo("/home/projects/TS/p1/src/x.ts").Text(), "") assert.Check(t, service.Projects()[0].GetProgram().GetSourceFile("/home/projects/TS/p1/src/x.ts") != nil) assert.Equal(t, service.Projects()[0].GetProgram().GetSourceFile("/home/projects/TS/p1/src/x.ts").Text(), "") @@ -157,22 +157,22 @@ func TestService(t *testing.T) { t.Parallel() t.Run("delete a file, close it, recreate it", func(t *testing.T) { t.Parallel() - filesCopy := maps.Clone(files) - delete(filesCopy, "/home/projects/TS/p1/tsconfig.json") - service, host := projecttestutil.Setup(filesCopy) - service.OpenFile("/home/projects/TS/p1/src/x.ts", files["/home/projects/TS/p1/src/x.ts"], core.ScriptKindTS, "") - service.OpenFile("/home/projects/TS/p1/src/index.ts", files["/home/projects/TS/p1/src/index.ts"], core.ScriptKindTS, "") + files := maps.Clone(defaultFiles) + delete(files, "/home/projects/TS/p1/tsconfig.json") + service, host := projecttestutil.Setup(files) + service.OpenFile("/home/projects/TS/p1/src/x.ts", defaultFiles["/home/projects/TS/p1/src/x.ts"], core.ScriptKindTS, "") + service.OpenFile("/home/projects/TS/p1/src/index.ts", defaultFiles["/home/projects/TS/p1/src/index.ts"], core.ScriptKindTS, "") - delete(filesCopy, "/home/projects/TS/p1/src/x.ts") - host.ReplaceFS(filesCopy) + delete(files, "/home/projects/TS/p1/src/x.ts") + host.ReplaceFS(files) service.CloseFile("/home/projects/TS/p1/src/x.ts") assert.Check(t, service.GetScriptInfo("/home/projects/TS/p1/src/x.ts") == nil) assert.Check(t, service.Projects()[0].GetProgram().GetSourceFile("/home/projects/TS/p1/src/x.ts") == nil) - filesCopy["/home/projects/TS/p1/src/x.ts"] = `` - host.ReplaceFS(filesCopy) - service.OpenFile("/home/projects/TS/p1/src/x.ts", filesCopy["/home/projects/TS/p1/src/x.ts"], core.ScriptKindTS, "") + files["/home/projects/TS/p1/src/x.ts"] = `` + host.ReplaceFS(files) + service.OpenFile("/home/projects/TS/p1/src/x.ts", files["/home/projects/TS/p1/src/x.ts"], core.ScriptKindTS, "") assert.Equal(t, service.GetScriptInfo("/home/projects/TS/p1/src/x.ts").Text(), "") assert.Check(t, service.Projects()[0].GetProgram().GetSourceFile("/home/projects/TS/p1/src/x.ts") != nil) assert.Equal(t, service.Projects()[0].GetProgram().GetSourceFile("/home/projects/TS/p1/src/x.ts").Text(), "") @@ -184,8 +184,8 @@ func TestService(t *testing.T) { t.Parallel() t.Run("projects with similar options share source files", func(t *testing.T) { t.Parallel() - filesCopy := maps.Clone(files) - filesCopy["/home/projects/TS/p2/tsconfig.json"] = `{ + files := maps.Clone(defaultFiles) + files["/home/projects/TS/p2/tsconfig.json"] = `{ "compilerOptions": { "noLib": true, "module": "nodenext", @@ -193,10 +193,10 @@ func TestService(t *testing.T) { "noCheck": true // Added }, }` - filesCopy["/home/projects/TS/p2/src/index.ts"] = `import { x } from "../../p1/src/x";` - service, _ := projecttestutil.Setup(filesCopy) - service.OpenFile("/home/projects/TS/p1/src/index.ts", filesCopy["/home/projects/TS/p1/src/index.ts"], core.ScriptKindTS, "") - service.OpenFile("/home/projects/TS/p2/src/index.ts", filesCopy["/home/projects/TS/p2/src/index.ts"], core.ScriptKindTS, "") + files["/home/projects/TS/p2/src/index.ts"] = `import { x } from "../../p1/src/x";` + service, _ := projecttestutil.Setup(files) + service.OpenFile("/home/projects/TS/p1/src/index.ts", files["/home/projects/TS/p1/src/index.ts"], core.ScriptKindTS, "") + service.OpenFile("/home/projects/TS/p2/src/index.ts", files["/home/projects/TS/p2/src/index.ts"], core.ScriptKindTS, "") assert.Equal(t, len(service.Projects()), 2) _, p1 := service.EnsureDefaultProjectForFile("/home/projects/TS/p1/src/index.ts") _, p2 := service.EnsureDefaultProjectForFile("/home/projects/TS/p2/src/index.ts") @@ -209,17 +209,17 @@ func TestService(t *testing.T) { t.Run("projects with different options do not share source files", func(t *testing.T) { t.Parallel() - filesCopy := maps.Clone(files) - filesCopy["/home/projects/TS/p2/tsconfig.json"] = `{ + files := maps.Clone(defaultFiles) + files["/home/projects/TS/p2/tsconfig.json"] = `{ "compilerOptions": { "module": "nodenext", "jsx": "react" } }` - filesCopy["/home/projects/TS/p2/src/index.ts"] = `import { x } from "../../p1/src/x";` - service, _ := projecttestutil.Setup(filesCopy) - service.OpenFile("/home/projects/TS/p1/src/index.ts", filesCopy["/home/projects/TS/p1/src/index.ts"], core.ScriptKindTS, "") - service.OpenFile("/home/projects/TS/p2/src/index.ts", filesCopy["/home/projects/TS/p2/src/index.ts"], core.ScriptKindTS, "") + files["/home/projects/TS/p2/src/index.ts"] = `import { x } from "../../p1/src/x";` + service, _ := projecttestutil.Setup(files) + service.OpenFile("/home/projects/TS/p1/src/index.ts", files["/home/projects/TS/p1/src/index.ts"], core.ScriptKindTS, "") + service.OpenFile("/home/projects/TS/p2/src/index.ts", files["/home/projects/TS/p2/src/index.ts"], core.ScriptKindTS, "") assert.Equal(t, len(service.Projects()), 2) _, p1 := service.EnsureDefaultProjectForFile("/home/projects/TS/p1/src/index.ts") _, p2 := service.EnsureDefaultProjectForFile("/home/projects/TS/p2/src/index.ts") @@ -235,41 +235,41 @@ func TestService(t *testing.T) { t.Run("change open file", func(t *testing.T) { t.Parallel() - service, host := projecttestutil.Setup(files) - service.OpenFile("/home/projects/TS/p1/src/x.ts", files["/home/projects/TS/p1/src/x.ts"], core.ScriptKindTS, "") - service.OpenFile("/home/projects/TS/p1/src/index.ts", files["/home/projects/TS/p1/src/index.ts"], core.ScriptKindTS, "") + service, host := projecttestutil.Setup(defaultFiles) + service.OpenFile("/home/projects/TS/p1/src/x.ts", defaultFiles["/home/projects/TS/p1/src/x.ts"], core.ScriptKindTS, "") + service.OpenFile("/home/projects/TS/p1/src/index.ts", defaultFiles["/home/projects/TS/p1/src/index.ts"], core.ScriptKindTS, "") _, project := service.EnsureDefaultProjectForFile("/home/projects/TS/p1/src/index.ts") programBefore := project.GetProgram() - filesCopy := maps.Clone(files) - filesCopy["/home/projects/TS/p1/src/x.ts"] = `export const x = 2;` - host.ReplaceFS(filesCopy) - service.OnWatchedFilesChanged([]*lsproto.FileEvent{ + files := maps.Clone(defaultFiles) + files["/home/projects/TS/p1/src/x.ts"] = `export const x = 2;` + host.ReplaceFS(files) + assert.NilError(t, service.OnWatchedFilesChanged([]*lsproto.FileEvent{ { Type: lsproto.FileChangeTypeChanged, Uri: "file:///home/projects/TS/p1/src/x.ts", }, - }) + })) assert.Equal(t, programBefore, project.GetProgram()) }) t.Run("change closed program file", func(t *testing.T) { t.Parallel() - service, host := projecttestutil.Setup(files) - service.OpenFile("/home/projects/TS/p1/src/index.ts", files["/home/projects/TS/p1/src/index.ts"], core.ScriptKindTS, "") + service, host := projecttestutil.Setup(defaultFiles) + service.OpenFile("/home/projects/TS/p1/src/index.ts", defaultFiles["/home/projects/TS/p1/src/index.ts"], core.ScriptKindTS, "") _, project := service.EnsureDefaultProjectForFile("/home/projects/TS/p1/src/index.ts") programBefore := project.GetProgram() - filesCopy := maps.Clone(files) - filesCopy["/home/projects/TS/p1/src/x.ts"] = `export const x = 2;` - host.ReplaceFS(filesCopy) - service.OnWatchedFilesChanged([]*lsproto.FileEvent{ + files := maps.Clone(defaultFiles) + files["/home/projects/TS/p1/src/x.ts"] = `export const x = 2;` + host.ReplaceFS(files) + assert.NilError(t, service.OnWatchedFilesChanged([]*lsproto.FileEvent{ { Type: lsproto.FileChangeTypeChanged, Uri: "file:///home/projects/TS/p1/src/x.ts", }, - }) + })) assert.Check(t, project.GetProgram() != programBefore) }) @@ -303,12 +303,12 @@ func TestService(t *testing.T) { } }` host.ReplaceFS(filesCopy) - service.OnWatchedFilesChanged([]*lsproto.FileEvent{ + assert.NilError(t, service.OnWatchedFilesChanged([]*lsproto.FileEvent{ { Type: lsproto.FileChangeTypeChanged, Uri: "file:///home/projects/TS/p1/tsconfig.json", }, - }) + })) program = project.GetProgram() assert.Equal(t, len(program.GetSemanticDiagnostics(program.GetSourceFile("/home/projects/TS/p1/src/index.ts"))), 1) @@ -335,12 +335,12 @@ func TestService(t *testing.T) { filesCopy := maps.Clone(files) delete(filesCopy, "/home/projects/TS/p1/src/x.ts") host.ReplaceFS(filesCopy) - service.OnWatchedFilesChanged([]*lsproto.FileEvent{ + assert.NilError(t, service.OnWatchedFilesChanged([]*lsproto.FileEvent{ { Type: lsproto.FileChangeTypeDeleted, Uri: "file:///home/projects/TS/p1/src/x.ts", }, - }) + })) program = project.GetProgram() assert.Equal(t, len(program.GetSemanticDiagnostics(program.GetSourceFile("/home/projects/TS/p1/src/index.ts"))), 1) @@ -368,12 +368,12 @@ func TestService(t *testing.T) { filesCopy := maps.Clone(files) delete(filesCopy, "/home/projects/TS/p1/src/index.ts") host.ReplaceFS(filesCopy) - service.OnWatchedFilesChanged([]*lsproto.FileEvent{ + assert.NilError(t, service.OnWatchedFilesChanged([]*lsproto.FileEvent{ { Type: lsproto.FileChangeTypeDeleted, Uri: "file:///home/projects/TS/p1/src/index.ts", }, - }) + })) program = project.GetProgram() assert.Equal(t, len(program.GetSemanticDiagnostics(program.GetSourceFile("/home/projects/TS/p1/src/x.ts"))), 1) @@ -402,12 +402,12 @@ func TestService(t *testing.T) { filesCopy := maps.Clone(files) filesCopy["/home/projects/TS/p1/src/y.ts"] = `export const y = 1;` host.ReplaceFS(filesCopy) - service.OnWatchedFilesChanged([]*lsproto.FileEvent{ + assert.NilError(t, service.OnWatchedFilesChanged([]*lsproto.FileEvent{ { Type: lsproto.FileChangeTypeCreated, Uri: "file:///home/projects/TS/p1/src/y.ts", }, - }) + })) // Error should be resolved program = project.GetProgram() @@ -438,12 +438,12 @@ func TestService(t *testing.T) { filesCopy := maps.Clone(files) filesCopy["/home/projects/TS/p1/src/z.ts"] = `export const z = 1;` host.ReplaceFS(filesCopy) - service.OnWatchedFilesChanged([]*lsproto.FileEvent{ + assert.NilError(t, service.OnWatchedFilesChanged([]*lsproto.FileEvent{ { Type: lsproto.FileChangeTypeCreated, Uri: "file:///home/projects/TS/p1/src/z.ts", }, - }) + })) // Error should be resolved and the new file should be included in the program program = project.GetProgram() diff --git a/internal/project/watch.go b/internal/project/watch.go index 208898f7c6..18db3a8c91 100644 --- a/internal/project/watch.go +++ b/internal/project/watch.go @@ -38,7 +38,7 @@ func (w *watchedFiles[T]) update(newData T) (updated bool, err error) { w.globs = newGlobs if w.watcherID != "" { - if err := w.client.UnwatchFiles(w.watcherID); err != nil { + if err = w.client.UnwatchFiles(w.watcherID); err != nil { return false, err } } From 0ee13125de55a1be0b7487d612642a3dd8f2d68c Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Tue, 6 May 2025 09:45:08 -0700 Subject: [PATCH 10/16] Use diagnostic refresh instead of publish on watch changes --- internal/api/api.go | 5 ++++ internal/lsp/server.go | 18 ++++++++++--- internal/project/host.go | 2 +- internal/project/project.go | 7 ++--- internal/project/service.go | 51 ++++++++----------------------------- 5 files changed, 35 insertions(+), 48 deletions(-) diff --git a/internal/api/api.go b/internal/api/api.go index 8279e2481d..42191d6915 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -124,6 +124,11 @@ func (api *API) Client() project.Client { return nil } +// IsWatchEnabled implements ProjectHost. +func (api *API) IsWatchEnabled() bool { + return false +} + func (api *API) HandleRequest(id int, method string, payload []byte) ([]byte, error) { params, err := unmarshalPayload(method, payload) if err != nil { diff --git a/internal/lsp/server.go b/internal/lsp/server.go index 1da871f573..c4e1ec9203 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -151,9 +151,14 @@ func (s *Server) UnwatchFiles(handle project.WatcherHandle) error { return fmt.Errorf("no file watcher exists with ID %s", handle) } -// PublishDiagnostics implements project.Client. -func (s *Server) PublishDiagnostics(params *lsproto.PublishDiagnosticsParams) error { - return s.sendNotification(lsproto.MethodTextDocumentPublishDiagnostics, params) +// RefreshDiagnostics implements project.Client. +func (s *Server) RefreshDiagnostics() error { + if ptrIsTrue(s.initializeParams.Capabilities.Workspace.Diagnostics.RefreshSupport) { + if err := s.sendRequest(lsproto.MethodWorkspaceDiagnosticRefresh, nil); err != nil { + return fmt.Errorf("failed to refresh diagnostics: %w", err) + } + } + return nil } func (s *Server) Run() error { @@ -551,3 +556,10 @@ func codeFence(lang string, code string) string { func ptrTo[T any](v T) *T { return &v } + +func ptrIsTrue(v *bool) bool { + if v == nil { + return false + } + return *v +} diff --git a/internal/project/host.go b/internal/project/host.go index b4dbb9dafa..b9e9635e26 100644 --- a/internal/project/host.go +++ b/internal/project/host.go @@ -10,7 +10,7 @@ type WatcherHandle string type Client interface { WatchFiles(watchers []*lsproto.FileSystemWatcher) (WatcherHandle, error) UnwatchFiles(handle WatcherHandle) error - PublishDiagnostics(params *lsproto.PublishDiagnosticsParams) error + RefreshDiagnostics() error } type ServiceHost interface { diff --git a/internal/project/project.go b/internal/project/project.go index 1ac6c64b55..f3d75c18b0 100644 --- a/internal/project/project.go +++ b/internal/project/project.go @@ -45,6 +45,7 @@ type ProjectHost interface { Log(s string) PositionEncoding() lsproto.PositionEncodingKind + IsWatchEnabled() bool Client() Client } @@ -90,7 +91,7 @@ func NewConfiguredProject(configFileName string, configFilePath tspath.Path, hos project.configFilePath = configFilePath project.initialLoadPending = true client := host.Client() - if client != nil { + if host.IsWatchEnabled() && client != nil { project.rootFilesWatch = newWatchedFiles(client, lsproto.WatchKindChange|lsproto.WatchKindCreate|lsproto.WatchKindDelete, core.Identity) } return project @@ -117,7 +118,7 @@ func NewProject(name string, kind Kind, currentDirectory string, host ProjectHos UseCaseSensitiveFileNames: host.FS().UseCaseSensitiveFileNames(), } client := host.Client() - if client != nil { + if host.IsWatchEnabled() && client != nil { project.failedLookupsWatch = newWatchedFiles(client, lsproto.WatchKindCreate, func(data map[tspath.Path]string) []string { return slices.Sorted(maps.Values(data)) }) @@ -271,7 +272,7 @@ func (p *Project) getModuleResolutionWatchGlobs() (failedLookups map[tspath.Path func (p *Project) updateWatchers() { client := p.host.Client() - if client == nil { + if !p.host.IsWatchEnabled() || client == nil { return } diff --git a/internal/project/service.go b/internal/project/service.go index 59759de23f..72b02206bf 100644 --- a/internal/project/service.go +++ b/internal/project/service.go @@ -2,7 +2,6 @@ package project import ( "fmt" - "slices" "strings" "sync" @@ -135,10 +134,12 @@ func (s *Service) PositionEncoding() lsproto.PositionEncodingKind { // Client implements ProjectHost. func (s *Service) Client() Client { - if s.options.WatchEnabled { - return s.host.Client() - } - return nil + return s.host.Client() +} + +// IsWatchEnabled implements ProjectHost. +func (s *Service) IsWatchEnabled() bool { + return s.options.WatchEnabled } func (s *Service) Projects() []*Project { @@ -261,13 +262,11 @@ func (s *Service) OnWatchedFilesChanged(changes []*lsproto.FileEvent) error { } } - for _, project := range s.configuredProjects { - if project.updateIfDirty() { - if err := s.publishDiagnosticsForOpenFiles(project); err != nil { - return err - } - } + client := s.host.Client() + if client != nil { + return client.RefreshDiagnostics() } + return nil } @@ -290,36 +289,6 @@ func (s *Service) onConfigFileChanged(project *Project, changeKind lsproto.FileC return nil } -func (s *Service) publishDiagnosticsForOpenFiles(project *Project) error { - client := s.host.Client() - if client == nil { - return nil - } - - for path := range s.openFiles { - info := s.GetScriptInfoByPath(path) - if slices.Contains(info.containingProjects, project) { - diagnostics := project.LanguageService().GetDocumentDiagnostics(info.fileName) - lspDiagnostics := make([]*lsproto.Diagnostic, len(diagnostics)) - for i, diagnostic := range diagnostics { - if diag, err := s.converters.ToLSPDiagnostic(diagnostic); err != nil { - return fmt.Errorf("error converting diagnostic: %w", err) - } else { - lspDiagnostics[i] = diag - } - } - - if err := client.PublishDiagnostics(&lsproto.PublishDiagnosticsParams{ - Uri: ls.FileNameToDocumentURI(info.fileName), - Diagnostics: lspDiagnostics, - }); err != nil { - return fmt.Errorf("error publishing diagnostics: %w", err) - } - } - } - return nil -} - func (s *Service) ensureProjectStructureUpToDate() { var hasChanges bool for _, project := range s.configuredProjects { From e540e15d11bd9b7e976cc45bbd8222f521ec8806 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Thu, 8 May 2025 08:34:11 -0700 Subject: [PATCH 11/16] Small fixes --- internal/lsp/lsproto/jsonrpc.go | 7 +++---- internal/lsp/server.go | 17 ++++++++--------- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/internal/lsp/lsproto/jsonrpc.go b/internal/lsp/lsproto/jsonrpc.go index af9626082c..e909e9442e 100644 --- a/internal/lsp/lsproto/jsonrpc.go +++ b/internal/lsp/lsproto/jsonrpc.go @@ -72,10 +72,9 @@ type RequestMessage struct { func NewRequestMessage(method Method, id *ID, params any) *RequestMessage { return &RequestMessage{ - JSONRPC: JSONRPCVersion{}, - ID: id, - Method: method, - Params: params, + ID: id, + Method: method, + Params: params, } } diff --git a/internal/lsp/server.go b/internal/lsp/server.go index c4e1ec9203..2931f40178 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -40,7 +40,6 @@ func NewServer(opts *ServerOptions) *Server { newLine: opts.NewLine, fs: opts.FS, defaultLibraryPath: opts.DefaultLibraryPath, - watchers: make(map[project.WatcherHandle]struct{}), } } @@ -67,9 +66,9 @@ type Server struct { initializeParams *lsproto.InitializeParams positionEncoding lsproto.PositionEncodingKind - watcheEnabled bool + watchEnabled bool watcherID int - watchers map[project.WatcherHandle]struct{} + watchers core.Set[project.WatcherHandle] logger *project.Logger projectService *project.Service converters *ls.Converters @@ -102,7 +101,7 @@ func (s *Server) Trace(msg string) { // Client implements project.ServiceHost. func (s *Server) Client() project.Client { - if !s.watcheEnabled { + if !s.watchEnabled { return nil } return s @@ -126,14 +125,14 @@ func (s *Server) WatchFiles(watchers []*lsproto.FileSystemWatcher) (project.Watc } handle := project.WatcherHandle(watcherId) - s.watchers[handle] = struct{}{} + s.watchers.Add(handle) s.watcherID++ return handle, nil } // UnwatchFiles implements project.Client. func (s *Server) UnwatchFiles(handle project.WatcherHandle) error { - if _, ok := s.watchers[handle]; ok { + if s.watchers.Has(handle) { if err := s.sendRequest(lsproto.MethodClientUnregisterCapability, &lsproto.UnregistrationParams{ Unregisterations: []*lsproto.Unregistration{ { @@ -144,7 +143,7 @@ func (s *Server) UnwatchFiles(handle project.WatcherHandle) error { }); err != nil { return fmt.Errorf("failed to unregister file watcher: %w", err) } - delete(s.watchers, handle) + s.watchers.Delete(handle) return nil } @@ -364,13 +363,13 @@ func (s *Server) handleInitialize(req *lsproto.RequestMessage) error { func (s *Server) handleInitialized(req *lsproto.RequestMessage) error { if s.initializeParams.Capabilities.Workspace.DidChangeWatchedFiles != nil && *s.initializeParams.Capabilities.Workspace.DidChangeWatchedFiles.DynamicRegistration { - s.watcheEnabled = true + s.watchEnabled = true } s.logger = project.NewLogger([]io.Writer{s.stderr}, "" /*file*/, project.LogLevelVerbose) s.projectService = project.NewService(s, project.ServiceOptions{ Logger: s.logger, - WatchEnabled: s.watcheEnabled, + WatchEnabled: s.watchEnabled, PositionEncoding: s.positionEncoding, }) From 08c6506e6755475d6a1148c3fc6bc1ac1950f8b6 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Mon, 12 May 2025 11:15:08 -0700 Subject: [PATCH 12/16] =?UTF-8?q?Include=20missing=20files=20in=20program?= =?UTF-8?q?=E2=80=99s=20root=20file=20list?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/compiler/fileloader.go | 16 ++++++++++++---- internal/project/project.go | 8 +------- internal/project/service_test.go | 20 ++++++++++---------- internal/tsoptions/parsedcommandline.go | 1 + 4 files changed, 24 insertions(+), 21 deletions(-) diff --git a/internal/compiler/fileloader.go b/internal/compiler/fileloader.go index f4fc8c1ecf..3439169cf9 100644 --- a/internal/compiler/fileloader.go +++ b/internal/compiler/fileloader.go @@ -38,6 +38,7 @@ type fileLoader struct { type processedFiles struct { files []*ast.SourceFile + missingFiles []string resolvedModules map[tspath.Path]module.ModeAwareCache[*module.ResolvedModule] sourceFileMetaDatas map[tspath.Path]*ast.SourceFileMetaData jsxRuntimeImportSpecifiers map[tspath.Path]*jsxRuntimeImportSpecifier @@ -84,6 +85,7 @@ func processAllProgramFiles( totalFileCount := int(loader.totalFileCount.Load()) libFileCount := int(loader.libFileCount.Load()) + var missingFiles []string files := make([]*ast.SourceFile, 0, totalFileCount-libFileCount) libFiles := make([]*ast.SourceFile, 0, totalFileCount) // totalFileCount here since we append files to it later to construct the final list @@ -94,6 +96,10 @@ func processAllProgramFiles( for task := range loader.collectTasks(loader.rootTasks) { file := task.file + if file == nil { + missingFiles = append(missingFiles, task.normalizedFilePath) + continue + } if task.isLib { libFiles = append(libFiles, file) } else { @@ -189,10 +195,8 @@ func (p *fileLoader) collectTasksWorker(tasks []*parseTask, seen core.Set[*parse } } - if task.file != nil { - if !yield(task) { - return false - } + if !yield(task) { + return false } } return true @@ -244,6 +248,10 @@ func (t *parseTask) start(loader *fileLoader) { loader.wg.Queue(func() { file := loader.parseSourceFile(t.normalizedFilePath) + if file == nil { + return + } + t.file = file loader.wg.Queue(func() { t.metadata = loader.loadSourceFileMetaData(file.Path()) diff --git a/internal/project/project.go b/internal/project/project.go index f3d75c18b0..f48ec43185 100644 --- a/internal/project/project.go +++ b/internal/project/project.go @@ -158,13 +158,7 @@ func (p *Project) GetProjectVersion() int { // GetRootFileNames implements LanguageServiceHost. func (p *Project) GetRootFileNames() []string { - fileNames := make([]string, 0, p.rootFileNames.Size()) - for path, fileName := range p.rootFileNames.Entries() { - if p.host.GetScriptInfoByPath(path) != nil { - fileNames = append(fileNames, fileName) - } - } - return fileNames + return slices.Collect(p.rootFileNames.Values()) } // GetSourceFile implements LanguageServiceHost. diff --git a/internal/project/service_test.go b/internal/project/service_test.go index 22e9b0e816..2a34997129 100644 --- a/internal/project/service_test.go +++ b/internal/project/service_test.go @@ -293,7 +293,7 @@ func TestService(t *testing.T) { service.OpenFile("/home/projects/TS/p1/src/index.ts", files["/home/projects/TS/p1/src/index.ts"], core.ScriptKindTS, "") _, project := service.EnsureDefaultProjectForFile("/home/projects/TS/p1/src/index.ts") program := project.GetProgram() - assert.Equal(t, len(program.GetSemanticDiagnostics(program.GetSourceFile("/home/projects/TS/p1/src/index.ts"))), 0) + assert.Equal(t, len(program.GetSemanticDiagnostics(t.Context(), program.GetSourceFile("/home/projects/TS/p1/src/index.ts"))), 0) filesCopy := maps.Clone(files) filesCopy["/home/projects/TS/p1/tsconfig.json"] = `{ @@ -311,7 +311,7 @@ func TestService(t *testing.T) { })) program = project.GetProgram() - assert.Equal(t, len(program.GetSemanticDiagnostics(program.GetSourceFile("/home/projects/TS/p1/src/index.ts"))), 1) + assert.Equal(t, len(program.GetSemanticDiagnostics(t.Context(), program.GetSourceFile("/home/projects/TS/p1/src/index.ts"))), 1) }) t.Run("delete explicitly included file", func(t *testing.T) { @@ -330,7 +330,7 @@ func TestService(t *testing.T) { service.OpenFile("/home/projects/TS/p1/src/index.ts", files["/home/projects/TS/p1/src/index.ts"], core.ScriptKindTS, "") _, project := service.EnsureDefaultProjectForFile("/home/projects/TS/p1/src/index.ts") program := project.GetProgram() - assert.Equal(t, len(program.GetSemanticDiagnostics(program.GetSourceFile("/home/projects/TS/p1/src/index.ts"))), 0) + assert.Equal(t, len(program.GetSemanticDiagnostics(t.Context(), program.GetSourceFile("/home/projects/TS/p1/src/index.ts"))), 0) filesCopy := maps.Clone(files) delete(filesCopy, "/home/projects/TS/p1/src/x.ts") @@ -343,7 +343,7 @@ func TestService(t *testing.T) { })) program = project.GetProgram() - assert.Equal(t, len(program.GetSemanticDiagnostics(program.GetSourceFile("/home/projects/TS/p1/src/index.ts"))), 1) + assert.Equal(t, len(program.GetSemanticDiagnostics(t.Context(), program.GetSourceFile("/home/projects/TS/p1/src/index.ts"))), 1) assert.Check(t, program.GetSourceFile("/home/projects/TS/p1/src/x.ts") == nil) }) @@ -363,7 +363,7 @@ func TestService(t *testing.T) { service.OpenFile("/home/projects/TS/p1/src/x.ts", files["/home/projects/TS/p1/src/x.ts"], core.ScriptKindTS, "") _, project := service.EnsureDefaultProjectForFile("/home/projects/TS/p1/src/x.ts") program := project.GetProgram() - assert.Equal(t, len(program.GetSemanticDiagnostics(program.GetSourceFile("/home/projects/TS/p1/src/x.ts"))), 0) + assert.Equal(t, len(program.GetSemanticDiagnostics(t.Context(), program.GetSourceFile("/home/projects/TS/p1/src/x.ts"))), 0) filesCopy := maps.Clone(files) delete(filesCopy, "/home/projects/TS/p1/src/index.ts") @@ -376,7 +376,7 @@ func TestService(t *testing.T) { })) program = project.GetProgram() - assert.Equal(t, len(program.GetSemanticDiagnostics(program.GetSourceFile("/home/projects/TS/p1/src/x.ts"))), 1) + assert.Equal(t, len(program.GetSemanticDiagnostics(t.Context(), program.GetSourceFile("/home/projects/TS/p1/src/x.ts"))), 1) }) t.Run("create explicitly included file", func(t *testing.T) { @@ -396,7 +396,7 @@ func TestService(t *testing.T) { program := project.GetProgram() // Initially should have an error because y.ts is missing - assert.Equal(t, len(program.GetSemanticDiagnostics(program.GetSourceFile("/home/projects/TS/p1/src/index.ts"))), 1) + assert.Equal(t, len(program.GetSemanticDiagnostics(t.Context(), program.GetSourceFile("/home/projects/TS/p1/src/index.ts"))), 1) // Add the missing file filesCopy := maps.Clone(files) @@ -411,7 +411,7 @@ func TestService(t *testing.T) { // Error should be resolved program = project.GetProgram() - assert.Equal(t, len(program.GetSemanticDiagnostics(program.GetSourceFile("/home/projects/TS/p1/src/index.ts"))), 0) + assert.Equal(t, len(program.GetSemanticDiagnostics(t.Context(), program.GetSourceFile("/home/projects/TS/p1/src/index.ts"))), 0) assert.Check(t, program.GetSourceFile("/home/projects/TS/p1/src/y.ts") != nil) }) @@ -432,7 +432,7 @@ func TestService(t *testing.T) { program := project.GetProgram() // Initially should have an error because z.ts is missing - assert.Equal(t, len(program.GetSemanticDiagnostics(program.GetSourceFile("/home/projects/TS/p1/src/index.ts"))), 1) + assert.Equal(t, len(program.GetSemanticDiagnostics(t.Context(), program.GetSourceFile("/home/projects/TS/p1/src/index.ts"))), 1) // Add a new file through wildcard inclusion filesCopy := maps.Clone(files) @@ -447,7 +447,7 @@ func TestService(t *testing.T) { // Error should be resolved and the new file should be included in the program program = project.GetProgram() - assert.Equal(t, len(program.GetSemanticDiagnostics(program.GetSourceFile("/home/projects/TS/p1/src/index.ts"))), 0) + assert.Equal(t, len(program.GetSemanticDiagnostics(t.Context(), program.GetSourceFile("/home/projects/TS/p1/src/index.ts"))), 0) assert.Check(t, program.GetSourceFile("/home/projects/TS/p1/src/z.ts") != nil) }) }) diff --git a/internal/tsoptions/parsedcommandline.go b/internal/tsoptions/parsedcommandline.go index 66d708f977..8675a34d02 100644 --- a/internal/tsoptions/parsedcommandline.go +++ b/internal/tsoptions/parsedcommandline.go @@ -64,6 +64,7 @@ func (p *ParsedCommandLine) GetConfigFileParsingDiagnostics() []*ast.Diagnostic return p.Errors } +// Porting reference: ProjectService.isMatchedByConfig func (p *ParsedCommandLine) MatchesFileName(fileName string, comparePathsOptions tspath.ComparePathsOptions) bool { path := tspath.ToPath(fileName, comparePathsOptions.CurrentDirectory, comparePathsOptions.UseCaseSensitiveFileNames) if slices.ContainsFunc(p.FileNames(), func(f string) bool { From 856536afbd470046eb62c5081421d85935e69315 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Mon, 12 May 2025 11:18:58 -0700 Subject: [PATCH 13/16] Add file names to root file watch globs --- internal/project/project.go | 9 ++++++--- internal/tsoptions/parsedcommandline.go | 9 +++++++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/internal/project/project.go b/internal/project/project.go index f48ec43185..cd8cb7d1ce 100644 --- a/internal/project/project.go +++ b/internal/project/project.go @@ -231,12 +231,15 @@ func (p *Project) LanguageService() *ls.LanguageService { func (p *Project) getRootFileWatchGlobs() []string { if p.kind == KindConfigured { - wildcardDirectories := p.parsedCommandLine.WildcardDirectories() - result := make([]string, 0, len(wildcardDirectories)+1) + globs := p.parsedCommandLine.WildcardDirectories() + result := make([]string, 0, len(globs)+1) result = append(result, p.configFileName) - for dir, recursive := range wildcardDirectories { + for dir, recursive := range globs { result = append(result, fmt.Sprintf("%s/%s", dir, core.IfElse(recursive, recursiveFileGlobPattern, fileGlobPattern))) } + for _, fileName := range p.parsedCommandLine.LiteralFileNames() { + result = append(result, fileName) + } return result } return nil diff --git a/internal/tsoptions/parsedcommandline.go b/internal/tsoptions/parsedcommandline.go index 8675a34d02..980436c4d6 100644 --- a/internal/tsoptions/parsedcommandline.go +++ b/internal/tsoptions/parsedcommandline.go @@ -36,6 +36,14 @@ func (p *ParsedCommandLine) WildcardDirectories() map[string]bool { return p.wildcardDirectories } +// Normalized file names explicitly specified in `files` +func (p *ParsedCommandLine) LiteralFileNames() []string { + if p.ConfigFile != nil { + return p.FileNames()[0:len(p.ConfigFile.configFileSpecs.validatedFilesSpec)] + } + return nil +} + func (p *ParsedCommandLine) SetParsedOptions(o *core.ParsedOptions) { p.ParsedConfig = o } @@ -48,6 +56,7 @@ func (p *ParsedCommandLine) CompilerOptions() *core.CompilerOptions { return p.ParsedConfig.CompilerOptions } +// All file names matched by files, include, and exclude patterns func (p *ParsedCommandLine) FileNames() []string { return p.ParsedConfig.FileNames } From e2953f93051c7e17284012519f7eda5ca6227f4f Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Mon, 12 May 2025 11:45:57 -0700 Subject: [PATCH 14/16] Split pending reload back into full config / just file names --- internal/project/project.go | 27 ++++++++++++++++++++------- internal/project/service.go | 2 +- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/internal/project/project.go b/internal/project/project.go index cd8cb7d1ce..ca9e05fa7c 100644 --- a/internal/project/project.go +++ b/internal/project/project.go @@ -34,6 +34,14 @@ const ( KindAuxiliary ) +type PendingReload int + +const ( + PendingReloadNone PendingReload = iota + PendingReloadFileNames + PendingReloadFull +) + type ProjectHost interface { tsoptions.ParseConfigHost NewLine() string @@ -62,7 +70,7 @@ type Project struct { hasAddedOrRemovedFiles bool hasAddedOrRemovedSymlinks bool deferredClose bool - pendingConfigReload bool + pendingReload PendingReload comparePathsOptions tspath.ComparePathsOptions currentDirectory string @@ -308,7 +316,7 @@ func (p *Project) onWatchEventForNilScriptInfo(fileName string) { path := p.toPath(fileName) if p.kind == KindConfigured { if p.rootFileNames.Has(path) || p.parsedCommandLine.MatchesFileName(fileName, p.comparePathsOptions) { - p.pendingConfigReload = true + p.pendingReload = PendingReloadFileNames p.markAsDirty() return } @@ -374,11 +382,16 @@ func (p *Project) updateGraph() bool { hasAddedOrRemovedFiles := p.hasAddedOrRemovedFiles p.initialLoadPending = false - if p.kind == KindConfigured && p.pendingConfigReload { - if err := p.LoadConfig(); err != nil { - panic(fmt.Sprintf("failed to reload config: %v", err)) + if p.kind == KindConfigured && p.pendingReload != PendingReloadNone { + switch p.pendingReload { + case PendingReloadFileNames: + p.setRootFiles(p.parsedCommandLine.FileNames()) + case PendingReloadFull: + if err := p.LoadConfig(); err != nil { + panic(fmt.Sprintf("failed to reload config: %v", err)) + } } - p.pendingConfigReload = false + p.pendingReload = PendingReloadNone } p.hasAddedOrRemovedFiles = false @@ -442,7 +455,7 @@ func (p *Project) removeFile(info *ScriptInfo, fileExists bool, detachFromProjec case KindInferred: p.rootFileNames.Delete(info.path) case KindConfigured: - p.pendingConfigReload = true + p.pendingReload = PendingReloadFileNames } } diff --git a/internal/project/service.go b/internal/project/service.go index 72b02206bf..d97cbf1bfe 100644 --- a/internal/project/service.go +++ b/internal/project/service.go @@ -283,7 +283,7 @@ func (s *Service) onConfigFileChanged(project *Project, changeKind lsproto.FileC s.delayUpdateProjectGraph(project) if !project.deferredClose { - project.pendingConfigReload = true + project.pendingReload = PendingReloadFull project.markAsDirty() } return nil From 163fea1c56d859b243dbd0f3cc6191b14744e0a7 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Mon, 12 May 2025 16:41:44 -0700 Subject: [PATCH 15/16] Add failing test for wildcard update --- internal/project/service_test.go | 74 +++++++- .../testutil/projecttestutil/clientmock.go | 168 ++++++++++++++++++ .../projecttestutil/projecttestutil.go | 9 +- 3 files changed, 246 insertions(+), 5 deletions(-) create mode 100644 internal/testutil/projecttestutil/clientmock.go diff --git a/internal/project/service_test.go b/internal/project/service_test.go index 2a34997129..3658ea7ebb 100644 --- a/internal/project/service_test.go +++ b/internal/project/service_test.go @@ -2,6 +2,7 @@ package project_test import ( "maps" + "slices" "testing" "github.com/microsoft/typescript-go/internal/bundled" @@ -398,6 +399,28 @@ func TestService(t *testing.T) { // Initially should have an error because y.ts is missing assert.Equal(t, len(program.GetSemanticDiagnostics(t.Context(), program.GetSourceFile("/home/projects/TS/p1/src/index.ts"))), 1) + // Missing location should be watched + assert.DeepEqual(t, host.ClientMock.WatchFilesCalls()[0].Watchers, []*lsproto.FileSystemWatcher{ + { + Kind: ptrTo(lsproto.WatchKindCreate | lsproto.WatchKindChange | lsproto.WatchKindDelete), + GlobPattern: lsproto.GlobPattern{ + Pattern: ptrTo("/home/projects/TS/p1/tsconfig.json"), + }, + }, + { + Kind: ptrTo(lsproto.WatchKindCreate | lsproto.WatchKindChange | lsproto.WatchKindDelete), + GlobPattern: lsproto.GlobPattern{ + Pattern: ptrTo("/home/projects/TS/p1/src/index.ts"), + }, + }, + { + Kind: ptrTo(lsproto.WatchKindCreate | lsproto.WatchKindChange | lsproto.WatchKindDelete), + GlobPattern: lsproto.GlobPattern{ + Pattern: ptrTo("/home/projects/TS/p1/src/y.ts"), + }, + }, + }) + // Add the missing file filesCopy := maps.Clone(files) filesCopy["/home/projects/TS/p1/src/y.ts"] = `export const y = 1;` @@ -415,14 +438,14 @@ func TestService(t *testing.T) { assert.Check(t, program.GetSourceFile("/home/projects/TS/p1/src/y.ts") != nil) }) - t.Run("create wildcard included file", func(t *testing.T) { + t.Run("create failed lookup location", func(t *testing.T) { t.Parallel() files := map[string]string{ "/home/projects/TS/p1/tsconfig.json": `{ "compilerOptions": { "noLib": true }, - "include": ["src"] + "files": ["src/index.ts"] }`, "/home/projects/TS/p1/src/index.ts": `import { z } from "./z";`, } @@ -434,7 +457,12 @@ func TestService(t *testing.T) { // Initially should have an error because z.ts is missing assert.Equal(t, len(program.GetSemanticDiagnostics(t.Context(), program.GetSourceFile("/home/projects/TS/p1/src/index.ts"))), 1) - // Add a new file through wildcard inclusion + // Missing location should be watched + assert.Check(t, slices.ContainsFunc(host.ClientMock.WatchFilesCalls()[1].Watchers, func(w *lsproto.FileSystemWatcher) bool { + return *w.GlobPattern.Pattern == "/home/projects/TS/p1/src/z.ts" && *w.Kind == lsproto.WatchKindCreate + })) + + // Add a new file through failed lookup watch filesCopy := maps.Clone(files) filesCopy["/home/projects/TS/p1/src/z.ts"] = `export const z = 1;` host.ReplaceFS(filesCopy) @@ -450,5 +478,45 @@ func TestService(t *testing.T) { assert.Equal(t, len(program.GetSemanticDiagnostics(t.Context(), program.GetSourceFile("/home/projects/TS/p1/src/index.ts"))), 0) assert.Check(t, program.GetSourceFile("/home/projects/TS/p1/src/z.ts") != nil) }) + + t.Run("create wildcard included file", func(t *testing.T) { + t.Parallel() + files := map[string]string{ + "/home/projects/TS/p1/tsconfig.json": `{ + "compilerOptions": { + "noLib": true + }, + "include": ["src"] + }`, + "/home/projects/TS/p1/src/index.ts": `a;`, + } + service, host := projecttestutil.Setup(files) + service.OpenFile("/home/projects/TS/p1/src/index.ts", files["/home/projects/TS/p1/src/index.ts"], core.ScriptKindTS, "") + _, project := service.EnsureDefaultProjectForFile("/home/projects/TS/p1/src/index.ts") + program := project.GetProgram() + + // Initially should have an error because declaration for 'a' is missing + assert.Equal(t, len(program.GetSemanticDiagnostics(t.Context(), program.GetSourceFile("/home/projects/TS/p1/src/index.ts"))), 1) + + // Add a new file through wildcard watch + filesCopy := maps.Clone(files) + filesCopy["/home/projects/TS/p1/src/a.ts"] = `const a = 1;` + host.ReplaceFS(filesCopy) + assert.NilError(t, service.OnWatchedFilesChanged([]*lsproto.FileEvent{ + { + Type: lsproto.FileChangeTypeCreated, + Uri: "file:///home/projects/TS/p1/src/a.ts", + }, + })) + + // Error should be resolved and the new file should be included in the program + program = project.GetProgram() + assert.Equal(t, len(program.GetSemanticDiagnostics(t.Context(), program.GetSourceFile("/home/projects/TS/p1/src/index.ts"))), 0) + assert.Check(t, program.GetSourceFile("/home/projects/TS/p1/src/a.ts") != nil) + }) }) } + +func ptrTo[T any](v T) *T { + return &v +} diff --git a/internal/testutil/projecttestutil/clientmock.go b/internal/testutil/projecttestutil/clientmock.go new file mode 100644 index 0000000000..e8bf1ab779 --- /dev/null +++ b/internal/testutil/projecttestutil/clientmock.go @@ -0,0 +1,168 @@ +// Code generated by moq; DO NOT EDIT. +// github.com/matryer/moq + +package projecttestutil + +import ( + "sync" + + "github.com/microsoft/typescript-go/internal/lsp/lsproto" + "github.com/microsoft/typescript-go/internal/project" +) + +// Ensure, that ClientMock does implement project.Client. +// If this is not the case, regenerate this file with moq. +var _ project.Client = &ClientMock{} + +// ClientMock is a mock implementation of project.Client. +// +// func TestSomethingThatUsesClient(t *testing.T) { +// +// // make and configure a mocked project.Client +// mockedClient := &ClientMock{ +// RefreshDiagnosticsFunc: func() error { +// panic("mock out the RefreshDiagnostics method") +// }, +// UnwatchFilesFunc: func(handle project.WatcherHandle) error { +// panic("mock out the UnwatchFiles method") +// }, +// WatchFilesFunc: func(watchers []*lsproto.FileSystemWatcher) (project.WatcherHandle, error) { +// panic("mock out the WatchFiles method") +// }, +// } +// +// // use mockedClient in code that requires project.Client +// // and then make assertions. +// +// } +type ClientMock struct { + // RefreshDiagnosticsFunc mocks the RefreshDiagnostics method. + RefreshDiagnosticsFunc func() error + + // UnwatchFilesFunc mocks the UnwatchFiles method. + UnwatchFilesFunc func(handle project.WatcherHandle) error + + // WatchFilesFunc mocks the WatchFiles method. + WatchFilesFunc func(watchers []*lsproto.FileSystemWatcher) (project.WatcherHandle, error) + + // calls tracks calls to the methods. + calls struct { + // RefreshDiagnostics holds details about calls to the RefreshDiagnostics method. + RefreshDiagnostics []struct { + } + // UnwatchFiles holds details about calls to the UnwatchFiles method. + UnwatchFiles []struct { + // Handle is the handle argument value. + Handle project.WatcherHandle + } + // WatchFiles holds details about calls to the WatchFiles method. + WatchFiles []struct { + // Watchers is the watchers argument value. + Watchers []*lsproto.FileSystemWatcher + } + } + lockRefreshDiagnostics sync.RWMutex + lockUnwatchFiles sync.RWMutex + lockWatchFiles sync.RWMutex +} + +// RefreshDiagnostics calls RefreshDiagnosticsFunc. +func (mock *ClientMock) RefreshDiagnostics() error { + callInfo := struct { + }{} + mock.lockRefreshDiagnostics.Lock() + mock.calls.RefreshDiagnostics = append(mock.calls.RefreshDiagnostics, callInfo) + mock.lockRefreshDiagnostics.Unlock() + if mock.RefreshDiagnosticsFunc == nil { + var ( + errOut error + ) + return errOut + } + return mock.RefreshDiagnosticsFunc() +} + +// RefreshDiagnosticsCalls gets all the calls that were made to RefreshDiagnostics. +// Check the length with: +// +// len(mockedClient.RefreshDiagnosticsCalls()) +func (mock *ClientMock) RefreshDiagnosticsCalls() []struct { +} { + var calls []struct { + } + mock.lockRefreshDiagnostics.RLock() + calls = mock.calls.RefreshDiagnostics + mock.lockRefreshDiagnostics.RUnlock() + return calls +} + +// UnwatchFiles calls UnwatchFilesFunc. +func (mock *ClientMock) UnwatchFiles(handle project.WatcherHandle) error { + callInfo := struct { + Handle project.WatcherHandle + }{ + Handle: handle, + } + mock.lockUnwatchFiles.Lock() + mock.calls.UnwatchFiles = append(mock.calls.UnwatchFiles, callInfo) + mock.lockUnwatchFiles.Unlock() + if mock.UnwatchFilesFunc == nil { + var ( + errOut error + ) + return errOut + } + return mock.UnwatchFilesFunc(handle) +} + +// UnwatchFilesCalls gets all the calls that were made to UnwatchFiles. +// Check the length with: +// +// len(mockedClient.UnwatchFilesCalls()) +func (mock *ClientMock) UnwatchFilesCalls() []struct { + Handle project.WatcherHandle +} { + var calls []struct { + Handle project.WatcherHandle + } + mock.lockUnwatchFiles.RLock() + calls = mock.calls.UnwatchFiles + mock.lockUnwatchFiles.RUnlock() + return calls +} + +// WatchFiles calls WatchFilesFunc. +func (mock *ClientMock) WatchFiles(watchers []*lsproto.FileSystemWatcher) (project.WatcherHandle, error) { + callInfo := struct { + Watchers []*lsproto.FileSystemWatcher + }{ + Watchers: watchers, + } + mock.lockWatchFiles.Lock() + mock.calls.WatchFiles = append(mock.calls.WatchFiles, callInfo) + mock.lockWatchFiles.Unlock() + if mock.WatchFilesFunc == nil { + var ( + watcherHandleOut project.WatcherHandle + errOut error + ) + return watcherHandleOut, errOut + } + return mock.WatchFilesFunc(watchers) +} + +// WatchFilesCalls gets all the calls that were made to WatchFiles. +// Check the length with: +// +// len(mockedClient.WatchFilesCalls()) +func (mock *ClientMock) WatchFilesCalls() []struct { + Watchers []*lsproto.FileSystemWatcher +} { + var calls []struct { + Watchers []*lsproto.FileSystemWatcher + } + mock.lockWatchFiles.RLock() + calls = mock.calls.WatchFiles + mock.lockWatchFiles.RUnlock() + return calls +} diff --git a/internal/testutil/projecttestutil/projecttestutil.go b/internal/testutil/projecttestutil/projecttestutil.go index d7898ea82a..e4a2ba175c 100644 --- a/internal/testutil/projecttestutil/projecttestutil.go +++ b/internal/testutil/projecttestutil/projecttestutil.go @@ -12,12 +12,15 @@ import ( "github.com/microsoft/typescript-go/internal/vfs/vfstest" ) +//go:generate go tool github.com/matryer/moq -stub -fmt goimports -pkg projecttestutil -out clientmock.go ../../project Client + type ProjectServiceHost struct { fs vfs.FS mu sync.Mutex defaultLibraryPath string output strings.Builder logger *project.Logger + ClientMock *ClientMock } // DefaultLibraryPath implements project.ProjectServiceHost. @@ -49,7 +52,7 @@ func (p *ProjectServiceHost) NewLine() string { // Client implements project.ProjectServiceHost. func (p *ProjectServiceHost) Client() project.Client { - return nil + return p.ClientMock } func (p *ProjectServiceHost) ReplaceFS(files map[string]string) { @@ -61,7 +64,8 @@ var _ project.ServiceHost = (*ProjectServiceHost)(nil) func Setup(files map[string]string) (*project.Service, *ProjectServiceHost) { host := newProjectServiceHost(files) service := project.NewService(host, project.ServiceOptions{ - Logger: host.logger, + Logger: host.logger, + WatchEnabled: true, }) return service, host } @@ -71,6 +75,7 @@ func newProjectServiceHost(files map[string]string) *ProjectServiceHost { host := &ProjectServiceHost{ fs: fs, defaultLibraryPath: bundled.LibPath(), + ClientMock: &ClientMock{}, } host.logger = project.NewLogger([]io.Writer{&host.output}, "", project.LogLevelVerbose) return host From 61af1e8b4034fc1cbdcff5d4cb6704a7fbbaf130 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Mon, 12 May 2025 16:56:36 -0700 Subject: [PATCH 16/16] Fix broken test --- internal/project/project.go | 1 + internal/tsoptions/parsedcommandline.go | 16 ++++++++++++++++ internal/tsoptions/tsconfigparsing.go | 1 + 3 files changed, 18 insertions(+) diff --git a/internal/project/project.go b/internal/project/project.go index ca9e05fa7c..bab2415b0c 100644 --- a/internal/project/project.go +++ b/internal/project/project.go @@ -385,6 +385,7 @@ func (p *Project) updateGraph() bool { if p.kind == KindConfigured && p.pendingReload != PendingReloadNone { switch p.pendingReload { case PendingReloadFileNames: + p.parsedCommandLine = tsoptions.ReloadFileNamesOfParsedCommandLine(p.parsedCommandLine, p.host.FS()) p.setRootFiles(p.parsedCommandLine.FileNames()) case PendingReloadFull: if err := p.LoadConfig(); err != nil { diff --git a/internal/tsoptions/parsedcommandline.go b/internal/tsoptions/parsedcommandline.go index 980436c4d6..de0948032b 100644 --- a/internal/tsoptions/parsedcommandline.go +++ b/internal/tsoptions/parsedcommandline.go @@ -7,6 +7,7 @@ import ( "github.com/microsoft/typescript-go/internal/ast" "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/tspath" + "github.com/microsoft/typescript-go/internal/vfs" ) type ParsedCommandLine struct { @@ -21,6 +22,7 @@ type ParsedCommandLine struct { comparePathsOptions tspath.ComparePathsOptions wildcardDirectoriesOnce sync.Once wildcardDirectories map[string]bool + extraFileExtensions []fileExtensionInfo } // WildcardDirectories returns the cached wildcard directories, initializing them if needed @@ -102,3 +104,17 @@ func (p *ParsedCommandLine) MatchesFileName(fileName string, comparePathsOptions return p.ConfigFile.configFileSpecs.matchesInclude(fileName, comparePathsOptions) } + +func ReloadFileNamesOfParsedCommandLine(p *ParsedCommandLine, fs vfs.FS) *ParsedCommandLine { + parsedConfig := *p.ParsedConfig + parsedConfig.FileNames = getFileNamesFromConfigSpecs( + *p.ConfigFile.configFileSpecs, + p.comparePathsOptions.CurrentDirectory, + p.CompilerOptions(), + fs, + p.extraFileExtensions, + ) + parsedCommandLine := *p + parsedCommandLine.ParsedConfig = &parsedConfig + return &parsedCommandLine +} diff --git a/internal/tsoptions/tsconfigparsing.go b/internal/tsoptions/tsconfigparsing.go index 386ecaa6e4..67d8f85b67 100644 --- a/internal/tsoptions/tsconfigparsing.go +++ b/internal/tsoptions/tsconfigparsing.go @@ -1246,6 +1246,7 @@ func parseJsonConfigFileContentWorker( Raw: parsedConfig.raw, Errors: errors, + extraFileExtensions: extraFileExtensions, comparePathsOptions: tspath.ComparePathsOptions{ UseCaseSensitiveFileNames: host.FS().UseCaseSensitiveFileNames(), CurrentDirectory: basePathForFileNames,