From f8edd059109301758e2b87b18d26dac600fc8c6a Mon Sep 17 00:00:00 2001 From: Lubos Date: Sat, 24 Oct 2026 04:38:08 +0800 Subject: [PATCH] Refresh diagnostics when a content-mapped or declaration file changes on disk #64159 changed DidChangeWatchedFiles to read the extension through path.Extension(), which knows only TypeScript's own extensions. A content-mapped file such as shape.data got "" and was treated as a possible directory, so the content mapper extension check was never reached. A .d.ts file got ".d.ts", which isRelevantExtension does not list. Neither change scheduled a diagnostics refresh. Fixes #64943 --- tsc/internal/project/contentmapper_test.go | 26 +++++++++++++++ tsc/internal/project/session.go | 7 ++-- tsc/internal/project/session_test.go | 38 ++++++++++++++++++++++ 3 files changed, 66 insertions(+), 5 deletions(-) diff --git a/tsc/internal/project/contentmapper_test.go b/tsc/internal/project/contentmapper_test.go index 95f2da159160a..070cf6b2f3b1f 100644 --- a/tsc/internal/project/contentmapper_test.go +++ b/tsc/internal/project/contentmapper_test.go @@ -265,6 +265,32 @@ func TestContentMapperInProject(t *testing.T) { assert.Assert(t, strings.Contains(updated.Text(), "export const watched = true;"), "updated app.box missing watched change: %q", updated.Text()) }) + t.Run("watch change to a content-mapped file schedules diagnostics refresh", func(t *testing.T) { + t.Parallel() + session, utils := newSession(true) + defer session.Close() + + ctx := context.Background() + mainURI := lsproto.DocumentUri("file:///home/project/main.ts") + session.DidOpenFile(ctx, mainURI, 1, files["/home/project/main.ts"].(string), lsproto.LanguageKindTypeScript) + _, err := session.GetLanguageService(ctx, mainURI) + assert.NilError(t, err) + session.WaitForBackgroundTasks() + + baselineRefreshCount := len(utils.Client().RefreshDiagnosticsCalls()) + assert.NilError(t, utils.FS().WriteFile("/home/project/app.box", "export const version = #{target};\nexport const watched = true;\n")) + session.DidChangeWatchedFiles(ctx, []*lsproto.FileEvent{{ + Uri: "file:///home/project/app.box", + Type: lsproto.FileChangeTypeChanged, + }}) + session.WaitForBackgroundTasks() + + refreshCount := len(utils.Client().RefreshDiagnosticsCalls()) + assert.Assert(t, refreshCount > baselineRefreshCount, + "expected RefreshDiagnostics to be called after app.box changed on disk, got %d calls (baseline %d)", + refreshCount, baselineRefreshCount) + }) + t.Run("unchanged content-mapped file is reused from the cache across a full rebuild", func(t *testing.T) { t.Parallel() session, utils := newSession(true) diff --git a/tsc/internal/project/session.go b/tsc/internal/project/session.go index e02dd45c3e842..d93fa052cf62a 100644 --- a/tsc/internal/project/session.go +++ b/tsc/internal/project/session.go @@ -448,7 +448,7 @@ func (s *Session) DidChangeWatchedFiles(ctx context.Context, changes []*lsproto. if !hasRelevantChange { filePath := change.Uri.Path() path := s.caseSensitivity.PathKey(filePath).RemoveTrailingDirectorySeparator() - if contentMapperWatchedFiles.Has(path) { + if contentMapperWatchedFiles.Has(path) || path.ExtensionIsOneOf(contentMapperExtensions) { hasRelevantChange = true continue } @@ -468,10 +468,7 @@ func (s *Session) DidChangeWatchedFiles(ctx context.Context, changes []*lsproto. } } } else { - if isRelevantExtension(extension) || - path.ExtensionIsOneOf(contentMapperExtensions) { - hasRelevantChange = true - } + hasRelevantChange = true } } } diff --git a/tsc/internal/project/session_test.go b/tsc/internal/project/session_test.go index 81199e3731803..1a329db9cde3f 100644 --- a/tsc/internal/project/session_test.go +++ b/tsc/internal/project/session_test.go @@ -1081,6 +1081,44 @@ export const value = content;`, refreshCount, baselineRefreshCount) }) + t.Run("change to a declaration file schedules diagnostics refresh", func(t *testing.T) { + t.Parallel() + files := map[string]any{ + "/home/projects/TS/p1/tsconfig.json": `{ + "compilerOptions": { + "noLib": true + }, + "files": ["index.ts", "types.d.ts"] + }`, + "/home/projects/TS/p1/index.ts": `export const value: Value = 1;`, + "/home/projects/TS/p1/types.d.ts": `type Value = string;`, + } + session, utils := projecttestutil.Setup(files) + session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/index.ts", 1, files["/home/projects/TS/p1/index.ts"].(string), lsproto.LanguageKindTypeScript) + + _, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/index.ts") + assert.NilError(t, err) + session.WaitForBackgroundTasks() + + baselineRefreshCount := len(utils.Client().RefreshDiagnosticsCalls()) + + err = utils.FS().WriteFile("/home/projects/TS/p1/types.d.ts", `type Value = number;`) + assert.NilError(t, err) + + session.DidChangeWatchedFiles(context.Background(), []*lsproto.FileEvent{ + { + Type: lsproto.FileChangeTypeChanged, + Uri: "file:///home/projects/TS/p1/types.d.ts", + }, + }) + session.WaitForBackgroundTasks() + + refreshCount := len(utils.Client().RefreshDiagnosticsCalls()) + assert.Assert(t, refreshCount > baselineRefreshCount, + "expected RefreshDiagnostics to be called after changing /home/projects/TS/p1/types.d.ts, got %d calls (baseline %d)", + refreshCount, baselineRefreshCount) + }) + t.Run("delete sibling folder schedules diagnostics refresh after opening third file", func(t *testing.T) { t.Parallel() files := map[string]any{