microsoft / microsoft/TypeScript
LSP: file contents are not re-read when `didChangeWatchedFiles` reports `Created` (type 1) for a file already in the program
@johnfav03 ci sta già lavorando.
Dal 14/9/2026.
- Lingua principale
- Go
- Stelle
- 111k
- Fork
- 14.3k
- Merge medio
- 2g 4h
- PR unite (30g)
- 132
Descrizione
🔎 Search Terms
didChangeWatchedFiles
FileChangeType.Created
type 1 Created event stale contents
language server stale file contents
LSP requires restart types out of sync
atomic save rename language server
has no exported member after the file was fixed
watched files not reloaded
tsc --lsp
🕗 Version & Regression Information
- I was unable to test this on prior versions because the LSP server (
tsc --lsp)
is new in TypeScript 7; TypeScript 6.x and earlier have no equivalent
workspace/didChangeWatchedFileshandling to compare against, so this is not a
regression from a previous release.
Reproduces on both:
typescript@7.0.2(latest)typescript@7.1.0-dev.20260907.1(next)
every-ts bisection is not applicable for the same reason — the code path does not
exist in earlier versions.
⏯ Playground Link
Not applicable. The bug is in how the language server reacts to file-system change notifications, so it cannot be reproduced in the Playground. A standalone LSP client script that reproduces it in isolation is attached instead (see 💻 Code).
💻 Code
Three files:
// tsconfig.json
{
"compilerOptions": {
"strict": true,
"noEmit": true,
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler"
},
"include": ["src/**/*"]
}
// src/main.ts
import { thing } from './helper';
export const used = thing;
// src/helper.ts — starts out without `thing`, so main.ts has an error
export const other = 1;
Then, over LSP with main.ts open and helper.ts not open:
-
initialize(advertisingworkspace.didChangeWatchedFiles.dynamicRegistration),
initialized, thentextDocument/didOpenforsrc/main.ts. -
textDocument/diagnosticformain.ts→
Module '"./helper"' has no exported member 'thing'.(expected at this point) -
Fix
src/helper.tson disk toexport const thing = 1;, written the way an
atomic save does: writehelper.ts.tmp, thenrename("helper.ts.tmp", "helper.ts"). -
Send the change notification with the events a client actually sends for that
rename (VS Code's watcher reports a rename-over as a create):{ "changes": [ { "uri": ".../src/helper.ts.tmp", "type": 1 }, { "uri": ".../src/helper.ts", "type": 1 }, { "uri": ".../src/helper.ts.tmp", "type": 3 } ] } -
textDocument/diagnosticformain.tsagain.
Attached: repro-created-event.js — performs exactly these steps against
tsc --lsp --stdio. Node only, no dependencies, no editor involved:
$ node repro-created-event.js <path-to-tsc>
PASS Changed (type 2) only — control
FAIL Created (type 1) only
still reported: Module '"./helper"' has no exported member 'thing'.
FAIL what VS Code sends for an atomic save
still reported: Module '"./helper"' has no exported member 'thing'.
🙁 Actual behavior
After helper.ts is fixed on disk, textDocument/diagnostic for main.ts keeps
returning Module '"./helper"' has no exported member 'thing'. The server never
re-reads helper.ts, so the program holds the old contents indefinitely — the stale
diagnostic survives further edits to other files and only goes away on a server
restart, or on textDocument/didOpen of helper.ts (which supplies the contents
explicitly).
The trigger is the change type. For the same on-disk change:
| notification for the changed file | result |
|---|---|
type: 2 (Changed) only |
✓ error clears (correct) |
type: 1 (Created) only |
✗ error persists |
tmp create → target create → tmp delete (what VS Code sends) |
✗ error persists |
🙂 Expected behavior
All three should clear the diagnostic, since the file's contents on disk changed.
A Created event for a path that is already in the program should invalidate that
file's cached contents, the same way Changed does. A client cannot reliably tell
the two apart: on Linux, rename() over an existing path produces IN_MOVED_TO,
which is a create from the watcher's point of view — VS Code therefore sends
type: 1 for what is semantically a modification.
This matters in practice because saving atomically (write a temp file, then rename
over the target) is a common pattern — formatters, code generators, editors with
files.atomicSave, and AI coding tools all do it. Every such save takes this path,
so the user-visible result is a permanent, wrong has no exported member on an
import line, "fixed" only by restarting the language server.
Additional information about the issue
Environment
- TypeScript 7.0.2 and 7.1.0-dev.20260907.1, Linux (arm64), Node 24.14.0
- Language server started as
tsc --lsp --stdio - Also observed through VS Code 1.127.0 with
js/ts.experimental.useTsgo: true
and the "TypeScript 7" extensionTypeScriptTeam.native-preview@0.20260708.2
(whose bundled binary also reports 7.0.2)
What is not the cause
- The watcher globs the server registers are
**/*(the extension-suffixed globs
from microsoft/typescript-go#2506 are gone), so the event does reach the server —
VS Code's own logs showhandled method 'workspace/didChangeWatchedFiles'for
these changes. - The server is robust in every adjacent scenario tested:
didChange,
didClose+didOpen,type: 2on an unopened file, 20 rapid successive writes,
delete+createof a previously unknown file, and create-then-modify before
notifying. Only "type: 1for a path already in the program" fails. - Not a client-side file-watching problem: reproduced with a plain Node LSP client,
with no editor in the picture.
Possibly related
- microsoft/typescript-go#2506 — same area (watched-file handling), different cause
(extension-suffixed watch globs andfailedLookupLocationsfor unresolved
modules). Already fixed: aCreatedevent for a path that is not yet in the
program does work. - JetBrains WEB-76338 "Typescript Go LSP often requires restart as types get out of
sync" — same user-visible symptom from a different client, no root cause
identified. Possibly the same bug, since JetBrains also saves atomically.
Workarounds
- Open the changed file in the editor (
didOpenre-reads from disk). - Restart the language server.
- Follow the atomic write with an in-place rewrite of identical bytes (produces
IN_MODIFY→type: 2). Note the client may coalesce the two events if they are
close together: with VS Code's watcher, a gap of ~600ms or more was needed before
the second event was reported as an update rather than another create.
Guida per i contributori
Apri la guida per i contributori
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Valutazione
Questa issue non è ancora stata valutata.