feat(filesystem): honor the non-recursive watch option - #17875
Open
EhabY wants to merge 2 commits into
Open
Conversation
EhabY
force-pushed
the
feat/non-recursive-file-watching
branch
from
August 3, 2026 09:35
63af86d to
231cf47
Compare
EhabY
force-pushed
the
feat/non-recursive-file-watching
branch
from
August 16, 2026 13:10
231cf47 to
191d465
Compare
EhabY
force-pushed
the
feat/non-recursive-file-watching
branch
from
August 16, 2026 13:43
191d465 to
95fbdb7
Compare
EhabY
force-pushed
the
feat/non-recursive-file-watching
branch
14 times, most recently
from
August 17, 2026 10:26
d61246c to
f329ff5
Compare
EhabY
force-pushed
the
feat/non-recursive-file-watching
branch
from
August 23, 2026 14:27
f329ff5 to
4e37ef0
Compare
This was referenced Aug 23, 2026
On macOS, creating a child also modifies its parent, so FSEvents may or may not additionally report the watched root. The exact-set assertion therefore failed intermittently: on one commit, macos-15 node-24.x passed while macos-15 node-22.x failed. Assert that no unexpected path arrived instead, excluding the root. The waitForChange calls above already establish that each expected URI arrived.
EhabY
force-pushed
the
feat/non-recursive-file-watching
branch
from
August 23, 2026 14:56
4e37ef0 to
1486eba
Compare
The backend dropped the recursive flag, so every watch became a recursive parcel subscription: a non-recursive watch rooted outside the workspace crawled every sibling subtree, and watching a single file crawled its parent directory. Non-recursive requests now go to a NodeDirectoryWatcher that watches one directory level with fs.watch, and requests resolving to the same directory share one handle. Sharing matters beyond saving descriptors: on macOS libuv keeps one FSEventStream per event loop and rebuilds it whenever any handle starts or stops, losing the events of every other watcher while it does. Platform handling follows VS Code where it is settled: NFC normalization and case-insensitive matching on macOS, an exact-case readdir so a rename that only changes case is not reported as an update, the deferred delete that survives atomic saves, and refusing to watch a macOS network share. It diverges where VS Code drops events: a Windows ReadDirectoryChangesW overflow arrives as a null file name and is recovered by rescanning the directory. This removes the mitigation from eclipse-theia#17633, restoring detection of a workspace folder deleted while a plugin watches its parent. Closes eclipse-theia#17700
EhabY
force-pushed
the
feat/non-recursive-file-watching
branch
from
August 23, 2026 15:36
1486eba to
afb6b17
Compare
EhabY
marked this pull request as ready for review
August 23, 2026 17:34
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What it does
Fixes #17700, supersedes the mitigation from #17633.
DiskFileSystemProvider.watchforwarded only{ ignored }, sorecursivewas dropped and every request became a recursive@parcel/watchersubscription: a non-recursive watch rooted outside the workspace crawled every sibling subtree (#17632), and a watch on a single file crawled its parent.recursiveis honored now. Recursive requests still go to parcel; non-recursive ones go to a newNodeDirectoryWatcherthat watches one directory level withfs.watch, and requests resolving to the same directory share one handle.MainFileSystemEventServiceno longer drops plugin watches rooted above a workspace root, which restores detection of a deleted workspace folder, the thing JDT-LS watches the parent for.Stacked: based on #17943, refactored further by #17944. Bases stay
masterbecause the base branches live on a fork.Decisions worth a look:
FSEventStreamon every handle start or stop, dropping in-flight events of every other watcher in the process. Excludes are per request, so different excludes still share a handle.readdir, becausestataccepts a differing case. Linux keeps the cheapstat. Mirrors VS Code'sexistsChildStrictCase.dev+ino+birthtimeMs, becauserm -rf dir && mkdir dirreuses the inode.client.onError, which drops the URI and shows a one-shot "large workspace" toast. Parcel watcher failures surface as a one-shot, path-less "large workspace" warning and never retry #17922 tracks the parcel side.Follows VS Code where settled: the 75 ms aggregation, the 100 ms deferred delete, NFC and case-insensitive matching, the
/Volumes/refusal (microsoft/vscode#106879). Diverges where VS Code drops events: a WindowsReadDirectoryChangesWoverflow arrives asfilename === nulland is recovered by rescanning, and a lost directory is polled at 500 ms with its missed changes replayed.Until #17943 merges this PR shows its commit too, so review just the feature commit:
git diff HEAD~1...HEAD. Nothing moves, so no copy detection is needed. Where the +1574/−213 goes:nodejs-watcher/node-directory-watcher.tsparcel-watcher/parcel-filesystem-service.tsrecursive, the renames with deprecated aliasesplugin-extmitigation removalmain-file-system-event-service.tsand its spec (#17633)recursiveflag in protocol and disk provider, DI module, imports, CHANGELOGHow to test
LightWeight):inotify-consumersshows the siblings are no longer watched, and deleting the workspace folder is detected.npx lerna run test --scope @theia/filesystemruns 43 cases: 32 on the watcher, 11 on routing and sharing. A substituted watch handle drives what no host can reproduce, such as buffer overflow, inode reuse, decomposed names and refused handles. Thewith a real fs.watch handleblock leaves platform truth to the CI matrix and is not platform-gated, so a regression on one host fails that host's job.Follow-ups
#17944 puts both watcher kinds behind one interface and fixes defects found while auditing this code against VS Code; they are listed there.
Known limitations, none regressions:
fs.watchis best-effort per Node's docs; a directory recreated reusing both inode and birth millisecond is not detected as replaced, the same gap as VS Code; macOS still rebuilds theFSEventStreamon any watcher start or stop; Windows recovers additions and deletions after an overflow but not updates; events buffered at restart are dropped and the children diff recovers additions and deletions; excludes match case-sensitively against absolute paths, same as the parcel watcher, so it should change in both places or neither.Breaking changes
[filesystem]:ParcelFileSystemWatcherServicerenamed toFileSystemWatcherServiceImplandPacelWatcherHandletoWatcherHandle, both keeping deprecated aliases; thewatchersmap now holds aWatcherInstanceof either kind, so overrides ofcreateWatcher/getWatcherKeyand readers ofwatchersmust check the new signatures.[plugin-ext]:MainFileSystemEventServicelost itsworkspaceServiceparameter andshouldSkipWatchmethod.Attribution
Review checklist
nlsservice (for details, please see the Internationalization/Localization section in the Coding Guidelines)Reminder for reviewers