Skip to content

Revert VS Code to canonical LSP file-watching, closing #31 (Q9) - #512

Merged
clrudolphi merged 5 commits into
masterfrom
fix/31-vscode-canonical-file-watching
Aug 28, 2026
Merged

Revert VS Code to canonical LSP file-watching, closing #31 (Q9)#512
clrudolphi merged 5 commits into
masterfrom
fix/31-vscode-canonical-file-watching

Conversation

@clrudolphi

Copy link
Copy Markdown
Collaborator

🤔 What's changed?

Removes the VS Code extension's client-side per-project output-assembly watcher (ProjectManager.watchProjectOutputPath/notifyOutputAssemblyChanged/_outputWatchers, added by #26) and the synthetic workspace/didChangeWatchedFiles notification it sent. The extension now relies solely on the server's standard dynamic registration (WatchedFilesHandler.cs) + vscode-languageclient's own generic FileSystemWatcherFeature — no Reqnroll-specific client code for this signal at all, same as it always could have been.

Also closes out issue #31 (Q9) in the design docs and adds two regression tests covering what the investigation actually found.

⚡️ What's your motivation?

Fixes #31.

#26 added the client-side glue on the (untested) assumption that vscode-languageclient's generic watcher wasn't reliably delivering workspace/didChangeWatchedFiles for **/bin/**/*.dll, specifically pointing at VS Code's files.watcherExclude (commonly set to exclude bin//obj/) as the likely cause. That assumption was investigated directly and found not to hold:

  • files.watcherExclude covering bin/ does suppress delivery — but it suppresses any createFileSystemWatcher-based approach equally, including the client-side glue. Narrowing the glob to a per-project RelativePattern didn't route around it. So the glue provided zero resilience against the one confirmed failure mode; it's not addressed by this PR either, and isn't specific to it.
  • Issue VS Code: "Define Step" code action has no visible effect #2's original symptom (VS Code's "Define missing step" never recovering after a post-open build) does not reproduce today. A VS Code Extension Host test that recreates the exact precondition — a real, unbuilt Reqnroll project registered before its first dotnet build, using a real bundled LSP server — recovers correctly with the client-side glue completely disabled.
  • Confirmed again outside the automated harness: two independent real dotnet builds in a live manual VS Code session against a real multi-project solution (one incremental rebuild, one full/clean rebuild producing 150+ dependency-DLL creation events as noise) were both correctly detected and filtered by the server, using canonical registration alone. Server-side log:
    HandleOutputAssemblyChange: Output assembly Changed: triggering discovery for 'Minimal'
    HandleOutputAssemblyChange: Output assembly Created: triggering discovery for 'Minimal'  (full rebuild, 150+ unrelated DLLs correctly filtered out)
    

Net: the client-side glue was pure duplicated intent — the exact concern issue #31 raised — with no measured reliability benefit under either configuration tested.

Rider was out of scope for this investigation (Visual Studio doesn't need this signal — VsProjectEventMonitor hooks DTE.Events.BuildEvents.OnBuildDone directly) and remains open, tracked in #511.

🏷️ What kind of change is this?

  • 🏦 Refactoring/debt/DX (improvement to code design, tooling, etc. without changing behaviour)
  • 📖 Documentation (improvements without changing code)

🧩 Area(s) touched

  • LSP server (src/LSP) — docs only
  • VS Code extension (src/VSCode)

♻️ Anything particular you want feedback on?

I only ran each side of the A/B comparison (glue enabled vs. disabled) once in the automated harness — the results were clean and the mechanism is deterministic, not flaky by nature, but a couple more repeats before merge would rule out a fluke if anyone wants extra confidence.

📋 Checklist:

  • I've changed the behaviour of the code
    • I have added/updated tests to cover my changes.
  • My change alters as-built behaviour described in a design doc under docs/
    • I have updated the relevant doc accordingly.
  • Users should know about my change
    • I have added an entry to the "[vNext]" section of the CHANGELOG.

clrudolphi and others added 5 commits August 28, 2026 16:23
…stration

Issue #31 (Q9) investigated whether the client-side per-project RelativePattern
DLL watcher added by PR #26 (for issue #2) is still needed. Two findings:

- files.watcherExclude covering bin/ suppresses any createFileSystemWatcher-based
  approach, including this client-side glue — narrowing the glob doesn't route
  around it, so the glue provided no resilience against the one confirmed
  failure mode.
- Issue #2's original symptom does not reproduce today. An Extension Host
  recreation (a real, unbuilt project registered before its first dotnet build,
  then built) recovers correctly with the client-side glue disabled — the
  server's standard workspace/didChangeWatchedFiles dynamic registration,
  delivered by vscode-languageclient's own FileSystemWatcherFeature with no
  Reqnroll-specific client code, is sufficient on its own under default
  VS Code settings.

Removes ProjectManager's per-project output-assembly watcher, its synthetic
didChangeWatchedFiles forwarding, and associated lifecycle tracking. Adds two
regression tests: one proving files.watcherExclude suppresses the watcher this
class used to create, one recreating issue #2 end-to-end against the real
bundled LSP server and a real dotnet build.

Experimental — pending manual verification before merge.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Marks Q9 resolved for VS Code and VS in docs/LSP-IDE-Support-Open-Questions.md
and docs/LSP-IDE-Support-Feature-Designs.md, citing both the Extension Host
recreation and live manual verification (two real dotnet builds against a
real multi-project solution, correctly detected and filtered using only the
canonical WatchedFilesHandler dynamic registration).

Rider was out of scope for this investigation and remains open, tracked in
a new follow-up issue (#511).

Adds a changelog entry and strengthens projectManager.ts's class doc with the
live-session confirmation.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
reqnroll.trace.server: verbose correctly drives vscode-languageclient's
tracing regardless of what's visible, but the Reqnroll LSP Trace channel is
a LogOutputChannel with its own independent display filter that only the
user can raise (Output panel dropdown / "Developer: Set Log Level..."), not
something the extension can set. Discovered while investigating #31: the
panel looked empty during manual testing even with tracing on, while the
file-based log under %LOCALAPPDATA%\Reqnroll\ had full content the whole
time.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
defineStepRecovery.test.ts assumed its __defineStepE2E__ fixture already
existed on disk, created out-of-band by hand during local investigation and
never committed to git. On a clean CI checkout it never existed at all, so
the test failed trying to open a nonexistent file. Now creates and removes
it itself via suiteSetup/suiteTeardown, making the suite fully self-contained.

That surfaced a real race once the fixture is created mid-session (via
ProjectManager's live .csproj watcher) instead of pre-existing at initial
workspace scan: the fixed 5s settle wait before running dotnet build wasn't
enough for registerProject's async msbuild evaluation to register the
project's OutputAssemblyPath first. Bumped to 15s.

Also adds actions/setup-dotnet to the VSCode CI job, which had no .NET SDK
setup step at all — the test's dotnet build had nothing to run against.

watcherExclude.test.ts's control case flaked on CI's Linux runner (false
!==true) — gave the watcher-arm delay and overall timeout more headroom
(300ms/5s -> 1s/10s) and added a settle delay after the config update, with
an explicit per-suite Mocha timeout to match.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Even with 10x the original timeouts (10s/90s), neither the watcherExclude
control case nor the #2 recreation test's discovery-retry ever fired on
this repo's xvfb-run + headless-Electron GitHub Actions runner -- including
the control case with no exclude at all, which has no reason to fail if
file-watch delivery works at all. That rules out a race and points at a
genuine capability gap in that specific CI sandbox: real OS-level
file-watch delivery for a freshly created nested directory doesn't appear
to work there, regardless of which watcher implementation (client-side glue
or canonical dynamic registration) is doing the watching -- so there's
nothing a longer wait or a different mechanism can fix.

Both suites now skip under CI (process.env.CI) and run fully locally / in
manual Extension Host runs, which is where every real finding in this
investigation actually came from. Also reverts the actions/setup-dotnet
addition to the VSCode CI job -- nothing in that job calls dotnet anymore
now that the only consumer is CI-skipped.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@clrudolphi
clrudolphi merged commit 9047f3b into master Aug 28, 2026
17 checks passed
@clrudolphi
clrudolphi deleted the fix/31-vscode-canonical-file-watching branch August 28, 2026 23:25
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.

Investigate and close Q9: reliability of workspace/didChangeWatchedFiles for build-completion detection

1 participant