Skip to content

Handle atomic folder renames/deletes in watched-files handling - #1463

Merged
davidanthoff merged 2 commits into
mainfrom
fix-folder-rename-watching
Aug 27, 2026
Merged

Handle atomic folder renames/deletes in watched-files handling#1463
davidanthoff merged 2 commits into
mainfrom
fix-folder-rename-watching

Conversation

@davidanthoff

Copy link
Copy Markdown
Member

Problem

Renaming a folder inside a workspace (e.g. testtest2) is never noticed by the server: the Test Explorer keeps stale test items from the old path and files under the new path are never discovered (no crash, simply no events).

Root cause, two layers:

  1. VS Code reports an atomic folder rename/delete as a single event for the folder path with no per-child events, and the registered watcher globs (**/*.{jl,jmd,md} + TOML lists) match only file paths — so the folder event is filtered out client-side and nothing reaches the server. (Same limitation gopls works around, see x/tools/gopls: explicitly watch for directories in file watching golang/go#42348; external tools that remove files one-by-one, e.g. git checkout, were unaffected.)
  2. Even a delivered directory event would no-op: the handler only processes exact file URIs — no prefix removal, no folder re-scan.

Fix

  • Registration (init.jl): for VS Code clients (detected via clientInfo, same heuristic already used in actions.jl and now factored into client_is_vscode), additionally register FileSystemWatcher("**", Create|Delete) — no glob can match "directories only", and VS Code's workspace watcher is recursive anyway, so this only widens event delivery — and restrict the extension globs to Change, so every event kind has exactly one source (no duplicate events). Other clients keep the previous registration unchanged, since some (e.g. Emacs-based) expand ** into one OS watcher per directory (the pyright problem).
  • Handler (workspace.jl): on Deleted, sweep all tracked files below the deleted path (prefix match with a trailing slash, so removing test/ does not sweep a sibling test2/); open files keep their in-memory content, matching the existing exact-URI behavior. On Created of a directory, scan its contents like a workspace folder at startup (reuses collect_folder_files!, including the MAX_WORKSPACE_JULIA_FILES cap). Irrelevant created files are already filtered by read_text_file_from_uri returning nothing.
  • Version bump 5.1.0 → 5.2.0 (new behavior, backwards compatible).

The existing downstream machinery (test item publish sweep, diagnostics) needs no changes — it just never fired because no file was ever removed/added.

Deliberately not using LSP workspace.fileOperations (didRenameFiles): the watcher sees the same atomic rename that explorer operations perform, and implementing both channels would double-report in-editor renames. Also deliberately not using JuliaWorkspaces.remove_all_children!, whose prefix match lacks the trailing-slash guard and ignores open files.

Merging/tagging sequencing

  • No JuliaWorkspaces changes are required — this PR is self-contained.
  • Merge order: this PR merges FIRST. The julia-vscode submodule-bump PR merges after it, pointing at this branch's head. julia-vscode consumes this repo as a submodule tracking latest main, so no registry tag is required for the extension.
  • Please use a merge commit (not squash): the julia-vscode PR's submodule pointer references commit e212895 on this branch; a squash-merge would leave that commit unreachable from main, and the julia-vscode PR would then need its pointer updated to the squashed commit before it can merge.
  • Tagging: if a registry release of 5.2.0 is desired, tag via Registrator/TagBot only after this PR is on main.

Testing

  • New test/test_watched_folders.jl (4 test items): folder delete sweeps children but not prefix-sharing siblings; folder create scans children (non-Julia files ignored); rename reported as delete+create in one notification; open files under a deleted folder stay in the workspace.
  • Full suite: 36/36 test items pass on Julia 1.12.

🤖 Generated with Claude Code

VS Code reports an atomic folder rename or delete as a single
didChangeWatchedFiles event for the folder path, with no events for the
files inside, and none of the registered file-extension globs match a
folder path - so the server never noticed and kept stale files (e.g.
test items from a renamed test/ folder).

For VS Code clients, additionally register a '**' watcher for
create/delete (no glob can match directories only) and restrict the
extension globs to change events so every event kind has exactly one
source. Other clients keep the previous registration, since some expand
'**' into one OS watcher per directory.

In the notification handler, sweep all tracked files below a deleted
path (prefix match with a trailing slash so removing test/ does not
sweep a sibling test2/) and scan the contents of a created directory.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread src/requests/init.jl Outdated
The folder-event problem is specific to VS Code's watcher, but every
Code-OSS fork (VSCodium, Cursor, Windsurf, Positron, ...) embeds the
same LSP client and watcher while reporting a different clientInfo
name, so the previous "code" substring check missed them.

Replace the gate with a gopls-style three-state design: the
julialangDirectoryWatching initialization option ("on"/"off", booleans
accepted) explicitly overrides, and the default "auto" mode sniffs
clientInfo.name against the Code-OSS family. Other clients stay off by
default - most watcher backends synthesize per-file events on folder
renames so the file globs already suffice, and some expand ** into one
OS watcher per directory - but any client can now opt in.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@davidanthoff

Copy link
Copy Markdown
Member Author

Pushed a follow-up commit that broadens the directory-watching gate beyond stock VS Code:

  • Why: the folder-only-event behavior is specific to VS Code's watcher stack — but every Code-OSS fork (VSCodium, Cursor, Windsurf, Positron, …) embeds the exact same vscode-languageclient + watcher while reporting a different clientInfo.name, so the previous occursin("code", …) check missed them. Most other clients don't need the workaround at all: their watcher backends (watchman, chokidar, …) synthesize per-file events on folder renames, so the plain file globs already suffice (same asymmetry gopls documents for its subdirWatchPatterns setting).
  • New design (gopls-style three-state): the julialangDirectoryWatching initialization option ("on"/"off", booleans accepted) explicitly overrides; the default auto mode sniffs clientInfo.name against the Code-OSS family list. Any client wrapper can now opt in or out regardless of its name.
  • The julia-vscode extension will pass julialangDirectoryWatching: 'on' explicitly (separate one-line PR), so forks running the extension get the fix even if their name is not on the sniff list.
  • New test item covering the gate (family names, opt-in/opt-out, unknown values fall back to auto); full suite 37/37.

@davidanthoff
davidanthoff merged commit c3bfa9f into main Aug 27, 2026
26 checks passed
@davidanthoff
davidanthoff deleted the fix-folder-rename-watching branch August 27, 2026 16:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants