From cdf6da702fff3a013a3c729dd55b9a5f57e7b375 Mon Sep 17 00:00:00 2001 From: Christian Findlay <16697547+MelbourneDeveloper@users.noreply.github.com> Date: Mon, 3 Aug 2026 20:45:25 +1000 Subject: [PATCH 01/29] Cleanup --- CLAUDE.md | 49 +- docs/bugs/BUILD-GENERATEDEPSFILE-LOCK-BUG.md | 138 --- docs/bugs/open-issues.csv | 56 + docs/plans/SIDECAR-LIFECYCLE-PLAN.md | 621 ++++++++++ docs/specs/BINARY-DEPLOYMENT.md | 274 ++--- docs/specs/DEBUGGING-SPEC.md | 342 ++---- docs/specs/DEFINITION-SPEC.md | 128 +- docs/specs/DESIGN-SYSTEM.md | 224 ++-- docs/specs/DIAGNOSTICS-SPEC.md | 194 +--- .../DIAGNOSTICS-STATIC-ANALYZERS-SPEC.md | 180 +-- docs/specs/DISTRIBUTION-SPEC.md | 61 +- docs/specs/HOVER-SPEC.md | 106 +- docs/specs/NUGET-BROWSER-SPEC.md | 149 +-- docs/specs/PACKAGE-MAINTENANCE-SPEC.md | 131 +-- docs/specs/PROFILER-SPEC.md | 222 ++-- docs/specs/REFERENCES-SPEC.md | 82 +- docs/specs/RENAME-SPEC.md | 8 +- docs/specs/SCRIPTING-FILEBASED-SPEC.md | 313 ++--- docs/specs/SHARPLSP-SPEC.md | 321 ++--- docs/specs/SIDECAR-LIFECYCLE-SPEC.md | 540 +++++++++ docs/specs/SOLUTION-EXPLORER-SPEC.md | 160 +-- docs/specs/VSCODE-REACTIVITY-SPEC.md | 55 +- website/eleventy.config.js | 27 +- website/package-lock.json | 8 +- website/package.json | 2 +- website/src/_data/i18n.json | 206 +++- website/src/_data/release.js | 42 +- website/src/_data/site.json | 10 +- website/src/_includes/layouts/base.njk | 50 +- website/src/_includes/layouts/blog.njk | 19 +- website/src/_includes/layouts/docs.njk | 45 +- website/src/_includes/layouts/prose.njk | 23 - .../src/_includes/overrides/blog-index.njk | 43 + website/src/_includes/partials/home.njk | 97 ++ website/src/_includes/partials/nav.njk | 85 +- website/src/_includes/partials/post-card.njk | 2 +- .../_includes/partials/releases-section.njk | 38 +- website/src/assets/css/pages.css | 184 +++ website/src/assets/css/prose.css | 139 +++ website/src/assets/css/styles.css | 1030 ++++------------- website/src/assets/js/custom.js | 78 +- website/src/docs/contributing.md | 2 +- website/src/docs/fsharp.md | 2 +- website/src/docs/index.md | 6 +- website/src/index.njk | 142 +-- website/src/ja/docs/contributing.md | 2 +- website/src/ja/docs/index.md | 6 +- website/src/ja/index.njk | 142 +-- website/src/zh/docs/contributing.md | 2 +- website/src/zh/docs/index.md | 6 +- website/src/zh/index.njk | 142 +-- website/tests/fsharp-docs.spec.js | 7 + website/tests/site-layout.spec.js | 131 +++ 53 files changed, 3422 insertions(+), 3650 deletions(-) delete mode 100644 docs/bugs/BUILD-GENERATEDEPSFILE-LOCK-BUG.md create mode 100644 docs/bugs/open-issues.csv create mode 100644 docs/plans/SIDECAR-LIFECYCLE-PLAN.md create mode 100644 docs/specs/SIDECAR-LIFECYCLE-SPEC.md create mode 100644 website/src/_includes/overrides/blog-index.njk create mode 100644 website/src/_includes/partials/home.njk create mode 100644 website/src/assets/css/pages.css create mode 100644 website/src/assets/css/prose.css create mode 100644 website/tests/site-layout.spec.js diff --git a/CLAUDE.md b/CLAUDE.md index 0ce610d4..438ceb66 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,17 +1,15 @@ # CLAUDE.md -⚠️ Never kill VS Code processes — not desktop, not browser. They belong to the user. ⚠️ +⚠️ Never kill VS Code processes — not desktop, not browser. They belong to the user. ⚠️ -⚠️ Don't ask the user questions — use your judgment. ⚠️ +⚠️ Don't ask the user questions — use your judgment. ⚠️ -⚠️ Don't use git. Especially critical: don't stamp yourself as coauthor on commits ⚠️ - -> "Git" here means **version-control operations**: commits, branches, merges, rebases, tags, pushes — and never stamping yourself as coauthor. It does **NOT** mean GitHub. **GitHub issues are allowed and encouraged** — use the `gh` CLI to file, comment on, and manage issues for bugs and tracking. GitHub ≠ Git. +⚠️ Don't perform Git version-control operations (commits, branches, merges, rebases, tags, or pushes) or add yourself as coauthor. GitHub issues are allowed and encouraged via `gh`. ⚠️ SharpLsp is an open-source, editor-agnostic .NET LSP (C# + F#) built in Rust. One LSP server = complete .NET development experience across every editor. **Overall aim #1: FIX THE .NET DEVELOPER EXPERIENCE.** -Match — and ultimately go beyond — Visual Studio, Rider, and C# Dev Kit. Full feature-for-feature parity, then more. Zero proprietary dependencies. Zero licenses. Zero vendor lock-in. +Match and surpass Visual Studio, Rider, and C# Dev Kit without proprietary dependencies, licenses, or vendor lock-in. **Overall aim #2: TREAT F# AS A FIRST CLASS CITIZEN.** F# ahead of C# when building new features. F# never takes the backseat. @@ -20,23 +18,18 @@ F# ahead of C# when building new features. F# never takes the backseat. ## Principles -This code would pass a review at Google, Meta, or Microsoft. No bad or duplicate code. Grade A+. Anything less must be fixed immediately. +Write review-ready, maintainable code with no duplication. - Logging is critical. Use structured logging: `tracing` crate in Rust, `ILogger` + Serilog in .NET. No raw `println!`/`Console.WriteLine`/`console.log` for diagnostics -- 100% test coverage is only the start -- Use libraries like Signals for reactivity -- No feature is complete without e2e tests -- Building a feature without tests is not allowed -- No unit tests. Only COARSE e2e tests +- Every feature requires coarse end-to-end tests; do not add unit tests ## Hard Rules -- Do not use Git. - All screens MUST BE 100% reactive. If underlying data changes, the screen must be listening and update accordingly - Zero duplication. Apply DRY rigorously. Check for existing code before writing new code — highest priority - Any function that can throw/panic must return Result (outcome package in .NET) - Avoid RegEx and string matching. Always use ACTUAL parsers and traverse the AST/CST -- **NEVER hand-manipulate structured files.** XML (csproj/fsproj/props/vsixmanifest), JSON, TOML, YAML, solution files, etc. MUST be loaded into a proper document model, mutated via the DOM/AST, and serialized back. Line splicing, regex replacement, and string concatenation on structured files are not permitted. No exceptions for "performance" or "formatting preservation" — use a parser that preserves trivia (e.g. `Microsoft.Build.Construction` for MSBuild, `XDocument`/`quick-xml` with trivia preservation for XML, `serde_json` with `preserve_order` for JSON). +- **Never hand-manipulate structured files.** Load XML, JSON, TOML, YAML, and solution files into a proper DOM/AST, mutate the model, and serialize it with a trivia-preserving parser where needed. Do not use line splicing, regex replacement, or string concatenation. Prefer Microsoft.Build.Construction for MSBuild, XDocument or quick-xml for XML, and serde_json with preserve_order for JSON. - `allow(clippy::` is not permitted without a strong, documented reason. **Aggressively remove** existing allows. - All code files < 500 LOC. Functions < 20 LOC - Aggressively move shared code to shared crates/modules @@ -48,10 +41,8 @@ This code would pass a review at Google, Meta, or Microsoft. No bad or duplicate 100% test coverage and high mutation score. Focus on assertions, not just coverage. -- Never delete failing tests -- Never remove assertions that cause test failures -- Add more failing tests for broken/missing functionality — never remove them -- Do not reduce test assertiveness to make tests pass +- Never delete failing tests or remove/weaken assertions to make tests pass +- Add failing tests for broken or missing functionality - Tests must not be skipped or ignored - Test against real .sln/.csproj/.fsproj files, not mocks @@ -127,8 +118,6 @@ All documentation lives in `docs/`. Every spec section MUST have a hierarchical ID: `[GROUP-TOPIC]` or `[GROUP-TOPIC-DETAIL]`. IDs are uppercase, hyphen-separated, NEVER numbered. The first word is the group — sections sharing a group must be adjacent. All code and tests implementing a spec section MUST reference its ID in a comment (e.g., `// Implements [AUTH-TOKEN-VERIFY]`). -Always propagate these to code and tests. We want as much cross-referencing as possible - # Critical Docs - [LSP Specification 3.17](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/) @@ -147,25 +136,14 @@ Three-tier architecture: IPC: MessagePack over named pipes (Windows) / Unix domain sockets (Linux, macOS). 4-byte LE length prefix framing. Target <500us round-trip overhead. -C# and F# are equal first-class citizens. F# is NOT a second-class bolt-on. - See `docs/specs/SHARPLSP-SPEC.md` for the full technical specification. -## Code Structure - -- Small, focused functions (<20 lines) -- Low cognitive complexity (clippy::cognitive_complexity enabled) -- Descriptive variable names (no single letters except in closures) -- Group related functionality into modules -- Public APIs must have documentation - ## Bug Fix Process 1. Write a test that fails because of the bug -2. Run the test — confirm it fails BECAUSE of the bug -3. Repeat until it's failing for the right reason -4. Fix the bug (do NOT change the test) -5. Run the test — confirm it passes +2. Run it and confirm the bug is the reason it fails +3. Fix the bug without changing the test +4. Run the test and confirm it passes ## Performance Targets @@ -189,8 +167,9 @@ See `docs/specs/SHARPLSP-SPEC.md` for the full technical specification. ## Website and CSS - **MINIMIZE CSS CLASSES** — consolidate where possible +- CSS Budget 2k LOC - Name classes after what the element IS, not what section it's in -- **Do not use common LLM colors like purple** — use RNG and color wheels +- Avoid default LLM palettes such as purple ## Key Technology Stack diff --git a/docs/bugs/BUILD-GENERATEDEPSFILE-LOCK-BUG.md b/docs/bugs/BUILD-GENERATEDEPSFILE-LOCK-BUG.md deleted file mode 100644 index b727e431..00000000 --- a/docs/bugs/BUILD-GENERATEDEPSFILE-LOCK-BUG.md +++ /dev/null @@ -1,138 +0,0 @@ -# BUILD-GENERATEDEPSFILE-LOCK — `GenerateDepsFile` fails: deps.json "used by another process" - -- **Status:** RESOLVED (2026-06-22) — see [Resolution](#resolution) -- **Severity:** Critical — blocks `dotnet build` of a sidecar project -- **Date logged:** 2026-06-22 -- **Reporter:** Christian Findlay -- **Tracking:** GitHub issue [#111](https://github.com/Nimblesite/SharpLsp/issues/111) -- **Area:** Build / .NET sidecars (`sidecars/SharpLsp.Sidecar.Common`) -- **Reproducibility:** Intermittent (file-lock race) - -## Resolution - -`SharpLsp.Sidecar.Common` is a **referenced-only class library** — its `deps.json` -is never read at runtime (the executable sidecar `SharpLsp.Sidecar.CSharp` and the -test project each generate their own `deps.json`, which already enumerate Common's -dependency graph). That unused artifact existed only to be re-written into `bin/` -on every build, where a transient holder (build-server node / Spotlight indexer) -could lock it and fail the `GenerateDepsFile` task with MSB4018. - -**Fix:** set `false` on the Common -library ([sidecars/SharpLsp.Sidecar.Common/SharpLsp.Sidecar.Common.csproj](../../sidecars/SharpLsp.Sidecar.Common/SharpLsp.Sidecar.Common.csproj)). -No `deps.json` is generated for Common, so the lock-prone write — and the MSB4018 -failure — can no longer occur for this project. - -**Test:** [tests/build_deps_file_e2e.rs](../../tests/build_deps_file_e2e.rs) — -`common_library_disables_dependency_file_generation` evaluates the real `.csproj` -via `dotnet msbuild -getProperty:GenerateDependencyFile` and asserts it is `false` -(failed pre-fix with `true`, passes post-fix). - -**Verification:** full `SharpLsp.Sidecars.sln` build succeeds (0 warnings, 0 errors); -Common emits its DLL but no `deps.json`; `SharpLsp.Sidecar.CSharp.deps.json` still -lists Common (runtime unaffected). - -**Follow-up (not blocking):** executable projects (CSharp/FSharp sidecars) legitimately -need a `deps.json` and could still hit the same transient lock. If it recurs there, -apply the systemic mitigation (disable MSBuild server / node reuse for repo + CI -builds) tracked in the original analysis below. - -## Symptom - -Building the Common sidecar project on its own fails during the -`GenerateDepsFile` MSBuild task with an `IOException` saying the generated -`deps.json` is locked by another process. - -``` -dotnet build sidecars/SharpLsp.Sidecar.Common/SharpLsp.Sidecar.Common.csproj - -/usr/local/share/dotnet/sdk/10.0.203/Sdks/Microsoft.NET.Sdk/targets/Microsoft.NET.Sdk.targets(308,5): error MSB4018: - The "GenerateDepsFile" task failed unexpectedly. - System.IO.IOException: The process cannot access the file - '.../sidecars/SharpLsp.Sidecar.Common/bin/Debug/net10.0/SharpLsp.Sidecar.Common.deps.json' - because it is being used by another process. - at Microsoft.Win32.SafeHandles.SafeFileHandle.Init(...) - at System.IO.File.Create(String path) - at Microsoft.NET.Build.Tasks.GenerateDepsFile.WriteDepsFile(String depsFilePath) - at Microsoft.NET.Build.Tasks.TaskBase.Execute() - -Build failed with 1 error(s) in 2.9s -``` - -## Reproduction (as observed) - -1. Build the whole sidecar solution — **succeeds**: - `dotnet build sidecars/SharpLsp.Sidecars.sln` -2. Immediately build the Common project alone — **fails** with the error above: - `dotnet build sidecars/SharpLsp.Sidecar.Common/SharpLsp.Sidecar.Common.csproj` - -The failure does not reproduce every time — a subsequent build wrote the file -successfully (it is present on disk, 8906 bytes), confirming a transient lock -rather than a permanently held handle. - -## Environment - -- **OS:** macOS (Darwin 25.5.0), arm64 -- **.NET SDK:** 10.0.203 -- **Target:** `net10.0` -- **Project:** `SharpLsp.Sidecar.Common` (class library — no `OutputType`) - -## Diagnostics captured at time of failure - -- No process was holding the `deps.json` by the time `lsof` ran (lock had already - been released — consistent with a transient/race lock). -- A persistent Roslyn build-server node was alive and had started right around - the failing build: - `…/sdk/10.0.203/Roslyn/bincore/VBCSCompiler -pipename:…` (started ~21:02). -- Several **long-running SharpLsp sidecar processes** were running, but all from - the **installed VS Code extension** directories - (`~/.vscode/extensions/nimblesite.sharplsp-*/bin/all/sharplsp-sidecar-*`), - **not** from the repo's `bin/Debug` output. These therefore do not hold a - handle on the repo's `deps.json` and are not the direct cause, though they - confirm sidecars are designed to be long-lived. - -## Suspected root cause - -A race on the freshly written `bin/Debug/net10.0/SharpLsp.Sidecar.Common.deps.json`: -`GenerateDepsFile` calls `File.Create` while another process still has a handle -open on the just-emitted file. On macOS the most likely transient holders are: - -1. **Persistent build-server / compiler node** (`VBCSCompiler`, MSBuild node - reuse) carrying handles to project outputs across back-to-back builds — note a - live `VBCSCompiler` was observed at failure time. -2. **Spotlight / file indexing** (`mdworker`/`mds`) momentarily opening the newly - created `.deps.json`. -3. **Concurrent writers** to the same output path — e.g. the IDE's background - build (or an in-flight build-server request) overlapping the CLI build of the - same project right after a full-solution build. - -The intermittency, the prior full-solution build, and the live build-server node -together point at handle reuse / indexing rather than a SharpLsp code defect. - -## Workarounds (not yet verified as fixes) - -- Disable build-server/node reuse for the failing build: - `dotnet build … /nodeReuse:false /p:UseRazorBuildServer=false` and/or - `dotnet build-server shutdown` before rebuilding. -- Re-run the build (the lock is transient and usually clears on retry). -- `export DOTNET_CLI_USE_MSBUILD_SERVER=0` for repo builds. -- Exclude `**/bin/` and `**/obj/` from Spotlight indexing for this workspace. - -## Proposed fix / next steps - -- [ ] Reproduce deterministically (tight loop alternating solution build then - single-project build; vary `nodeReuse`/build-server on and off). -- [ ] Confirm which process holds the handle (`lsof` in a loop, or `fs_usage` - filtered on `deps.json` during the build) — distinguish build-server vs. - `mdworker`. -- [ ] If build-server/node reuse is the cause, standardize repo builds (Makefile - / CI) on `nodeReuse:false` or `DOTNET_CLI_USE_MSBUILD_SERVER=0`, and keep - `.devcontainer`/`ci.yml` in sync per CLAUDE.md. -- [ ] Consider a build-output exclusion from indexing as a developer-environment - note in the README. -- [ ] Do **not** kill VS Code processes as part of any fix (CLAUDE.md hard rule). - -## Notes - -Per CLAUDE.md this repo does not use Git/GitHub issues for tracking, so this bug -is logged here under `docs/bugs/`. Move to `docs/specs`/`docs/plans` only if it -turns into a structural change to the build setup. diff --git a/docs/bugs/open-issues.csv b/docs/bugs/open-issues.csv new file mode 100644 index 00000000..e6d335eb --- /dev/null +++ b/docs/bugs/open-issues.csv @@ -0,0 +1,56 @@ +"repository","issue_number","issue_url","title","author_login","author_name","summary_line_1","summary_line_2","labels","issue_date","created_at_utc","updated_at_utc","state","comments_count","component","functionality_area","functionality_subarea","language_scope","area_source","area_confidence","parent_issue_number","child_issue_numbers" +"Nimblesite/SharpLsp","9","https://github.com/Nimblesite/SharpLsp/issues/9","Rename: classes, structs, interfaces, records, and delegates","MelbourneDeveloper","MelbourneDeveloper","Add semantic rename for C# and F# type-like declarations.","Update constructors and references across real solutions with invalid-name e2e coverage.",".NET; cluster:rename","2026-04-27","2026-04-27T06:57:52Z","2026-08-03T09:45:12Z","open","0",".NET sidecars (Roslyn/FCS)","Code actions & refactoring","Rename","C#; F#","inferred","high","","" +"Nimblesite/SharpLsp","10","https://github.com/Nimblesite/SharpLsp/issues/10","Rename: enums","MelbourneDeveloper","MelbourneDeveloper","Add semantic rename for C# and F# enum declarations.","Update every type reference across real solutions and cover invalid names end to end.",".NET; cluster:rename","2026-04-27","2026-04-27T06:57:54Z","2026-08-03T09:45:13Z","open","0",".NET sidecars (Roslyn/FCS)","Code actions & refactoring","Rename","C#; F#","inferred","high","","" +"Nimblesite/SharpLsp","11","https://github.com/Nimblesite/SharpLsp/issues/11","Rename: enum members","MelbourneDeveloper","MelbourneDeveloper","Add semantic rename for C# enum members and F# enum cases.","Update declarations and all semantic usages with invalid-name e2e coverage.",".NET; cluster:rename","2026-04-27","2026-04-27T06:57:55Z","2026-08-03T09:45:14Z","open","0",".NET sidecars (Roslyn/FCS)","Code actions & refactoring","Rename","C#; F#","inferred","high","","" +"Nimblesite/SharpLsp","12","https://github.com/Nimblesite/SharpLsp/issues/12","Rename: methods, functions, local functions, and operators","MelbourneDeveloper","MelbourneDeveloper","Add semantic rename for C# methods, functions, and operators plus their F# equivalents.","Update declarations and references with invalid-name e2e coverage.",".NET; cluster:rename","2026-04-27","2026-04-27T06:57:58Z","2026-08-03T09:45:15Z","open","0",".NET sidecars (Roslyn/FCS)","Code actions & refactoring","Rename","C#; F#","inferred","high","","" +"Nimblesite/SharpLsp","13","https://github.com/Nimblesite/SharpLsp/issues/13","Rename: constructors through containing type rename","MelbourneDeveloper","MelbourneDeveloper","Keep constructor declarations and calls correct when their containing type is renamed.","Treat constructors as dependent references rather than independently renameable symbols.",".NET; cluster:rename","2026-04-27","2026-04-27T06:57:59Z","2026-08-03T09:45:16Z","open","0",".NET sidecars (Roslyn/FCS)","Code actions & refactoring","Rename","C#; F#","inferred","high","","" +"Nimblesite/SharpLsp","14","https://github.com/Nimblesite/SharpLsp/issues/14","Rename: properties and indexers","MelbourneDeveloper","MelbourneDeveloper","Add semantic rename for C# and F# properties and indexers.","Update accessors, implementations, overrides, and references across real solutions.",".NET; cluster:rename","2026-04-27","2026-04-27T06:58:01Z","2026-08-03T09:45:17Z","open","0",".NET sidecars (Roslyn/FCS)","Code actions & refactoring","Rename","C#; F#","inferred","high","","" +"Nimblesite/SharpLsp","15","https://github.com/Nimblesite/SharpLsp/issues/15","Rename: fields and events","MelbourneDeveloper","MelbourneDeveloper","Add semantic rename for C# fields, constants, and events plus F# fields, values, and events.","Update declarations and references with invalid-name e2e coverage.",".NET; cluster:rename","2026-04-27","2026-04-27T06:58:03Z","2026-08-03T09:45:18Z","open","0",".NET sidecars (Roslyn/FCS)","Code actions & refactoring","Rename","C#; F#","inferred","high","","" +"Nimblesite/SharpLsp","16","https://github.com/Nimblesite/SharpLsp/issues/16","Rename: local variables and pattern/deconstruction bindings","MelbourneDeveloper","MelbourneDeveloper","Add semantic rename for local and pattern-bound variables in C# and F#.","Cover loops, catches, deconstruction, patterns, and local bindings across their valid scopes.",".NET; cluster:rename","2026-04-27","2026-04-27T06:58:05Z","2026-08-03T09:45:19Z","open","0",".NET sidecars (Roslyn/FCS)","Code actions & refactoring","Rename","C#; F#","inferred","high","","" +"Nimblesite/SharpLsp","17","https://github.com/Nimblesite/SharpLsp/issues/17","Rename: parameters and lambda parameters","MelbourneDeveloper","MelbourneDeveloper","Add semantic rename for parameters in methods, constructors, functions, delegates, and lambdas.","Support both C# and F# parameter forms with invalid-name e2e coverage.",".NET; cluster:rename","2026-04-27","2026-04-27T06:58:07Z","2026-08-03T09:45:20Z","open","0",".NET sidecars (Roslyn/FCS)","Code actions & refactoring","Rename","C#; F#","inferred","high","","" +"Nimblesite/SharpLsp","18","https://github.com/Nimblesite/SharpLsp/issues/18","Rename: namespaces and modules","MelbourneDeveloper","MelbourneDeveloper","Add semantic rename for C# and F# namespaces, modules, and applicable aliases.","Update declarations and semantic references across real solutions.",".NET; cluster:rename","2026-04-27","2026-04-27T06:58:09Z","2026-08-03T09:45:21Z","open","0",".NET sidecars (Roslyn/FCS)","Code actions & refactoring","Rename","C#; F#","inferred","high","","" +"Nimblesite/SharpLsp","19","https://github.com/Nimblesite/SharpLsp/issues/19","Rename: generic type parameters","MelbourneDeveloper","MelbourneDeveloper","Add semantic rename for generic type parameters in C# and F#.","Support both type-level and method-level parameters with invalid-name e2e coverage.",".NET; cluster:rename","2026-04-27","2026-04-27T06:58:11Z","2026-08-03T09:45:22Z","open","0",".NET sidecars (Roslyn/FCS)","Code actions & refactoring","Rename","C#; F#","inferred","high","","" +"Nimblesite/SharpLsp","20","https://github.com/Nimblesite/SharpLsp/issues/20","Rename: aliases and type abbreviations","MelbourneDeveloper","MelbourneDeveloper","Add safe semantic rename for C# using aliases and F# type abbreviations or module aliases.","Limit support to locations exposed reliably by compiler services.",".NET; cluster:rename","2026-04-27","2026-04-27T06:58:12Z","2026-08-03T09:45:23Z","open","0",".NET sidecars (Roslyn/FCS)","Code actions & refactoring","Rename","C#; F#","inferred","high","","" +"Nimblesite/SharpLsp","21","https://github.com/Nimblesite/SharpLsp/issues/21","Rename: F# record fields","MelbourneDeveloper","MelbourneDeveloper","Add semantic rename for F# record fields.","Update declarations, construction and copy expressions, patterns, and field access sites.",".NET; cluster:rename","2026-04-27","2026-04-27T06:58:14Z","2026-08-03T09:45:24Z","open","0","F# sidecar (FCS)","Code actions & refactoring","Rename","F# only","inferred","high","","" +"Nimblesite/SharpLsp","22","https://github.com/Nimblesite/SharpLsp/issues/22","Rename: F# discriminated union cases","MelbourneDeveloper","MelbourneDeveloper","Add semantic rename for F# discriminated union cases.","Update declarations, construction sites, and pattern matches across real solutions.",".NET; cluster:rename","2026-04-27","2026-04-27T06:58:16Z","2026-08-03T09:45:25Z","open","0","F# sidecar (FCS)","Code actions & refactoring","Rename","F# only","inferred","high","","" +"Nimblesite/SharpLsp","23","https://github.com/Nimblesite/SharpLsp/issues/23","Rename: F# active patterns","MelbourneDeveloper","MelbourneDeveloper","Add semantic rename for F# active patterns.","Update declarations and every pattern usage site with invalid-name e2e coverage.",".NET; cluster:rename","2026-04-27","2026-04-27T06:58:18Z","2026-08-03T09:45:26Z","open","0","F# sidecar (FCS)","Code actions & refactoring","Rename","F# only","inferred","high","","" +"Nimblesite/SharpLsp","33","https://github.com/Nimblesite/SharpLsp/issues/33","feat: update Shipwright repo fixtures for Forge after all deployment issues are resolved","MelbourneDeveloper","MelbourneDeveloper","Synchronize Shipwright’s Forge manifest and version-output fixtures after prerequisite deployment work.","Validate all fixtures with Shipwright’s test suite before closing.","shipwright","2026-04-28","2026-04-28T10:48:14Z","2026-06-23T21:31:31Z","open","0","Shipwright repository","Distribution & platform","Shipwright fixtures","Editor/tooling (language-agnostic)","label","high","","" +"Nimblesite/SharpLsp","43","https://github.com/Nimblesite/SharpLsp/issues/43","Shipwright deployment-contract hardening checklist","MelbourneDeveloper","MelbourneDeveloper","Bring release and IDE deployment into full Shipwright contract compliance.","Harden version checks, Actions permissions and pinning, provenance, SBOMs, checksums, and trusted publishing.","shipwright","2026-06-05","2026-06-05T21:16:02Z","2026-06-05T21:36:56Z","open","0","Release workflows + editor integrations","Security","Supply-chain and deployment hardening","Editor/tooling (language-agnostic)","inferred","high","","45; 46; 47; 48" +"Nimblesite/SharpLsp","45","https://github.com/Nimblesite/SharpLsp/issues/45","Enforce the expected version during resolution","MelbourneDeveloper","MelbourneDeveloper","Reject resolved binaries whose versions differ from the expected release.","Surface a precise startup error under the Shipwright contract.","shipwright","2026-06-05","2026-06-05T21:37:10Z","2026-06-05T21:37:10Z","open","0","Binary resolution","Distribution & platform","Expected-version enforcement","Editor/tooling (language-agnostic)","label","high","43","" +"Nimblesite/SharpLsp","46","https://github.com/Nimblesite/SharpLsp/issues/46","Add Zed LSP-initialize version enforcement","MelbourneDeveloper","MelbourneDeveloper","Enforce binary compatibility during Zed’s LSP initialize handshake.","Reject version mismatches under the Shipwright contract.","shipwright","2026-06-05","2026-06-05T21:37:12Z","2026-06-05T21:37:12Z","open","0","Zed extension","Distribution & platform","Zed version compatibility","Editor/tooling (language-agnostic)","label","high","43","" +"Nimblesite/SharpLsp","47","https://github.com/Nimblesite/SharpLsp/issues/47","Sign + notarize the macOS binaries","MelbourneDeveloper","MelbourneDeveloper","Developer ID sign, notarize, and staple every macOS host and sidecar binary.","Retain cosign provenance alongside Apple signing.","cluster:macos-release; shipwright","2026-06-05","2026-06-05T21:37:38Z","2026-08-03T09:45:27Z","open","0","Release workflows","Distribution & platform","macOS signing and notarization","Editor/tooling (language-agnostic)","label","high","43","" +"Nimblesite/SharpLsp","48","https://github.com/Nimblesite/SharpLsp/issues/48","Windows code signing — current position","MelbourneDeveloper","MelbourneDeveloper","Track the unresolved path to native Windows Authenticode signing.","Use package-manager trust and cosign provenance until a durable option is chosen.","shipwright","2026-06-05","2026-06-05T21:37:45Z","2026-06-05T21:37:45Z","open","0","Release workflows","Distribution & platform","Windows code signing","Editor/tooling (language-agnostic)","label","high","43","" +"Nimblesite/SharpLsp","107","https://github.com/Nimblesite/SharpLsp/issues/107","[Feature]: F# treesitter","ShalokShalom","ShalokShalom","Evaluate the newer generic tree-sitter grammar for F# parsing.","Compare it with the older Neovim-focused implementation before adoption.","","2026-06-20","2026-06-20T15:41:16Z","2026-08-03T09:26:42Z","open","1","Rust LSP host","Core LSP & runtime","F# syntax parsing","F# only","issue_form","high","","" +"Nimblesite/SharpLsp","122","https://github.com/Nimblesite/SharpLsp/issues/122","F#: completion auto-`open` insertion (FSAC parity) needs an unopened-symbol entity index","MelbourneDeveloper","MelbourneDeveloper","Index symbols from referenced assemblies and current F# files so unopened namespaces appear in completion.","Cache insertion edits so resolved completions can add the required open directive.","cluster:fsharp-project-model","2026-06-24","2026-06-24T08:34:56Z","2026-08-03T09:45:28Z","open","0","F# sidecar (FCS)","Code intelligence","F# completion auto-open insertion","F# only","inferred","high","","" +"Nimblesite/SharpLsp","123","https://github.com/Nimblesite/SharpLsp/issues/123","NuGet browser: finish post-install sidecar workspace reload + prerelease/restore polish","MelbourneDeveloper","MelbourneDeveloper","Reload sidecar workspaces after NuGet install or uninstall and add prerelease and restore workflows.","Later add caching, cancellation, performance coverage, and notification-ordering tests.","cluster:nuget-pipeline","2026-06-24","2026-06-24T09:11:43Z","2026-08-03T09:45:29Z","open","0","NuGet browser + .NET sidecars","NuGet & package management","Workspace reload and prerelease/restore","C#; F#","inferred","high","","" +"Nimblesite/SharpLsp","142","https://github.com/Nimblesite/SharpLsp/issues/142","Flaky e2e: stale F# error diagnostic survives 60s after file correction (clears-diagnostics race)","abdushakoor12","abdushakoor12","A flaky F# diagnostic can remain for 60 seconds after corrected content is closed and reopened.","Investigate stale pull-result versioning and FCS invalidation, adding an old-version guard if the race is confirmed.",".NET; bug","2026-07-09","2026-07-09T07:50:50Z","2026-07-15T09:36:04Z","open","0","F# sidecar (FCS)","Diagnostics & analyzers","F# stale-diagnostic clearing","F# only","inferred","high","","" +"Nimblesite/SharpLsp","150","https://github.com/Nimblesite/SharpLsp/issues/150","Sidecar listener failure is invisible: exit 0, no stderr, error only in temp log","MelbourneDeveloper","MelbourneDeveloper","Make sidecar listener failures visible through nonzero exits and stderr diagnostics.","Have the Rust host report child status and the sidecar log path when READY never arrives.",".NET; bug; cluster:sidecar-startup","2026-07-15","2026-07-15T09:18:02Z","2026-08-03T09:45:30Z","open","0",".NET sidecar common + Rust LSP host","Core LSP & runtime","Sidecar startup observability","C#; F#","inferred","high","","" +"Nimblesite/SharpLsp","151","https://github.com/Nimblesite/SharpLsp/issues/151","Deterministic sidecar IPC endpoint collides across hosts on the same workspace","MelbourneDeveloper","MelbourneDeveloper","Generate unique sidecar IPC endpoints for concurrent hosts on one workspace.","Prevent Windows pipe collisions and Unix socket stealing, with busy-pipe retry as defense in depth.",".NET; bug; cluster:multi-host-isolation; cluster:sidecar-startup; critical","2026-07-15","2026-07-15T09:18:08Z","2026-08-03T09:45:31Z","open","0","Rust LSP host + .NET sidecars","Core LSP & runtime","IPC endpoint allocation","C#; F#","inferred","high","","" +"Nimblesite/SharpLsp","152","https://github.com/Nimblesite/SharpLsp/issues/152","Sidecar spawn-time failures bypass crash backoff — unthrottled respawn storm","MelbourneDeveloper","MelbourneDeveloper","Apply crash backoff when a sidecar fails before completing startup.","Prevent semantic requests from spawning unthrottled doomed processes.","bug; cluster:sidecar-startup; critical","2026-07-15","2026-07-15T09:18:12Z","2026-08-03T09:45:32Z","open","0","Rust LSP host","Core LSP & runtime","Sidecar crash backoff","C#; F#","inferred","high","","" +"Nimblesite/SharpLsp","153","https://github.com/Nimblesite/SharpLsp/issues/153","SidecarHost.MessageLoopAsync hot-loops on persistent transport exceptions","MelbourneDeveloper","MelbourneDeveloper","Stop the sidecar message loop after persistent transport exceptions.","Avoid hot zombie processes that consume CPU and flood logs.",".NET; bug; cluster:sidecar-lifecycle; critical","2026-07-15","2026-07-15T09:18:17Z","2026-08-03T09:45:33Z","open","0","Shared .NET sidecar infrastructure","Core LSP & runtime","IPC exception handling","C#; F#","inferred","high","","" +"Nimblesite/SharpLsp","154","https://github.com/Nimblesite/SharpLsp/issues/154","READY echoes the requested endpoint even when the listener bound a relocated path","MelbourneDeveloper","MelbourneDeveloper","A latent long-Unix-socket-path bug reports the requested endpoint instead of the shortened bound path.","Prevent the host from connecting to the original unbound path.",".NET; bug; cluster:sidecar-startup","2026-07-15","2026-07-15T09:18:22Z","2026-08-03T09:45:34Z","open","0","Rust LSP host + shared .NET sidecar infrastructure","Core LSP & runtime","Unix socket endpoint reporting","C#; F#","inferred","high","","" +"Nimblesite/SharpLsp","155","https://github.com/Nimblesite/SharpLsp/issues/155","Sidecar solution restore broken: MessagePack 3.1.7/3.1.8 downgrade + FSharp.Core 10.1.301 violates FCS exact pin","MelbourneDeveloper","MelbourneDeveloper","Align MessagePack versions across sidecar projects and match FSharp.Core to FCS’s exact requirement.","Add dependency-consistency guards to prevent restore-blocking version drift.",".NET; bug; showstopper","2026-07-15","2026-07-15T11:40:26Z","2026-08-03T09:43:46Z","open","0",".NET sidecars","Engineering infrastructure",".NET dependency consistency","C#; F#","inferred","high","","" +"Nimblesite/SharpLsp","157","https://github.com/Nimblesite/SharpLsp/issues/157","[Feature]: .sqlproj support","Dazfl","Dazfl","SharpLsp does not load SDK-style .sqlproj projects when opening a solution.","Support should include SQL projects alongside C# and F# projects during solution loading.","","2026-07-15","2026-07-15T22:00:28Z","2026-08-03T09:54:54Z","open","2","C# sidecar (Roslyn)","Workspace & project system",".sqlproj loading","C#; F#","inferred","medium","","" +"Nimblesite/SharpLsp","161","https://github.com/Nimblesite/SharpLsp/issues/161","C# rename returns a single whole-document TextEdit — SourceText normalization defeats granular GetTextChanges (contrary to its own comment)","MelbourneDeveloper","MelbourneDeveloper","C# rename emits one whole-document TextEdit instead of granular symbol edits.","Using unrelated SourceText instances loses change history and disrupts editor state and partial previews.","bug; cluster:rename","2026-07-16","2026-07-16T13:00:45Z","2026-08-03T09:45:35Z","open","0","C# sidecar (Roslyn)","Code actions & refactoring","Rename edit granularity","C# only","inferred","high","","" +"Nimblesite/SharpLsp","162","https://github.com/Nimblesite/SharpLsp/issues/162","make _test-vsix cannot go green on Windows — Linux-shaped staging assertions + temp-dir EPERM cleanup flakes","MelbourneDeveloper","MelbourneDeveloper","The full VSIX test gate fails on Windows because sidecar filenames are asserted without .exe and cleanup hits EPERM.","Platform-aware staging assertions and retrying cleanup after watcher disposal should make the gate reliable.","bug","2026-07-16","2026-07-16T13:22:14Z","2026-08-03T09:43:01Z","open","0","VS Code extension + test tooling","Engineering infrastructure","Windows VSIX test reliability","Editor/tooling (language-agnostic)","inferred","high","","" +"Nimblesite/SharpLsp","163","https://github.com/Nimblesite/SharpLsp/issues/163","Sidecar process-tree cleanup on Windows: no Job Object / tree kill, no parent-death watchdog","MelbourneDeveloper","MelbourneDeveloper","Windows kills only direct sidecar children, allowing dotnet grandchildren and build workers to survive host failure.","A Job Object and parent-death watchdog are needed to terminate the full process tree and release named pipes.","bug; cluster:sidecar-lifecycle; critical","2026-07-16","2026-07-16T22:42:08Z","2026-08-03T09:45:36Z","open","0","Rust LSP host + .NET sidecars","Core LSP & runtime","Windows process-tree cleanup","C#; F#","inferred","high","","" +"Nimblesite/SharpLsp","164","https://github.com/Nimblesite/SharpLsp/issues/164","Sidecar protocol hardening: response-id correlation + health-check lock race","MelbourneDeveloper","MelbourneDeveloper","The sidecar manager neither validates response IDs nor performs health checks without a transport lock race.","Mismatched responses should force reconnection and health monitoring should track genuinely stalled requests.","bug; cluster:sidecar-lifecycle","2026-07-16","2026-07-16T22:42:09Z","2026-08-03T09:45:38Z","open","0","Rust LSP host","Core LSP & runtime","IPC response correlation and health monitoring","C#; F#","inferred","high","","" +"Nimblesite/SharpLsp","165","https://github.com/Nimblesite/SharpLsp/issues/165","F# sidecar loads only the first .fsproj discovered — multi-project F# workspaces mostly unanalyzed","MelbourneDeveloper","MelbourneDeveloper","The F# sidecar loads only the first discovered .fsproj, leaving other projects on synthetic single-file analysis.","It should load every solution project and route each file through its owning FSharpProjectOptions.","bug; cluster:fsharp-project-model; critical","2026-07-16","2026-07-16T22:42:10Z","2026-08-03T09:45:38Z","open","0","F# sidecar (FCS)","Workspace & project system","F# multi-project loading","F# only","inferred","high","","" +"Nimblesite/SharpLsp","166","https://github.com/Nimblesite/SharpLsp/issues/166","MSBuildInstanceSelector: exact Roslyn version equality — SDK servicing skew falls back to a known-broken registration","MelbourneDeveloper","MelbourneDeveloper","Exact Roslyn assembly version matching rejects compatible serviced SDKs and can trigger the known broken fallback registration.","SDK selection should accept a compatible minimum version or fully isolate bundled Roslyn assemblies.","bug; critical","2026-07-16","2026-07-16T22:42:12Z","2026-08-03T09:43:40Z","open","0","C# sidecar (Roslyn)","Distribution & platform","MSBuild SDK selection","C# only","inferred","high","","" +"Nimblesite/SharpLsp","167","https://github.com/Nimblesite/SharpLsp/issues/167","Sidecar PATH resolution accepts .cmd/.bat/extensionless shims the spawn then cannot execute","MelbourneDeveloper","MelbourneDeveloper","Windows PATH discovery accepts command shims that CreateProcess cannot execute and then suppresses working fallback locations.","Resolution should launch a supported absolute executable or continue through the fallback chain after spawn failure.","bug; cluster:sidecar-startup","2026-07-16","2026-07-16T22:42:14Z","2026-08-03T09:45:39Z","open","0","Rust LSP host","Distribution & platform","Sidecar executable discovery","C#; F#","inferred","high","","" +"Nimblesite/SharpLsp","168","https://github.com/Nimblesite/SharpLsp/issues/168","Profiler: default output paths are CWD-relative and ProfilerConfig.output_directory is dead","MelbourneDeveloper","MelbourneDeveloper","Profiler outputs default to a host-CWD-relative directory that may be unwritable, while the configured output directory is ignored.","Defaults should use the workspace or user data directory and report clear write failures.","bug","2026-07-16","2026-07-16T22:42:15Z","2026-08-03T09:43:08Z","open","0","Rust LSP host","Profiling","Output path configuration","Editor/tooling (language-agnostic)","inferred","high","","" +"Nimblesite/SharpLsp","169","https://github.com/Nimblesite/SharpLsp/issues/169","workspace symbols: collect_source_files follows directory junctions with no cycle detection or depth cap","MelbourneDeveloper","MelbourneDeveloper","Workspace-symbol file discovery follows symlink or junction cycles without a visited set or depth limit.","Canonical-path cycle detection and a depth cap are needed to prevent host stack overflow.","bug; cluster:workspace-symbols; critical","2026-07-16","2026-07-16T22:42:16Z","2026-08-03T09:45:40Z","open","0","Rust LSP host","Navigation & symbols","Workspace-symbol file traversal","C#; F#","inferred","high","","" +"Nimblesite/SharpLsp","170","https://github.com/Nimblesite/SharpLsp/issues/170","NuGet: packages-root prefix strip lacks a separator boundary; parse.rs scrapes MSBuild XML line-wise","MelbourneDeveloper","MelbourneDeveloper","NuGet path matching can confuse sibling package roots, and project XML is parsed with fragile line scanning.","Boundary-aware paths and a real MSBuild or XML document model should replace both behaviors.","bug; cluster:nuget-pipeline","2026-07-16","2026-07-16T22:42:17Z","2026-08-03T09:45:41Z","open","0","Rust LSP host","NuGet & package management","Project parsing and package-root handling","C#; F#","inferred","high","","" +"Nimblesite/SharpLsp","171","https://github.com/Nimblesite/SharpLsp/issues/171","native_paths_equal is ASCII-only case-insensitive — NTFS is case-insensitive across Unicode","MelbourneDeveloper","MelbourneDeveloper","VFS path equality folds ASCII only, so equivalent non-ASCII NTFS paths can miss the live buffer.","Use operating-system case comparison or suitable Unicode folding and cover non-ASCII fixture paths.","bug","2026-07-16","2026-07-16T22:42:19Z","2026-08-03T09:43:12Z","open","0","Rust LSP host","Core LSP & runtime","VFS path normalization","C#; F#","inferred","high","","" +"Nimblesite/SharpLsp","172","https://github.com/Nimblesite/SharpLsp/issues/172","Sidecar shutdown acknowledgement is never sent — handler cancels the token the response write depends on","MelbourneDeveloper","MelbourneDeveloper","The sidecar cancels the token used to write its shutdown acknowledgement, so graceful shutdown always times out.","It should flush the acknowledgement before cancellation and hard termination.","bug; cluster:sidecar-lifecycle","2026-07-16","2026-07-16T22:42:21Z","2026-08-03T09:45:42Z","open","0","Shared .NET sidecar infrastructure","Core LSP & runtime","Sidecar shutdown","C#; F#","inferred","high","","" +"Nimblesite/SharpLsp","173","https://github.com/Nimblesite/SharpLsp/issues/173","Decompiled-source temp files are shared across sidecar processes and written without sharing — Windows write race","MelbourneDeveloper","MelbourneDeveloper","Concurrent C# sidecars share and exclusively overwrite the same decompiled-source temp paths.","Process-scoped paths or atomic write-and-rename semantics should prevent Windows races and partial reads.","bug; cluster:multi-host-isolation","2026-07-16","2026-07-16T22:42:22Z","2026-08-03T09:45:43Z","open","0","C# sidecar (Roslyn)","Navigation & symbols","Metadata navigation and decompilation","C# only","inferred","high","","" +"Nimblesite/SharpLsp","174","https://github.com/Nimblesite/SharpLsp/issues/174","C# parity: textDocument/signatureHelp is F#-only — Roslyn sidecar has no handler, host answers null","MelbourneDeveloper","MelbourneDeveloper","C# signature-help requests return null because only the F# sidecar implements the shared route.","The Roslyn sidecar should produce the shared SignatureHelpResult and the C# end-to-end test should require it.","bug","2026-07-16","2026-07-16T22:51:39Z","2026-08-03T09:43:17Z","open","0","C# sidecar (Roslyn)","Code intelligence","Signature help","C# only","inferred","high","","" +"Nimblesite/SharpLsp","176","https://github.com/Nimblesite/SharpLsp/issues/176","[Feature]: MacOS support","et1975","et1975","The VS Code extension is unavailable for macOS.","Publishing and packaging should support macOS as a language-agnostic editor platform.","bug; cluster:macos-release; critical","2026-07-17","2026-07-17T15:10:49Z","2026-08-03T09:45:44Z","open","0","VS Code extension","Distribution & platform","macOS packaging and support","Editor/tooling (language-agnostic)","issue_form","high","","" +"Nimblesite/SharpLsp","178","https://github.com/Nimblesite/SharpLsp/issues/178","[Bug]: Completion inserts duplicate method name after . (e.g. Console.WriteLineWriteLine)","sammychinedu2ky","sammychinedu2ky","Selecting a C# member completion after a dot appends the method name to itself.","Completion edits should replace the active identifier range instead of duplicating the selected item.","bug; critical","2026-07-18","2026-07-18T14:52:07Z","2026-08-03T09:42:20Z","open","0","Not specified in issue","Code intelligence","Completion edit ranges","C# only","inferred","high","","" +"Nimblesite/SharpLsp","180","https://github.com/Nimblesite/SharpLsp/issues/180","Test Explorer: MSTest tests not discovered (dotnet test --list-tests emits bare DisplayName, not FQN)","MelbourneDeveloper","MelbourneDeveloper","MSTest tests are omitted because text scraping expects dotted names while dotnet test emits bare display names.","Discovery should consume TestCase.FullyQualifiedName through VSTest or another structured result source.","bug","2026-07-18","2026-07-18T23:58:18Z","2026-08-03T09:43:19Z","open","0","VS Code extension","Testing & coverage","Test Explorer discovery","C#; F#","inferred","high","","" +"Nimblesite/SharpLsp","190","https://github.com/Nimblesite/SharpLsp/issues/190","Closure dedup is case-insensitive on case-sensitive filesystems, silently dropping includes","MelbourneDeveloper","MelbourneDeveloper","Case-insensitive closure deduplication silently drops distinct include files on case-sensitive filesystems.","Comparer behavior should match the actual volume and report every skipped already-visited include.","bug; cluster:csharp-single-file","2026-07-28","2026-07-28T22:14:25Z","2026-08-03T09:45:45Z","open","0","C# sidecar (Roslyn)","Scripting & file-based apps","C# file-based include closure","C# only","inferred","high","","" +"Nimblesite/SharpLsp","191","https://github.com/Nimblesite/SharpLsp/issues/191","Windows: Solution Explorer / workspaceSymbols serve stale data after rename (3 e2e failures)","MelbourneDeveloper","MelbourneDeveloper","Windows Solution Explorer and workspaceSymbols can show the previous symbol name after unsaved or rapid renames.","Instrument VFS/path resolution and refresh ordering; add generation-aware responses if that race is confirmed.","bug; cluster:workspace-symbols","2026-07-28","2026-07-28T22:44:56Z","2026-08-03T09:45:46Z","open","0","VS Code extension + Rust LSP host","Workspace & project system","Solution Explorer and workspace-symbol reactivity","Editor/tooling (language-agnostic)","inferred","medium","","" +"Nimblesite/SharpLsp","192","https://github.com/Nimblesite/SharpLsp/issues/192","Branch protection on main requires no functional test job — a fully red test run can merge","MelbourneDeveloper","MelbourneDeveloper","Branch protection on main requires no functional or coverage test jobs, so fully failing changes can merge.","Required checks should include Rust, .NET, VS Code, Windows, and coverage jobs with verified skip behavior.","bug; critical","2026-07-28","2026-07-28T22:45:34Z","2026-08-03T09:43:44Z","open","0","GitHub repository settings","Engineering infrastructure","CI branch protection","Editor/tooling (language-agnostic)","inferred","high","","" +"Nimblesite/SharpLsp","193","https://github.com/Nimblesite/SharpLsp/issues/193","[Bug]: C# Sidecar Initialization Fails Permanently When Opening a Projectless Directory","ashar-builds","ashar-builds","Opening a projectless directory leaves the C# sidecar permanently uninitialized when a C# file is created later.","Directory initialization should succeed provisionally and lazily open single-file mode on the first C# document.","bug; cluster:csharp-single-file; critical","2026-07-29","2026-07-29T05:00:18Z","2026-08-03T09:45:47Z","open","0","C# sidecar (Roslyn)","Workspace & project system","Projectless and single-file initialization","C# only","issue_form","high","","" +"Nimblesite/SharpLsp","195","https://github.com/Nimblesite/SharpLsp/issues/195","vscode-languageclient v10 breaks restartServer: client never returns to Running","MelbourneDeveloper","MelbourneDeveloper","vscode-languageclient 10 prevents restartServer from returning the client and status bar to Running.","Investigate vscode-languageclient v10 restart readiness and stop-start sequencing while retaining the completed logging API migration.","bug","2026-07-29","2026-07-29T23:35:01Z","2026-08-03T09:43:25Z","open","0","VS Code extension","Editor integrations & UI","Extension lifecycle and server restart","Editor/tooling (language-agnostic)","inferred","high","","" +"Nimblesite/SharpLsp","196","https://github.com/Nimblesite/SharpLsp/issues/196","Flaky e2e: profiler object-graph test fails when the baseline heap dump has no StringBuilder instances","MelbourneDeveloper","MelbourneDeveloper","The profiler object-graph test races heap capture against StringBuilder allocation and fails intermittently.","A readiness signal or bounded polling should synchronize the baseline dump without weakening the assertion.","bug","2026-07-30","2026-07-30T00:06:36Z","2026-08-03T09:43:27Z","open","0","Rust profiler e2e tests","Engineering infrastructure","Profiler e2e reliability","Editor/tooling (language-agnostic)","inferred","high","","" diff --git a/docs/plans/SIDECAR-LIFECYCLE-PLAN.md b/docs/plans/SIDECAR-LIFECYCLE-PLAN.md new file mode 100644 index 00000000..aea4e6a1 --- /dev/null +++ b/docs/plans/SIDECAR-LIFECYCLE-PLAN.md @@ -0,0 +1,621 @@ +# Sidecar Lifecycle Reliability Implementation Plan `[SIDECAR-PLAN]` + +**Status:** Active — implements the normative specification; checklist state records completion +**Normative specification:** [SIDECAR-LIFECYCLE-SPEC.md](../specs/SIDECAR-LIFECYCLE-SPEC.md) +**Primary cluster:** `cluster:sidecar-startup` + `cluster:sidecar-lifecycle` +**Issues:** [#150](https://github.com/Nimblesite/SharpLsp/issues/150), +[#151](https://github.com/Nimblesite/SharpLsp/issues/151), +[#152](https://github.com/Nimblesite/SharpLsp/issues/152), +[#153](https://github.com/Nimblesite/SharpLsp/issues/153), +[#154](https://github.com/Nimblesite/SharpLsp/issues/154), +[#163](https://github.com/Nimblesite/SharpLsp/issues/163), +[#164](https://github.com/Nimblesite/SharpLsp/issues/164), +[#167](https://github.com/Nimblesite/SharpLsp/issues/167), and +[#172](https://github.com/Nimblesite/SharpLsp/issues/172) + +## 1. Outcome `[SIDECAR-PLAN-OUTCOME]` + +Implement one per-language sidecar supervisor and one per-generation connection driver, then route +all startup, health, request, crash, recovery, and shutdown events through them. This is the +highest-value cluster because the nine issues share three mutable resources—child process, IPC +endpoint/transport, and retry state—and the current code lets multiple call paths manage those +resources independently. + +The finished system will: + +- start one isolated C# or F# sidecar generation even under concurrent semantic requests; +- select only a directly spawnable absolute executable and fall through bad non-explicit candidates; +- use a new unpredictable IPC endpoint per spawn and connect to the endpoint actually bound; +- make every pre-READY failure visible and subject to the same bounded backoff as runtime crashes; +- own all frames in one driver, validate response IDs, and dispatch interleaved notifications; +- distinguish a busy request from an idle or genuinely stalled sidecar without a lock race; +- acknowledge shutdown before cancellation and clean the whole process tree on every platform; +- rehydrate workspace, analyzer configuration, and current VFS documents after recovery; and +- prove all of the above with coarse, real-process end-to-end tests on Windows and Unix. + +## 2. Why this cluster is the highest-value fix `[SIDECAR-PLAN-CLUSTER]` + +The cluster affects every Roslyn- and FCS-backed feature. A startup storm, endpoint collision, +protocol desynchronization, or orphaned sidecar can disable completion, hover, navigation, +diagnostics, refactoring, and Solution Explorer together. The root problem is not any language +engine; it is fragmented lifecycle ownership in `src/sidecar/manager.rs` and +`SidecarHost.MessageLoopAsync`. + +The supervisor architecture directly resolves or supplies the necessary foundation for every issue: + +| Priority | Issue | Current user impact | Architectural owner after this plan | +|---|---|---|---| +| P0 / critical | #151 | Second editor host collides with or steals the first host's endpoint | Per-spawn endpoint lease + handshake validation | +| P0 / critical | #152 | Each semantic request can launch another doomed process | Single supervisor attempt + unified backoff state | +| P0 / critical | #153 | Broken transport can leave a 100%-CPU zombie | Terminal message-loop outcome + process watcher | +| P0 / critical | #163 | Host failure leaves sidecars, BuildHost, or MSBuild descendants | Parent watcher + Job Object/process group | +| P1 / bug | #150 | Listener failures appear as opaque “before READY” exits | Structured startup outcome + stderr/status/log capture | +| P1 / bug | #154 | Relocated Unix listener advertises the wrong path | Versioned READY record with effective endpoint | +| P1 / bug | #164 | Wrong response can reach a caller; health can kill healthy work | Single transport owner + exact ID/activity tracking | +| P1 / bug | #167 | Windows PATH shim blocks valid fallback artifacts | Typed candidate list + absolute direct spawn | +| P1 / bug | #172 | Graceful shutdown always falls into hard kill | Ack-after-flush shutdown state | + +All nine issues currently list `MelbourneDeveloper` as reporter, so severity and implementation +dependency—not a reporter-priority boost—determine their order inside this cluster. The broader issue +inventory can still prioritize reports from outside the owner/Abdul group when clusters have similar +impact. + +All nine issues are siblings under this implementation effort. #150 is the label's lead issue, but it +is an observability defect rather than a genuine parent of the other bugs; the plan must not fabricate +native parent/child relationships by making the siblings children of #150. + +## 3. Existing plans and present baseline `[SIDECAR-PLAN-BASELINE]` + +No existing document owns the complete cluster. Three plans contain adjacent requirements: + +- [SCRIPTING-FILEBASED-PLAN.md](SCRIPTING-FILEBASED-PLAN.md) requires one eager/lazy initialization + path and health only after `workspace/open`. +- [DIAGNOSTICS-PLAN.md](DIAGNOSTICS-PLAN.md) needs sidecar generation invalidation, retries, and + server-initiated notifications. +- [INFRASTRUCTURE-PLAN.md](INFRASTRUCTURE-PLAN.md) contains only sidecar startup performance (R2R), + not reliability or process ownership. + +This plan becomes the lifecycle source of truth and those plans consume its ready-generation and +notification APIs. + +The current tree already contains useful partial patches. They should be retained as behavior but +folded into the new ownership model: + +| Issue | Present implementation | Remaining gap | +|---|---|---| +| #150 | `StartupFailed`, one fatal stderr line, non-zero entry-point exit, and host exit-status/log hint | Boolean outcome is listener-specific; startup output is not centrally captured/classified; no full host process test | +| #151 | PID + process-local counter makes managers distinct | Token is reused across restarts; endpoint is not random; Unix listener still deletes a pre-existing path | +| #152 | `spawn_retry_after` throttles an immediate retry | State is split across locks; crash and spawn failure paths differ; concurrent waiters and stable-reset semantics are incomplete | +| #153 | I/O exceptions break and generic failures are capped | Run result does not consistently communicate fatal exit; no process/CPU/log-flood acceptance test | +| #154 | `BoundEndpoint` is printed in READY | Handshake is unversioned/unfenced and is not exercised through the real Rust host | +| #163 | No complete implementation | Direct-child kill, parent death before connect, Windows descendants, and Unix group cleanup remain | +| #164 | Request timeout drops a suspect transport | Response ID is unchecked; health performs check/drop/reacquire; notifications can be mistaken for responses | +| #167 | PATH finder accepts `.cmd`, `.bat`, extensionless entries and spawns a bare name | Candidate type/validation/fallback must be redesigned | +| #172 | Host waits briefly for a response | Sidecar cancels the response write token before returning the ack | + +## 4. Target design `[SIDECAR-PLAN-DESIGN]` + +### 4.1 Rust facade and supervisor `[SIDECAR-PLAN-DESIGN-SUPERVISOR]` + +Keep `SidecarManager` as the stable facade so feature call sites do not learn process details. Internally +it owns bounded `tokio::mpsc` senders to a long-lived supervisor task. Define typed models roughly as: + +- `SidecarKind` (`CSharp`, `FSharp`); +- `Generation(u64)`; +- `SupervisorCommand` (`EnsureReady`, `Request`, `UpdateSession`, `Restart`, `Status`, `Shutdown`); +- `SupervisorState` matching [SIDECAR-STATE-MODEL]; +- `LaunchCandidate` with absolute program, arguments, and source; +- `EndpointLease` with requested/effective endpoint and ownership; +- `FailureKind` matching [SIDECAR-RECOVERY-FAILURES]; +- `SessionSnapshot` for target/configuration/open documents; and +- `SidecarUnavailable` with language, category, and retry time. + +The supervisor uses `tokio::select!` across commands, generation-scoped child exit, connection events, +startup timeout, stable-ready timer, and shutdown. It performs no blocking OS wait on the Tokio worker. +It uses `Result`/`Option`, structured errors, and generation checks; there are no production +`unwrap`/`expect`/`panic` paths. + +### 4.2 Connection driver `[SIDECAR-PLAN-DESIGN-CONNECTION]` + +Move `FramedTransport` out of the manager mutex and into a task that is its sole owner. It maintains +one `ActiveRequest { id, method, written_at, deadline, completion }`, a bounded pending queue, and an +idle-health deadline. It reads continuously, dispatches `id=null/method!=null` notifications, and +requires an exact ID for the active response. + +This deliberately keeps semantic dispatch sequential. It supplies correct correlation and +notifications now without introducing concurrent Roslyn/FCS mutation ordering. If multiplexed +handlers are added later, the driver can replace `ActiveRequest` with a pending map without changing +the supervisor contract. + +### 4.3 Managed host lifecycle `[SIDECAR-PLAN-DESIGN-DOTNET]` + +Replace the `StartupFailed` boolean with a typed `SidecarRunResult`/exit outcome that distinguishes +normal peer close, acknowledged shutdown, startup fatal, transport fatal, and parent death. Parse a +shared `SidecarStartupOptions` in both C# and F# entry points. Initialize `ParentProcessWatchdog` and +`ProcessContainment` before listener creation and before any engine can spawn descendants. + +The shutdown request records “shutdown requested” but does not cancel. `ProcessOneMessageAsync` writes +and flushes the response, then cancels the loop. Persistent stream failures terminate the run outcome +instead of re-entering the broken read. + +### 4.4 Session recovery `[SIDECAR-PLAN-DESIGN-RECOVERY]` + +Extract the duplicated eager startup, lazy project-less startup, second-language startup, and +`sharplsp/loadSolution` code in `src/main.rs` into one session-update path. The supervisor stores the +desired target and analyzer configuration; a VFS snapshot provider supplies current open documents. +On every generation it performs the ordered bootstrap in [SIDECAR-RECOVERY-REHYDRATE] before making +the request queue available. + +The connection driver exposes sidecar notifications to the LSP orchestration layer. This is the +required transport foundation for `diagnostics/refresh` and `workspace/projectInitializationComplete` +in the diagnostics plan. + +## 5. File-level change map `[SIDECAR-PLAN-FILES]` + +| Path | Planned responsibility/change | +|---|---| +| `src/sidecar/manager.rs` | Thin facade, public request/session/status/shutdown API; remove child/transport/backoff lock ownership | +| `src/sidecar/supervisor.rs` | New actor, state transitions, generation fencing, launch/bootstrap/backoff/shutdown orchestration | +| `src/sidecar/connection.rs` | New sole transport owner, request queue, ID validation, notifications, activity/deadlines | +| `src/sidecar/launch.rs` | New typed resolution candidates, spawn validation, versioned READY parsing, capped output collection, endpoint leases | +| `src/sidecar/process_tree.rs` | New safe platform abstraction for direct child/process group and hard termination; no Rust unsafe code | +| `src/sidecar/protocol.rs` | Envelope shape validators, READY DTO, notification classification, typed protocol faults | +| `src/sidecar/transport.rs` | Keep bounded framing; distinguish clean EOF from truncated frame; split/ownership support if required by driver | +| `src/sidecar/mod.rs` | Export only facade/public status types; keep internal modules private | +| `src/main.rs` | Replace eager/lazy health tasks with session updates; provide target/config/VFS replay and notification sink | +| `src/diagnostics.rs` / pull diagnostics path | Invalidate on generation change and consume sidecar notifications without owning lifecycle | +| `sidecars/SharpLsp.Sidecar.Common/SidecarStartupOptions.cs` | Shared strict argument parser for endpoint, parent PID, generation, protocol | +| `sidecars/SharpLsp.Sidecar.Common/SidecarRunResult.cs` | Shared typed terminal outcome and failure category | +| `sidecars/SharpLsp.Sidecar.Common/ParentProcessWatchdog.cs` | Pre-READY hard-parent-death detection | +| `sidecars/SharpLsp.Sidecar.Common/ProcessContainment.cs` | Windows safe Job Object lifetime and Unix group termination support | +| `sidecars/SharpLsp.Sidecar.Common/SidecarHost.cs` | Versioned READY, terminal loop faults, ack-before-cancel, typed run outcome | +| `sidecars/SharpLsp.Sidecar.Common/Ipc/IpcConnection.cs` | No blind socket deletion; owned path cleanup; effective endpoint; current-user access | +| C# and F# `Program` entry points | Use shared options/outcome; emit correct non-zero status once; remove duplicated lifecycle decisions | +| `tests/fixtures/SidecarLifecycleFixture/` | Real separately spawned shared-host fixture for protocol faults, delayed handlers, and child-process containment | +| `tests/e2e_modules/sidecar_lifecycle.rs` | Full host/process/IPC recovery scenarios and issue traceability | +| `sidecars/SharpLsp.Sidecar.Common.Tests/SidecarHostEndToEndTests.cs` | Keep only coarse real-IPC host lifecycle coverage; add ack and process-exit assertions | +| `.github/workflows/ci-rust.yml` / `ci-dotnet.yml` | Run platform-relevant real-process lifecycle cases | +| `.github/workflows/ci-vsix-windows.yml` | Gate the lifecycle chunk on concurrent hosts, restart, and Windows tree cleanup | + +File names may be adjusted to match an equivalent existing abstraction discovered during +implementation, but responsibilities MUST remain single-owner and the final tree MUST not retain a +second restart/health loop. + +## 6. Implementation sequence `[SIDECAR-PLAN-SEQUENCE]` + +### 6.1 Phase 0 — characterize the contract `[SIDECAR-PLAN-PHASE-0]` + +First add real-process failing scenarios for the nine issues and record current attempt counts, PIDs, +exit statuses, endpoint paths, and shutdown behavior. Build the lifecycle fixture on production +`SidecarHost` and OS IPC; it is an executable artifact, not an in-memory mock. Add deterministic +commands to induce wrong response IDs, delayed responses, malformed frames, and a real child helper. + +Audit available safe process/job primitives before adding dependencies. Any reused crate must work +with `unsafe_code = "deny"`; if Windows native calls are needed, keep safe handles and P/Invoke in the +shared managed process-containment implementation rather than adding Rust unsafe blocks. + +### 6.2 Phase 1 — supervisor skeleton and generation state `[SIDECAR-PLAN-PHASE-1]` + +Introduce the actor and facade behind the current `SidecarManager` API. Move coalesced ensure-ready, +child exit monitoring, one retry timestamp, failure classification, backoff growth/reset, and +idempotent shutdown into the actor before changing the wire protocol. Preserve observable request +behavior while deleting the independent health-loop and spawn-failure state only after call sites use +the actor. + +This phase is the root fix for #152 and prevents later endpoint/connection work from creating another +set of shared locks. + +### 6.3 Phase 2 — resolution, endpoint, and startup handshake `[SIDECAR-PLAN-PHASE-2]` + +Replace `sidecar_launch`/`find_on_path` with the candidate model. Resolve absolute apphosts or explicit +`dotnet ` launches, remove `dotnet run`, and iterate only allowed fallback failures. Allocate a +new CSPRNG endpoint lease per generation. Stop deleting pre-existing Unix sockets and clean only +owned paths. + +Add shared sidecar option parsing and emit the versioned READY JSON with generation, PID, and effective +endpoint. The supervisor races READY, child exit, EOF, and timeout; captures bounded output; validates +the record; performs transient connect retries; and reports one classified error/backoff event. + +This phase completes #150, #151, #154, and #167 and folds their current partial patches into the actor. + +### 6.4 Phase 3 — connection driver, correlation, and health `[SIDECAR-PLAN-PHASE-3]` + +Move the transport into its driver task. Add strict envelope classification, exact response-ID +validation, notification dispatch, bounded command capacity, pre/post-write cancellation behavior, +and transport poisoning on timeout/correlation/framing faults. Replace the external health monitor +with the driver's idle/activity timer. Keep existing 600s/120s request budgets and the 2s ping budget. + +This phase completes #164 and supplies notification support needed by diagnostics recovery. + +### 6.5 Phase 4 — managed loop and graceful shutdown `[SIDECAR-PLAN-PHASE-4]` + +Return typed run outcomes from `SidecarHost`. Treat permanent stream errors as terminal, cap recoverable +decode failures, and make both entry points map terminal outcomes consistently. Change shutdown to +flush the correlated response before cancelling and let the supervisor wait for ack/clean exit before +hard termination. + +This phase completes #153 and #172. + +### 6.6 Phase 5 — process-tree containment `[SIDECAR-PLAN-PHASE-5]` + +Make production and development launches direct. Start the parent watcher and containment before +READY. On Windows, create/retain a kill-on-close Job Object and assign the sidecar before engine child +processes can start. On Unix, create a dedicated process group and target only that generation's group +for planned hard termination. Verify host death while the sidecar is still waiting for a connection, +while idle, and while a real child helper exists. + +This phase completes #163. It must land for C# and F# together. + +### 6.7 Phase 6 — bootstrap and recovery integration `[SIDECAR-PLAN-PHASE-6]` + +Unify `start_sidecar`, lazy initialization, second-language initialization, and load-solution updates +around `SessionSnapshot`. On a new generation, open the target, configure analyzers, replay latest VFS +documents, attach notification consumers, then mark ready. Emit generation-change invalidation so +diagnostics and semantic caches retry/refresh safely. + +This phase turns process restart into actual feature recovery instead of merely reconnecting an empty +sidecar. + +### 6.8 Phase 7 — cross-platform gates and rollout `[SIDECAR-PLAN-PHASE-7]` + +Run focused lifecycle e2e tests during development, then the complete Rust, .NET, and Windows VSIX +gates. Capture structured logs for one forced failure/recovery cycle and prove there is one spawn per +backoff window, no stale endpoint, no child process, and no hard kill on normal shutdown. Close each +issue only with its specific platform evidence; do not close the cluster solely because the refactor +compiled. + +## 7. Test and verification strategy `[SIDECAR-PLAN-TESTING]` + +### 7.1 Test artifact policy `[SIDECAR-PLAN-TESTING-ARTIFACTS]` + +The test fixture is a real executable using the same `SidecarHost`, `IpcListener`, framing, argument +parser, parent watcher, containment, and shutdown code as production. It may expose handlers whose +normal behavior is “delay”, “respond with a selected protocol fault”, or “spawn a child helper”; the +production supervisor contains no test-only branch. Engine recovery tests use the actual published +C# and F# sidecars and real workspaces. + +Do not add in-memory transport tests as acceptance evidence. Existing narrow tests may remain, but +issue closure requires the real-process scenarios in [SIDECAR-TESTING]. + +### 7.2 Platform matrix `[SIDECAR-PLAN-TESTING-MATRIX]` + +| Scenario | Windows | Linux | macOS | +|---|---:|---:|---:| +| Listener fatal/status/log path | Required | Required | Required | +| Two hosts, one workspace | Required named pipe | Required Unix socket | Required Unix socket | +| Spawn backoff and recovery | Required | Required | Required | +| Long/effective Unix endpoint | N/A | Required | Required | +| PATH shim fallback | Required | N/A | N/A | +| Wrong ID, notification interleave, health activity | Required | Required | Required | +| Ack-before-exit shutdown | Required | Required | Required | +| Job Object descendants | Required | N/A | N/A | +| Process-group/parent-death cleanup | N/A | Required | Required | +| C# and F# VFS rehydration | Required | Required | At least CI smoke if runner budget is constrained | +| Full packaged editor lifecycle | Required VSIX | Existing VSIX gate | Existing VSIX gate | + +Use event-driven readiness, process exit, log record, and semantic-response assertions. Polling may be +bounded where an OS API has no awaitable interface, but a fixed sleep is never the only success +condition. + +### 7.3 Validation commands `[SIDECAR-PLAN-TESTING-COMMANDS]` + +During implementation, use the narrowest relevant real-process target first, followed by: + +```text +make _test-dotnet +make _test-rust +make _test-vsix-win # Windows lifecycle/editor surface +make lint +``` + +The final verification also runs the release-built sidecars' `--version` and startup contracts and +checks that both C# and F# artifacts use the shared lifecycle implementation. + +## 8. Rollout and failure handling `[SIDECAR-PLAN-ROLLOUT]` + +Ship the host and sidecars together behind the protocol version in READY. During one release the host +may accept the legacy handshake only after exact binary version verification; remove the compatibility +parser after all supported bundles emit protocol 1. No user-facing feature flag should select between +old and new supervisors because dual lifecycle implementations would make failures untriageable. + +Before release, exercise one forced failure of every category and inspect the editor output channel: +one concise event and log link are allowed, while repeated request chatter, raw stacks, and ANSI are +not. Measure startup and request latency against [SIDECAR-PERFORMANCE]. If a regression occurs, fix the +single supervisor path; do not restore the independent health or spawn loops. + +## 9. Risks and mitigations `[SIDECAR-PLAN-RISKS]` + +| Risk | Mitigation | +|---|---| +| Actor refactor changes many call sites at once | Keep the facade signature, land state/driver behind it, then migrate session updates | +| Bootstrap deadlocks by calling the public facade from its own actor | Give the supervisor a private internal connection request path; never re-enter its public command channel | +| Slow `workspace/open` is mistaken for death | Driver owns the 600s deadline; health is idle-only | +| Old response/exit mutates a replacement | Fence every event and completion with generation | +| Random endpoint cleanup deletes another host's resource | Lease ownership + no pre-bind unlink + private runtime directory | +| Job Object conflicts with an enclosing job | Establish before READY, use safe handles, fail visibly, and cover the packaged VSIX environment in CI | +| Development `dotnet run` removal hurts local workflow | Build sidecars through existing make target and launch the resulting apphost/assembly directly | +| Recovery replays stale document text | Snapshot latest version/text from authoritative VFS immediately before bootstrap; generation-gate completion | +| Shutdown races parent watcher or health timer | `Stopping` disables health/admission; normal ack path is the sole clean-exit owner | +| New driver is mistaken for permission to run handlers concurrently | Keep one active request and document ordering; concurrency is a separate future design | + +## 10. Definition of done `[SIDECAR-PLAN-DONE]` + +The effort is complete only when all nine issue scenarios pass on their required platforms, the +published C# and F# sidecars share the same lifecycle code, restart restores usable semantic state, +normal shutdown is acknowledged without hard kill, hard host death leaves no sidecar or descendant, +and no old spawn/health/retry owner remains. Documentation IDs, issue links, implementation comments, +and test names must provide a direct trace from each issue to its requirement and evidence. + +## 11. Detailed implementation checklist `[SIDECAR-PLAN-CHECKLIST]` + +This is the execution checklist. Keep it at the bottom of this document; update boxes only when the +item and its required evidence are complete. + +### 11.1 Contract and baseline `[SIDECAR-PLAN-CHECKLIST-CONTRACT]` + +- [x] Confirm all nine issues are still open and capture their current labels, descriptions, and + platform scope in the implementation PR/working notes. +- [x] Reconcile [SIDECAR-LIFECYCLE-SPEC.md](../specs/SIDECAR-LIFECYCLE-SPEC.md) with any issue-body + updates made after 2026-08-03; change the spec before code when behavior differs. +- [x] Verify every heading in the lifecycle spec has one unique hierarchical uppercase ID. +- [x] Confirm [SIDECAR-REQUEST-TIMEOUT], [SCRIPT-ROUTE-HEALTH], [DIST-CLEAN-OUTPUT], and + [DIST-CI-WIN-TRANSPORT] do not contradict the detailed lifecycle contract. +- [x] Record the current behavior for #150–#154 so the useful partial fixes are preserved during the + refactor rather than accidentally reverted. +- [ ] Record current spawn count, retry timing, child PID, endpoint, exit status, and shutdown path for + one healthy C# and one healthy F# session. +- [ ] Record the same evidence for a forced pre-READY failure, runtime transport failure, and host + hard death on Windows and one Unix platform. +- [ ] Audit existing crates and shared libraries for a maintained safe process/job abstraction that + satisfies `unsafe_code = "deny"`; document the reuse or rejection decision. +- [x] Confirm the implementation creates no new native parent/child GitHub relationship among + #150–#172 unless a genuine umbrella tracking issue is created; keep the defects as siblings. + +### 11.2 Real-process test harness first `[SIDECAR-PLAN-CHECKLIST-HARNESS]` + +- [ ] Add `tests/fixtures/SidecarLifecycleFixture` as a separately built executable referencing the + production shared sidecar host and IPC assemblies. +- [ ] Give the fixture a normal echo/ping handler for healthy request/response verification. +- [ ] Give the fixture a bounded delayed handler to exercise busy-within-budget and deadline-expired + behavior without mocked clocks or streams. +- [ ] Give the fixture a protocol-fault mode that can emit a wrong response ID and a notification + before a valid response over the real connection. +- [ ] Give the fixture a malformed-frame/connection-close mode that exercises terminal .NET loop + errors in a separate process. +- [ ] Give the fixture a handler that starts a real long-lived child helper and reports its PID for + containment assertions. +- [ ] Make all fixture modes available through normal command arguments or handlers; add no + `cfg(test)`/test-only behavior to the production supervisor. +- [ ] Add bounded helpers that await READY, process exit, retry timestamp, child disappearance, and + endpoint rebinding through observable events rather than fixed sleeps. +- [ ] Add issue numbers and spec IDs to scenario names/comments so test failure output is traceable. + +### 11.3 Supervisor state and typed errors `[SIDECAR-PLAN-CHECKLIST-SUPERVISOR]` + +- [ ] Define `SidecarKind`, `Generation`, `FailureKind`, `SidecarUnavailable`, and `SessionSnapshot` + with structured fields and no string parsing for control flow. +- [ ] Define every state in [SIDECAR-STATE-MODEL] and make impossible resource combinations + unrepresentable where practical. +- [ ] Create a bounded supervisor command channel and per-command completion channels. +- [ ] Keep `SidecarManager` as a cloneable facade; remove direct public access to child, transport, + endpoint, backoff, and health-loop internals. +- [ ] Implement coalesced `EnsureReady` so concurrent callers wait on one generation. +- [ ] Allocate a monotonic generation before every spawn attempt and attach it to every async event. +- [ ] Ignore stale child-exit, driver, timeout, bootstrap, and shutdown events after logging their + generation mismatch at debug level. +- [ ] Race supervisor commands, child exit, driver events, startup deadlines, stable-ready reset, and + shutdown using non-blocking Tokio primitives. +- [ ] Implement one failure transition function that records category/context, cleans the generation, + advances backoff, and completes affected callers. +- [ ] Implement the 1/2/4/8/16/30-second base sequence with ±20% jitter and monotonic retry timestamp. +- [ ] Return `SidecarUnavailable` immediately during backoff; prove requests do not sleep or spawn. +- [ ] Reset backoff only after 60 continuous ready seconds or a new full LSP session. +- [ ] Make restart and shutdown idempotent and ensure C# state cannot mutate F# state. +- [ ] Delete `spawn_retry_after`, the separate crash sleep, and all other superseded retry owners only + after the actor tests pass. +- [ ] Remove production `unwrap`, `expect`, `panic`, and unstructured expected-error paths introduced + or touched by the refactor. + +### 11.4 Resolution and direct launch `[SIDECAR-PLAN-CHECKLIST-RESOLUTION]` + +- [ ] Define `LaunchCandidate` with source, absolute program, arguments, explicit/non-explicit policy, + and redacted diagnostic rendering. +- [ ] Re-resolve candidates for each generation rather than caching one command at manager creation. +- [ ] Treat an explicit sidecar environment override as authoritative and surface a clear hard error + when it is missing, the wrong file type, or unspawnable. +- [ ] Resolve Shipwright/bundled and PATH candidates to the exact absolute path passed to spawn. +- [ ] On Windows accept only direct `.exe` candidates or explicit `dotnet.exe ` pairs. +- [ ] On Windows reject `.cmd`, `.bat`, PowerShell, and extensionless shims without invoking a shell. +- [ ] On Unix require a regular executable file for direct candidates. +- [ ] Continue through non-explicit candidates only for absence/invalid-format/mechanical spawn + failures; stop and classify application/listener/handshake failures. +- [ ] Remove the `dotnet run` fallback and launch prebuilt development output directly. +- [ ] Ensure the development/build instructions produce the required apphost or DLL before tests. +- [ ] Add the real Windows PATH test with a bad shim before a valid candidate and assert the valid + absolute executable starts. + +### 11.5 Endpoint leases and READY `[SIDECAR-PLAN-CHECKLIST-STARTUP]` + +- [ ] Generate at least 64 unpredictable bits from the OS CSPRNG for every spawn attempt. +- [ ] Include language, host PID, generation, and nonce in a length-bounded platform endpoint. +- [ ] Create/use a validated owner-only Unix runtime directory and keep socket mode `0600`. +- [ ] Keep `PipeOptions.CurrentUserOnly` and a single named-pipe server instance on Windows. +- [ ] Remove blind pre-bind `File.Delete` of Unix socket paths. +- [ ] Track listener ownership and delete only the Unix path actually created by that listener. +- [ ] Allocate a new endpoint after every failed generation; never reuse an endpoint because a + `SidecarManager` instance survived. +- [ ] Add shared parsing for `--endpoint`, `--parent-pid`, `--generation`, and `--protocol` in C# and + F# entry points. +- [ ] Initialize logging, containment, parent watcher, and listener before READY. +- [ ] Emit and flush one versioned READY JSON record containing protocol, generation, actual PID, and + effective bound endpoint. +- [ ] Validate READY schema, protocol, generation, PID, platform endpoint shape, and lease attribution + in the host. +- [ ] Race READY against exit, stdout EOF, and the 30-second deadline; terminate and reap every losing + child. +- [ ] Retry only transient post-READY connect errors for at most 2 seconds with bounded delay. +- [ ] Capture/drain stdout and stderr concurrently, cap retained tails at 16KiB each, and prevent pipe + backpressure from blocking startup. +- [ ] Preserve one sanitized pre-READY `FATAL` stderr line, structured file details, non-zero exit, + host exit status, and log-directory hint for #150. +- [ ] Exercise listener failure end to end with both published sidecar entry points. +- [ ] Start two hosts on one real workspace on Windows and Unix; assert distinct endpoint/PID and + successful semantic requests from both. +- [ ] Exercise an overlong Unix requested path through READY and connect to the advertised effective + path without host-side shortening. + +### 11.6 Connection driver and protocol `[SIDECAR-PLAN-CHECKLIST-CONNECTION]` + +- [ ] Move `FramedTransport` into a connection driver task as its sole reader and writer. +- [ ] Add a bounded request queue and typed saturation result. +- [ ] Allocate non-zero request IDs monotonically within each generation. +- [ ] Validate request, response, and notification envelope shapes before dispatch. +- [ ] Require every response—including ping/bootstrap/shutdown—to match the active request ID. +- [ ] On missing, duplicate, unknown, or wrong ID, fail the request, stop writes, poison transport, and + report one protocol failure to the supervisor. +- [ ] Dispatch `id=null/method!=null` sidecar notifications while a request is active and continue + waiting for the correct response. +- [ ] Preserve one host-to-sidecar active request at a time and document arrival-order semantics. +- [ ] Start response deadlines when the request frame is written, not while queued. +- [ ] Cancel queued/unwritten requests without poisoning the connection. +- [ ] After post-write cancellation, send cancellation when supported and drain/discard the matching + response before admitting the next request. +- [ ] Poison and restart when a written request cannot be drained within its 120s/600s budget. +- [ ] Distinguish clean EOF between frames from truncated length/payload EOF and classify the latter as + a protocol failure. +- [ ] Keep the 64MiB frame check before allocation in Rust and .NET. +- [ ] Add a real wrong-ID scenario proving the stale response reaches neither current nor next caller. +- [ ] Add a real notification-before-response scenario proving both are delivered correctly. + +### 11.7 Activity-aware health `[SIDECAR-PLAN-CHECKLIST-HEALTH]` + +- [ ] Put the idle timer and ping request inside the connection driver; create no second transport + caller or monitor task. +- [ ] Suppress ping outside `Ready` and while an ordinary request is active within its budget. +- [ ] Send a ping only after 5 idle seconds and require its exact response within 2 seconds. +- [ ] Treat request deadline, ping deadline, EOF, process exit, and protocol fault as distinct failure + categories routed through the supervisor. +- [ ] Remove the `try_lock`/drop/reacquire health sequence and `start_health_monitor` call sites. +- [ ] Remove eager/lazy manual monitor ordering once the supervisor owns health internally. +- [ ] Prove a delayed request inside its budget survives multiple nominal ping intervals. +- [ ] Prove an idle unresponsive sidecar is terminated/backed off and later recovers. +- [ ] Prove a request beyond its budget poisons the transport and no late response is reused. + +### 11.8 Managed message loop and shutdown `[SIDECAR-PLAN-CHECKLIST-DOTNET]` + +- [ ] Replace `StartupFailed` with a typed run result covering normal close, acknowledged shutdown, + startup fatal, transport fatal, and parent death. +- [ ] Make C# and F# entry points map the same run result to the same zero/non-zero semantics. +- [ ] Treat `IOException`, `ObjectDisposedException`, truncated frame, and response write failure as + terminal message-loop outcomes. +- [ ] Bound recoverable decode/dispatch failures and reset the counter only after a complete valid + message/response cycle. +- [ ] Emit one structured terminal error and exit; do not retry the same permanently broken stream. +- [ ] Change the shutdown handler to create the `ok` payload without cancelling `_shutdownCts`. +- [ ] Write and flush the correlated shutdown response with a bounded write token. +- [ ] Cancel dispatch and dispose listener/transport only after the response flush succeeds. +- [ ] In the supervisor, stop admission, cancel unwritten commands, send shutdown, and wait 1 second + for the exact acknowledgement. +- [ ] After acknowledgement, wait within the remaining 5-second graceful budget for zero process exit. +- [ ] On ack/exit timeout, hard-terminate only the current generation's contained process tree and reap + the direct child. +- [ ] Add a real-process test that observes the matching ack before process exit and asserts the hard + kill path was not used. +- [ ] Add a persistent broken-stream/decode-storm test that exits within a bound and produces bounded + logs rather than a hot loop. + +### 11.9 Parent death and process-tree cleanup `[SIDECAR-PLAN-CHECKLIST-PROCESS]` + +- [ ] Guarantee every production/development launch is direct so Rust child PID equals READY PID. +- [ ] Start parent-death detection before listener bind and fail pre-READY when the parent cannot be + validated. +- [ ] On Windows open a waitable handle to the exact parent process object and detect death within one + second even while waiting in accept. +- [ ] On Unix verify direct parent identity and detect reparenting/disappearance within one second. +- [ ] Implement Windows Job Object creation with `JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE` through safe + managed handles and assign the sidecar before engine descendants can start. +- [ ] Retain the Job Object handle for the full sidecar lifetime and make setup failure visible before + READY. +- [ ] Launch Unix sidecars as leaders of dedicated process groups without placing the Rust host in the + group. +- [ ] On planned Unix hard termination, signal only the current generation's group and escalate within + a bound. +- [ ] On hard parent death, terminate descendants, dispose the listener, and exit without an IPC + shutdown request. +- [ ] Never enumerate/kill processes by executable name and never target VS Code or an unverified PID. +- [ ] Add a Windows test: fixture spawns child, host dies before IPC connect, sidecar and child vanish, + and the pipe can be rebound. +- [ ] Add a Windows test: wedged sidecar is hard-terminated and its child/BuildHost does not survive. +- [ ] Add Linux/macOS equivalents asserting no process-group member or owned socket survives. +- [ ] Assert every child is awaited/reaped and every containment/listener handle is disposed on all + success and failure paths. + +### 11.10 Session bootstrap and feature recovery `[SIDECAR-PLAN-CHECKLIST-RECOVERY]` + +- [ ] Define the desired per-language session snapshot: workspace/solution/root-file target, analyzer + configuration, notification subscriptions, and current open-document snapshots. +- [ ] Source open-document URI, language, version, and full text from the authoritative VFS. +- [ ] Replace eager `start_sidecar` orchestration with one supervisor session-update/bootstrap path. +- [ ] Replace lazy project-less startup with that same path and start only the owning language. +- [ ] Start the second language on its first supported document without duplicating bootstrap/health + tasks. +- [ ] Route `sharplsp/loadSolution` through a desired-target update rather than an independent + `workspace/open` task. +- [ ] Bootstrap each generation in order: `workspace/open`, analyzer/configuration, stable-URI VFS + replay, then notification activation. +- [ ] Keep the generation out of `Ready` until every required bootstrap step succeeds. +- [ ] Route bootstrap failure through normal cleanup/backoff; do not expose a half-initialized sidecar. +- [ ] Emit generation-change invalidation to semantic/diagnostic caches before new results publish. +- [ ] Connect `diagnostics/refresh` and project-initialization notifications through the driver without + giving diagnostics ownership of the transport. +- [ ] Rate-limit user-facing unavailable/recovery notices to one per failure episode. +- [ ] Keep Rust syntax features usable during sidecar backoff and return typed errors for semantic + requests. +- [ ] Add a C# recovery e2e: edit an open document, kill the sidecar, await recovery, and assert hover + or diagnostics reflects the latest VFS text. +- [ ] Add the equivalent F# recovery e2e with a real FCS semantic request. +- [ ] Add a mixed-language recovery e2e proving one language restart does not reset or kill the other. + +### 11.11 Documentation, CI, and issue closure `[SIDECAR-PLAN-CHECKLIST-RELEASE]` + +- [x] Update `SHARPLSP-SPEC.md` lifecycle/IPC summaries to link the detailed spec and accurately state + one active request plus interleaved notifications. +- [x] Update [DIST-CLEAN-OUTPUT] to sanction one fatal pre-READY diagnostic and no other sidecar stderr + chatter. +- [x] Update [DIST-CI-WIN-TRANSPORT] to require unpredictable per-spawn endpoints instead of calling + endpoint names deterministic. +- [x] Update [SCRIPT-ROUTE-HEALTH] to reference activity-aware supervisor health and prohibit a + duplicate caller-started monitor. +- [ ] Add implementation comments citing the most specific spec IDs at state, timeout, handshake, + correlation, containment, and shutdown boundaries. +- [ ] Run the focused shared-host real-process lifecycle suite on Windows, Linux, and macOS. +- [ ] Run the Rust e2e lifecycle module with published C# and F# sidecars. +- [ ] Run `make _test-dotnet` and resolve every failure without weakening assertions. +- [ ] Run `make _test-rust` and resolve every failure without filtering lifecycle cases. +- [ ] Run the complete Windows VSIX lifecycle chunk via `make _test-vsix-win`. +- [ ] Run `make lint`; keep Rust `unsafe_code = "deny"`, missing-doc, and structured-error rules green. +- [ ] Inspect one healthy and one forced-failure editor output: no ANSI, raw stack flood, payload text, + or repeated per-request toast. +- [ ] Verify startup/request/shutdown/backoff timings against [SIDECAR-PERFORMANCE]. +- [ ] Verify concurrent hosts have distinct endpoints and that restart uses a new endpoint. +- [ ] Verify normal shutdown received an ack and left no sidecar/descendant/socket/pipe. +- [ ] Verify hard host death left no sidecar/descendant/socket/pipe on every supported platform. +- [ ] Verify both C# and F# recovered latest open-document state after a forced generation change. +- [ ] Search the tree for old independent spawn, health, crash sleep, transport mutex, and shutdown + owners; remove or document every remaining occurrence. +- [ ] Attach platform-specific passing evidence to #150 and close only when fatal/status/log behavior + is proven. +- [ ] Attach concurrent-host Windows+Unix evidence to #151 and close only when no collision/steal is + possible. +- [ ] Attach measured attempt/backoff evidence to #152 and close only when requests cannot create a + respawn storm. +- [ ] Attach bounded-exit/log evidence to #153 and close only when the hot loop is impossible. +- [ ] Attach overlong-Unix-path evidence to #154 and close only when the effective endpoint connects. +- [ ] Attach Windows parent-death and descendant cleanup evidence to #163 and close only when no + process-tree member survives. +- [ ] Attach wrong-ID and activity-aware health evidence to #164 and close only when both halves pass. +- [ ] Attach real Windows PATH fallback evidence to #167 and close only when shims cannot block the + valid candidate. +- [ ] Attach ack-before-exit evidence to #172 and close only when graceful shutdown avoids hard kill. +- [ ] Re-export `docs/bugs/open-issues.csv` after issue states/relationships change so the inventory + remains synchronized with GitHub. diff --git a/docs/specs/BINARY-DEPLOYMENT.md b/docs/specs/BINARY-DEPLOYMENT.md index 06ac6d83..6af796d2 100644 --- a/docs/specs/BINARY-DEPLOYMENT.md +++ b/docs/specs/BINARY-DEPLOYMENT.md @@ -1,36 +1,18 @@ -# Distribute SharpLsp via Homebrew, Scoop, and dotnet tool +# Distribute SharpLsp via Homebrew, Scoop, and dotnet tool `[BINARY-DEPLOYMENT]` -## Context +## Context `[BINARY-CONTEXT]` -SharpLsp currently ships a single tagged GitHub release containing one monolithic -archive per platform with `bin/sharplsp` + `sidecar-csharp/` + `sidecar-fsharp/` -folders. The VS Code extension downloads that archive and extracts it into -`~/.local/` (see [install.ts:308-384](editors/vscode/src/install.ts#L308-L384)). +SharpLsp currently ships a single tagged GitHub release containing one monolithic archive per platform with `bin/sharplsp` + `sidecar-csharp/` + `sidecar-fsharp/` folders. The VS Code extension downloads that archive and extracts it into `~/.local/` (see [`install.ts`](../../editors/vscode/src/install.ts)). -The user wants three distinct distribution channels: +The distribution contract defines three channels: -1. **Rust `sharplsp` binary** — Homebrew (macOS/Linux) and Scoop (Windows), - driven by GitHub release assets. -2. **C# and F# sidecars** — published as global `dotnet tool` packages - (`dotnet tool install -g SharpLsp.Sidecar.CSharp` / `.FSharp`). -3. **VS Code extension (and any future editor extension)** — at activation, - MUST verify that all three components are installed at the exact version - the VSIX expects by spawning each binary with `--version` and comparing - the output to the version in `package.json`. This is non-negotiable: no - trust-on-presence, no bundled fallback, no version drift. +1. **Rust `sharplsp` binary** — Homebrew (macOS/Linux) and Scoop (Windows), driven by GitHub release assets. +2. **C# and F# sidecars** — published as global `dotnet tool` packages (`dotnet tool install -g SharpLsp.Sidecar.CSharp` / `.FSharp`). +3. **VS Code extension (and any future editor extension)** — at activation, MUST verify that all three components are installed at the exact version the VSIX expects by spawning each binary with `--version` and comparing the output to the version in `package.json`. This is non-negotiable: no trust-on-presence, no bundled fallback, no version drift. - If any component is missing or mismatched, the extension MUST actively - run the matching package manager (`brew` / `scoop` / `dotnet tool install`) - to install or update it — after prompting the user once with a modal. - Editor extensions are forbidden from downloading binaries directly; all - installation goes through Homebrew, Scoop, or the dotnet tool CLI. + If any component is missing or mismatched, the extension MUST actively run the matching package manager (`brew` / `scoop` / `dotnet tool install`) to install or update it — after prompting the user once with a modal. Editor extensions are forbidden from downloading binaries directly; all installation goes through Homebrew, Scoop, or the dotnet tool CLI. -The reference is [dart_mutant's release.yml](../../Documents/Code/dart_mutant/.github/workflows/release.yml): -tag-triggered build → GitHub release → auto-update of `Nimblesite/homebrew-tap` -and `Nimblesite/scoop-bucket` via `BREW_SCOOP_PAT`. SharpLsp follows the same -pattern, plus a NuGet.org push for the two sidecars. - -## Architecture +## Architecture `[BINARY-ARCHITECTURE]` ```mermaid flowchart TD @@ -50,7 +32,7 @@ flowchart TD UserBrew & UserScoop & UserCS & UserFS --> VSIX[VS Code extension activates] ``` -## Runtime resolution in the VSIX +## Runtime resolution in the VSIX `[BINARY-RUNTIME]` ```mermaid flowchart TD @@ -78,72 +60,40 @@ flowchart TD Rules: -- Version check is ALWAYS by spawning the binary with `--version` and string - matching against the `package.json` version. No file-presence shortcuts, no - cached results across sessions. -- The extension is forbidden from downloading binaries directly over HTTPS. - The only installation paths are `brew`, `scoop`, and `dotnet tool install`. -- If the package manager itself is missing (no `brew` on macOS, no `scoop` on - Windows, no `dotnet` anywhere), show a modal with a link to install the - package manager and abort activation. -- Never fall back to a "best effort" older version. Expected version == installed - version, byte-for-byte. Mismatch = install/update. +- Version check is ALWAYS by spawning the binary with `--version` and string matching against the `package.json` version. No file-presence shortcuts, no cached results across sessions. +- The extension is forbidden from downloading binaries directly over HTTPS. The only installation paths are `brew`, `scoop`, and `dotnet tool install`. +- If the package manager itself is missing (no `brew` on macOS, no `scoop` on Windows, no `dotnet` anywhere), show a modal with a link to install the package manager and abort activation. +- Never fall back to a "best effort" older version. Expected version == installed version, byte-for-byte. Mismatch = install/update. -## Changes +## Required changes `[BINARY-CHANGES]` -### 1. Sidecar projects — make them dotnet tools (framework-dependent) +### Framework-dependent sidecar tools `[BINARY-SIDECARS]` -`sidecars/SharpLsp.Sidecar.CSharp/SharpLsp.Sidecar.CSharp.csproj` -`sidecars/SharpLsp.Sidecar.FSharp/SharpLsp.Sidecar.FSharp.fsproj` +`sidecars/SharpLsp.Sidecar.CSharp/SharpLsp.Sidecar.CSharp.csproj` `sidecars/SharpLsp.Sidecar.FSharp/SharpLsp.Sidecar.FSharp.fsproj` Changes: -- **Remove `true`.** Sidecars ship as - framework-dependent dotnet tools. One .nupkg per sidecar, cross-platform. - Users install `.NET 10 Runtime` as a prerequisite (checked by the VSIX - before prompting — if `dotnet --version` is missing, send the user to - dotnet.microsoft.com). Roslyn's `BuildHost-netcore` DLLs and FCS's runtime - dependencies ship inside the tool package's `tools//any/` directory - and are resolved at runtime relative to the tool entry point. +- **Remove `true`.** Sidecars ship as framework-dependent dotnet tools. One .nupkg per sidecar, cross-platform. Users install `.NET 10 Runtime` as a prerequisite (checked by the VSIX before prompting — if `dotnet --version` is missing, send the user to dotnet.microsoft.com). Roslyn's `BuildHost-netcore` DLLs and FCS's runtime dependencies ship inside the tool package's `tools//any/` directory and are resolved at runtime relative to the tool entry point. - Add `true` - Add `sharplsp-sidecar-csharp` / `sharplsp-sidecar-fsharp` - Add `SharpLsp.Sidecar.CSharp` / `SharpLsp.Sidecar.FSharp` -- Add ``, ``, ``, - ``, `` -- `` injected at pack time from the git tag - (`dotnet pack -p:PackageVersion=$VERSION`) -- Add `--version` flag handling in `Program.cs` / `Program.fs` that prints - `sharplsp-sidecar-csharp ` (read from the assembly's - `InformationalVersion` attribute, stamped at pack time) so the extension - can version-check by spawning the installed tool. - -**Risk: will MSBuildWorkspace still work?** `MSBuildWorkspace` spawns -`BuildHost-netcore.dll` as a child process using a path resolved relative to -the Roslyn assembly location. Inside a dotnet global tool, the Roslyn -assemblies are unpacked to -`~/.dotnet/tools/.store/sharplsp.sidecar.csharp//sharplsp.sidecar.csharp//tools/net10.0/any/` -and `BuildHost-netcore.dll` is in the same folder as the Roslyn package -dependencies (dotnet pack copies all `PackageReference` content into the -tool output). This should Just Work — but verification step 1 MUST confirm -it against a real `.csproj` before merging. If it genuinely breaks (e.g. FCS -using `Assembly.Location` returning a path that no longer contains FSharp.Core), -the fix is to flip `LatestMajor` and ensure -`true` so every -transitive dep lands next to the tool DLL. Still dotnet tool. No self-contained. - -### 2. Rust binary — already has `--version`, confirm via `sharplsp --version` - -No changes needed in the Rust source. Already verified at -[install.ts:74-91](editors/vscode/src/install.ts#L74-L91). - -### 3. `.github/workflows/release.yml` — rewrite +- Add ``, ``, ``, ``, `` +- `` injected at pack time from the git tag (`dotnet pack -p:PackageVersion=$VERSION`) +- Add `--version` flag handling in `Program.cs` / `Program.fs` that prints `sharplsp-sidecar-csharp ` (read from the assembly's `InformationalVersion` attribute, stamped at pack time) so the extension can version-check by spawning the installed tool. + +**Risk: will MSBuildWorkspace still work?** `MSBuildWorkspace` spawns `BuildHost-netcore.dll` as a child process using a path resolved relative to the Roslyn assembly location. Inside a dotnet global tool, the Roslyn assemblies are unpacked to `~/.dotnet/tools/.store/sharplsp.sidecar.csharp//sharplsp.sidecar.csharp//tools/net10.0/any/` and `BuildHost-netcore.dll` is in the same folder as the Roslyn package dependencies (dotnet pack copies all `PackageReference` content into the tool output). This should Just Work — but verification step 1 MUST confirm it against a real `.csproj` before merging. If it genuinely breaks (e.g. FCS using `Assembly.Location` returning a path that no longer contains FSharp.Core), the fix is to flip `LatestMajor` and ensure `true` so every transitive dep lands next to the tool DLL. Still dotnet tool. No self-contained. + +### Rust binary version contract `[BINARY-RUST]` + +No changes needed in the Rust source. Already verified at [`install.ts`](../../editors/vscode/src/install.ts). + +### Release workflow `[BINARY-RELEASE]` Replace the current monolithic archive job with: **Job A: `build-sharplsp`** (matrix: 4 targets) - Build `cargo build --release --target ` -- Package single binary as `sharplsp--.{tar.gz,zip}` (no - sidecar dirs — just the binary, like dart_mutant) +- Package single binary as `sharplsp--.{tar.gz,zip}` (no sidecar dirs — just the binary, like dart_mutant) - Upload artifact **Job B: `pack-sidecars`** (single ubuntu job — framework-dependent, no RID matrix) @@ -163,8 +113,7 @@ Replace the current monolithic archive job with: **Job E: `update-homebrew`** (needs [release]) - Checkout `Nimblesite/homebrew-tap` with `BREW_SCOOP_PAT` - Download macOS arm64 + macOS x64 + linux x64 tar.gz assets, sha256 each -- Generate `Formula/sharplsp.rb` with `on_macos {on_arm / on_intel}` and - `on_linux { on_intel }` blocks, one url+sha256 per block +- Generate `Formula/sharplsp.rb` with `on_macos {on_arm / on_intel}` and `on_linux { on_intel }` blocks, one url+sha256 per block - `def install; bin.install "sharplsp"; end` - `test do; assert_match "sharplsp", shell_output("#{bin}/sharplsp --version"); end` - Commit and push @@ -172,15 +121,12 @@ Replace the current monolithic archive job with: **Job F: `update-scoop`** (needs [release]) - Checkout `Nimblesite/scoop-bucket` with `BREW_SCOOP_PAT` - Download win x64 zip, sha256 it -- Write `bucket/sharplsp.json` with `architecture."64bit".{url,hash,bin}`, - `checkver.github`, `autoupdate.architecture."64bit".url` template +- Write `bucket/sharplsp.json` with `architecture."64bit".{url,hash,bin}`, `checkver.github`, `autoupdate.architecture."64bit".url` template - Commit and push -### 4. VS Code extension — rewrite `editors/vscode/src/install.ts` +### VS Code installation flow `[BINARY-VSCODE]` -Replace `ensureBinaries` and the entire download path -([install.ts:107-306](editors/vscode/src/install.ts#L107-L306)) with a -verify-then-install-via-package-manager layer. +Replace `ensureBinaries` and the entire download path in [`install.ts`](../../editors/vscode/src/install.ts) with a verify-then-install-via-package-manager layer. **Version check (mandatory, always via `--version`):** @@ -191,8 +137,7 @@ function getVersion(command: string): string | undefined { } ``` -This is called for all three: `sharplsp`, `sharplsp-sidecar-csharp`, -`sharplsp-sidecar-fsharp`. No file-existence checks, no fallbacks. +This is called for all three: `sharplsp`, `sharplsp-sidecar-csharp`, `sharplsp-sidecar-fsharp`. No file-existence checks, no fallbacks. **Package-manager-driven install (the only install path):** @@ -216,161 +161,88 @@ const INSTALL_COMMANDS = { }; ``` -- `update` (not `install`) is used so the same command works for both first - install and version bump. `dotnet tool update -g --version X` installs if - absent and re-pins if present. -- For `sharplsp` on Scoop, version pinning uses `scoop install sharplsp@X` - if the bucket manifest supports it, otherwise `scoop update sharplsp`. +- `update` (not `install`) is used so the same command works for both first install and version bump. `dotnet tool update -g --version X` installs if absent and re-pins if present. +- For `sharplsp` on Scoop, version pinning uses `scoop install sharplsp@X` if the bucket manifest supports it, otherwise `scoop update sharplsp`. **Flow for each binary:** 1. `getVersion(binary)` → compare to `expectedVersion()` 2. If match: use it, done. -3. If mismatch: show modal with OK/Cancel — "SharpLsp needs to install - `` at version ``. Run ``?" -4. OK → spawn the command, stream stdout/stderr to an Output Channel so - the user sees progress. On exit, re-run step 1. +3. If mismatch: show modal with OK/Cancel — "SharpLsp needs to install `` at version ``. Run ``?" +4. OK → spawn the command, stream stdout/stderr to an Output Channel so the user sees progress. On exit, re-run step 1. 5. Cancel → throw, activation aborts. **Preflight — package manager presence:** -Before running any install command, run `getVersion("brew")` / -`getVersion("scoop")` / `getVersion("dotnet")`. If the required package -manager is missing, show a modal with a link to the install page and -abort. Do not offer to install package managers automatically. +Before running any install command, run `getVersion("brew")` / `getVersion("scoop")` / `getVersion("dotnet")`. If the required package manager is missing, show a modal with a link to the install page and abort. Do not offer to install package managers automatically. **Deletions:** -- `downloadAndInstall`, `downloadToFile`, `extractTarGz`, `platformRid`, - `bundledBinaryPath`, and the whole GitHub-release HTTPS path. -- The `bin/` VSIX bundling path and the `~/.local/lib/sharplsp/` staging from - both the Makefile `install` target and `.github/workflows/test-vscode.yml` - (recent commits `c6f29f0` and `e1dd2ca` become partially obsolete). +- `downloadAndInstall`, `downloadToFile`, `extractTarGz`, `platformRid`, `bundledBinaryPath`, and the whole GitHub-release HTTPS path. +- The `bin/` VSIX bundling path and the `~/.local/lib/sharplsp/` staging from both the Makefile `install` target and `.github/workflows/test-vscode.yml` (recent commits `c6f29f0` and `e1dd2ca` become partially obsolete). **Forbidden patterns (encoded as lint / code review):** - `https.get(...)` or `fetch(...)` for binary downloads -- Any path that writes executables into `~/.local/`, `extensionPath/bin/`, - or a temp dir with intent to execute +- Any path that writes executables into `~/.local/`, `extensionPath/bin/`, or a temp dir with intent to execute - Any "skip version check if binary exists" shortcut -### 5. Makefile — simplify `install` target +### Makefile installation targets `[BINARY-MAKEFILE]` -`Makefile:370-383` — the `install` target currently stages sharplsp + -sidecars into `$PREFIX`. Replace with: +`Makefile:370-383` — the `install` target currently stages sharplsp + sidecars into `$PREFIX`. Replace with: - `install-rust`: just copies `sharplsp` to `$PREFIX/bin` (for local dev) -- `install-sidecars`: runs `dotnet tool install -g` from locally packed - nupkgs so contributors can test the tool install flow end-to-end -- Drop `~/.local/lib/sharplsp/` entirely. Sidecars now live wherever - `dotnet tool` puts them (`~/.dotnet/tools` on macOS/Linux, - `%USERPROFILE%\.dotnet\tools` on Windows). - -### 6. Docs - -New: `docs/specs/DISTRIBUTION-SPEC.md` — canonical spec for how SharpLsp is -distributed. MUST state the following as normative requirements (not -suggestions): - -1. **Three channels, no alternatives.** - - `sharplsp` → Homebrew (macOS/Linux) and Scoop (Windows). - - `SharpLsp.Sidecar.CSharp` → dotnet global tool on NuGet.org. - - `SharpLsp.Sidecar.FSharp` → dotnet global tool on NuGet.org. - - Sidecars are **framework-dependent** dotnet tools. `SelfContained=true` - is forbidden in sidecar csproj/fsproj files. - -2. **Version invariant.** `Cargo.toml` `version` is the single source of - truth. The release workflow stamps the tag version into: - - `Cargo.toml` (at build time only, not committed) - - `editors/vscode/package.json` (at build time only) - - Sidecar `.nupkg` package versions - - Assembly `InformationalVersion` for sidecar `--version` output - All five must match byte-for-byte for a release to be valid. - -3. **Editor extension contract.** Any editor extension (VS Code today, - Zed/JetBrains/Neovim in the future) MUST: - - Check all three binary versions on activation by spawning each with - `--version` and string-matching against the extension's own version. - - NEVER download binaries directly over HTTPS. The only installation - mechanisms are `brew`, `scoop`, and `dotnet tool install`/`update`. - - On mismatch, prompt the user once (modal) and then run the matching - package-manager command, streaming output to a visible log. - - Abort activation on user cancel or install failure — never fall back - to a degraded mode or older version. - -4. **Tap/bucket repo layout.** `Nimblesite/homebrew-tap` contains - `Formula/sharplsp.rb`. `Nimblesite/scoop-bucket` contains - `bucket/sharplsp.json`. Both are auto-updated by the release workflow - using `BREW_SCOOP_PAT`. Manual edits are forbidden. - -5. **Required secrets on `Nimblesite/SharpLsp`:** `BREW_SCOOP_PAT` (PAT with - `contents:write` on both tap repos), `NUGET_API_KEY` (push rights to - `SharpLsp.Sidecar.*` on nuget.org). - -New: `docs/plans/DISTRIBUTION-PLAN.md` — TODO checklist mirroring the -changes in this plan file. - -Update: `docs/specs/SHARPLSP-SPEC.md` — add a short "Distribution" section -linking to `DISTRIBUTION-SPEC.md`. - -## Critical files - -- [.github/workflows/release.yml](.github/workflows/release.yml) — rewrite -- [editors/vscode/src/install.ts](editors/vscode/src/install.ts) — gut and replace -- [sidecars/SharpLsp.Sidecar.CSharp/SharpLsp.Sidecar.CSharp.csproj](sidecars/SharpLsp.Sidecar.CSharp/SharpLsp.Sidecar.CSharp.csproj) — add `PackAsTool` -- [sidecars/SharpLsp.Sidecar.FSharp/SharpLsp.Sidecar.FSharp.fsproj](sidecars/SharpLsp.Sidecar.FSharp/SharpLsp.Sidecar.FSharp.fsproj) — add `PackAsTool` -- [sidecars/SharpLsp.Sidecar.CSharp/Program.cs](sidecars/SharpLsp.Sidecar.CSharp/Program.cs) — add `--version` -- [sidecars/SharpLsp.Sidecar.FSharp/Program.fs](sidecars/SharpLsp.Sidecar.FSharp/Program.fs) — add `--version` -- [Makefile](Makefile) — simplify `install` target +- `install-sidecars`: runs `dotnet tool install -g` from locally packed nupkgs so contributors can test the tool install flow end-to-end +- Drop `~/.local/lib/sharplsp/` entirely. Sidecars now live wherever `dotnet tool` puts them (`~/.dotnet/tools` on macOS/Linux, `%USERPROFILE%\.dotnet\tools` on Windows). + +### Canonical documentation `[BINARY-DOCS]` + +[`DISTRIBUTION-SPEC.md`](DISTRIBUTION-SPEC.md) is the canonical distribution contract; [`DISTRIBUTION-PLAN.md`](../plans/DISTRIBUTION-PLAN.md) tracks implementation. `SHARPLSP-SPEC.md` links to the canonical contract rather than duplicating it. + +## Critical files `[BINARY-FILES]` + +- [`.github/workflows/release.yml`](../../.github/workflows/release.yml) — rewrite +- [`editors/vscode/src/install.ts`](../../editors/vscode/src/install.ts) — replace the download path +- [`sidecars/SharpLsp.Sidecar.CSharp/SharpLsp.Sidecar.CSharp.csproj`](../../sidecars/SharpLsp.Sidecar.CSharp/SharpLsp.Sidecar.CSharp.csproj) — add `PackAsTool` +- [`sidecars/SharpLsp.Sidecar.FSharp/SharpLsp.Sidecar.FSharp.fsproj`](../../sidecars/SharpLsp.Sidecar.FSharp/SharpLsp.Sidecar.FSharp.fsproj) — add `PackAsTool` +- [`sidecars/SharpLsp.Sidecar.CSharp/Program.cs`](../../sidecars/SharpLsp.Sidecar.CSharp/Program.cs) — add `--version` +- [`sidecars/SharpLsp.Sidecar.FSharp/Program.fs`](../../sidecars/SharpLsp.Sidecar.FSharp/Program.fs) — add `--version` +- [`Makefile`](../../Makefile) — simplify `install` target - `docs/specs/DISTRIBUTION-SPEC.md` — new - `docs/plans/DISTRIBUTION-PLAN.md` — new -## External prerequisites +## External prerequisites `[BINARY-PREREQUISITES]` -These must exist before the release workflow will succeed. Create them -before merging the changes: +These must exist before the release workflow will succeed. Create them before merging the changes: - GitHub repo `Nimblesite/homebrew-tap` (empty, default branch `main`) - GitHub repo `Nimblesite/scoop-bucket` (empty, default branch `main`) -- PAT with `contents:write` on both repos → add as `BREW_SCOOP_PAT` secret - on `Nimblesite/SharpLsp` -- NuGet.org account + API key with push rights to `SharpLsp.Sidecar.*` → - add as `NUGET_API_KEY` secret -- Reserve package IDs `SharpLsp.Sidecar.CSharp` and `SharpLsp.Sidecar.FSharp` on - nuget.org via a manual 0.0.1-preview push, to prevent squatting +- PAT with `contents:write` on both repos → add as `BREW_SCOOP_PAT` secret on `Nimblesite/SharpLsp` +- NuGet.org account + API key with push rights to `SharpLsp.Sidecar.*` → add as `NUGET_API_KEY` secret +- Reserve package IDs `SharpLsp.Sidecar.CSharp` and `SharpLsp.Sidecar.FSharp` on nuget.org via a manual 0.0.1-preview push, to prevent squatting -## Verification +## Verification `[BINARY-VERIFY]` 1. **Local dry-run of sidecar packaging** ``` - dotnet pack sidecars/SharpLsp.Sidecar.CSharp -p:PackageVersion=0.1.1 \ - -p:RuntimeIdentifier=osx-arm64 -o /tmp/nupkgs + dotnet pack sidecars/SharpLsp.Sidecar.CSharp -p:PackageVersion=0.1.1 -o /tmp/nupkgs dotnet tool install -g --add-source /tmp/nupkgs SharpLsp.Sidecar.CSharp sharplsp-sidecar-csharp --version # must print "sharplsp-sidecar-csharp 0.1.1" ``` - Confirms `PackAsTool` + `SelfContained` + multi-RID strategy actually works. - **If this fails**, the whole dotnet-tool channel is invalid and we need - to reconsider (fallback: ship sidecars as GitHub release tarballs beside - sharplsp, verified by path on PATH). + Confirms `PackAsTool` and the framework-dependent dependency layout. Failure blocks the dotnet-tool channel. 2. **VSIX verification path** - Build VSIX locally with version `0.1.1` - Install sidecars at `0.1.0` and `sharplsp` at `0.1.1` - Activate extension in a fresh VS Code window - - Expect: activation fails fast with a modal showing the exact - `dotnet tool update -g SharpLsp.Sidecar.CSharp --version 0.1.1` command + - Expect: activation fails fast with a modal showing the exact `dotnet tool update -g SharpLsp.Sidecar.CSharp --version 0.1.1` command - Install matching versions, reactivate — expect clean startup 3. **Tag-driven end-to-end** - Push tag `v0.1.1-rc1` to a test fork - - Observe: `release` workflow succeeds, GitHub release created, - `homebrew-tap` and `scoop-bucket` forks receive commits, nupkgs - appear on nuget.org - - On a clean macOS VM: `brew install Nimblesite/tap/sharplsp` + both - `dotnet tool install` commands → VS Code extension activates cleanly + - Observe: `release` workflow succeeds, GitHub release created, `homebrew-tap` and `scoop-bucket` forks receive commits, nupkgs appear on nuget.org + - On a clean macOS VM: `brew install Nimblesite/tap/sharplsp` + both `dotnet tool install` commands → VS Code extension activates cleanly - On a clean Windows VM: same via scoop 4. **CI smoke test** - - Add a job to `ci.yml` that runs `dotnet pack` on both sidecars - (without publishing) on every PR, so packaging regressions are caught - before tag time. + - Add a job to `ci.yml` that runs `dotnet pack` on both sidecars (without publishing) on every PR, so packaging regressions are caught before tag time. diff --git a/docs/specs/DEBUGGING-SPEC.md b/docs/specs/DEBUGGING-SPEC.md index e3c8cbcf..9d798339 100644 --- a/docs/specs/DEBUGGING-SPEC.md +++ b/docs/specs/DEBUGGING-SPEC.md @@ -1,51 +1,16 @@ -# DEBUGGING-SPEC +# SharpLsp Debugging Technical Specification `[DEBUG]` -**SharpLsp Debugging Technical Specification** +## Mission `[DEBUG-MISSION]` -*March 2026 | DRAFT* +SharpLsp debugging MUST use redistributable open-source components, work through DAP in any editor, and provide the same specified behavior for C# and F#. The proprietary `vsdbg` binary MUST NOT be distributed or invoked. ---- +## Debugger Adapter Selection `[DEBUG-ADAPTER]` -## 1. Mission +### Phase Four Adapter `[DEBUG-ADAPTER-NETCOREDBG]` -SharpLsp must deliver a top-tier .NET debugging experience that is fully open-source, editor-agnostic, and license-free. Microsoft's proprietary `vsdbg` is explicitly forbidden by its license from use in any editor except Visual Studio, Visual Studio Code (Microsoft-signed binary), and Visual Studio for Mac. SharpLsp must match or exceed the vsdbg experience using only open-source infrastructure. +Phase Four uses the MIT-licensed netcoredbg `3.1.3-1062` adapter over DAP `1.71.0` on stdin/stdout. It provides line, conditional, function, and exception breakpoints; step in/over/out; variables; call stacks; and Linux x64/ARM64 mixed-mode debugging. Phase Five replaces it with the native SharpLsp Debug Sidecar specified in [DEBUG-ARCHITECTURE-SIDECAR]. -The benchmark is brutal: a developer coming from `vsdbg` must not feel degraded. Every mainstream debugging workflow must work. Gaps in open-source tooling that cannot be closed by configuration must be closed by engineering. - -C# and F# are treated as equal first-class citizens. F# debugging is not an afterthought. - ---- - -## 2. Debugger Adapter Selection - -### 2.1 The Landscape - -| Debugger | License | Language | Production-Ready | Notable Gaps | -|---|---|---|---|---| -| **vsdbg** (Microsoft) | **Proprietary — FORBIDDEN** | C++ | Yes | License bars non-VS-Code products | -| **netcoredbg** (Samsung) | MIT | C++ over ICorDebug | Mostly (see §2.3) | Expression eval, async stacks, logpoints, DebuggerDisplay, EnC on Linux/macOS | -| **SharpDbg** (MattParkerDev) | MIT | C# over ClrDebug | Preview (0.1.0-preview5) | Lambda stepping incomplete; Source Link absent; pre-production | -| **Mono SDB** | MIT | Mono | Yes (Mono only) | Incompatible with CoreCLR; not applicable | -| **Rider debugger** (JetBrains) | Proprietary | Java + .NET | Yes | Not redistributable; IntelliJ-coupled | - -**Decision: netcoredbg is the primary debugger for Phase 4, with a parallel investment in a SharpLsp-native C# Debug Sidecar (Tier 4) targeting full vsdbg parity in Phase 5.** - -### 2.2 Why netcoredbg - -- Only MIT-licensed CoreCLR debugger with production DAP support -- Implements the full DAP protocol (v1.71.0) over stdin/stdout — drop-in compatible with any editor -- Used in production by VSCodium, Neovim, Helix, Emacs, and MonoDevelop communities -- Actively maintained by Samsung's Linux Platform team (latest: 3.1.3-1062, December 2025) -- Covers all P1 debugging scenarios: line/conditional/function/exception breakpoints, step in/over/out, variable inspection, call stack navigation -- Supports Linux (x64, ARM64, ARM, RISCV64), Windows (x64, x86, ARM64), macOS (x64, ARM64 community builds) -- Three protocol frontends: CLI, GDB/MI, VSCode DAP — all sharing the same `ManagedDebugger` core -- Supports mixed-mode (managed + native interop) debugging on Linux x64/ARM64 - -### 2.3 netcoredbg Known Gaps and Open Issues - -netcoredbg has real, material gaps versus vsdbg. These are not cosmetic. - -**Confirmed missing features:** +### netcoredbg Gaps `[DEBUG-ADAPTER-GAPS]` | Gap | Impact | Upstream Issue | |---|---|---| @@ -66,66 +31,13 @@ netcoredbg has real, material gaps versus vsdbg. These are not cosmetic. | `Nullable` expansion broken | `Nullable` and similar value types cannot be expanded in debugger | Issue #213 | | Version 3.1.3 stability regression | Crashes on every run in some configurations | Issue #217, #206 | -### 2.4 Why Not Stop at netcoredbg - -The SharpLsp answer is a **two-phase approach**: - -1. **Phase 4**: Ship netcoredbg integration, closing the most impactful gaps via DapRouter-layer workarounds and upstream contributions -2. **Phase 5**: Ship the SharpLsp Debug Sidecar — a C# Tier 4 process built on `ClrDebug` + `ICorDebug` that achieves full vsdbg feature parity - -### 2.5 SharpDbg — Watch and Contribute - -SharpDbg (MattParkerDev, MIT, C#) is the most promising long-term foundation for community .NET debugging. It already implements `[DebuggerDisplay]`, `[DebuggerTypeProxy]`, and `[DebuggerBrowsable]` — all absent in netcoredbg. It uses ClrDebug, the same foundation as the planned SharpLsp Debug Sidecar. +SharpDbg `0.1.0-preview5` MAY replace a from-scratch Phase Five sidecar only after its missing lambda stepping and Source Link behavior is implemented and its DAP behavior passes SharpLsp acceptance tests. ICorDebug wrapper fixes SHOULD be contributed upstream; SharpLsp MUST NOT maintain a product fork. -**SharpLsp's relationship with SharpDbg:** -- Monitor SharpDbg for production readiness; evaluate as a Phase 5 foundation vs. building from scratch -- Contribute upstream: any ICorDebug wrapper gaps discovered during SharpLsp Debug Sidecar work -- Do not fork SharpDbg; if it reaches production maturity before Phase 5, adopt rather than reinvent +## Architecture `[DEBUG-ARCHITECTURE]` ---- - -## 3. Architecture - -### 3.1 System Topology - -``` -┌────────────────────────────────────────────────────────────────────┐ -│ Editor (VS Code, Neovim, Helix, Zed, Emacs, …) │ -│ DAP JSON-RPC over stdio/socket │ -└───────────────────────────┬────────────────────────────────────────┘ - │ DAP 1.71.0 (JSON-RPC) - ▼ -┌────────────────────────────────────────────────────────────────────┐ -│ Tier 1: Rust LSP/DAP Host (sharplsp) │ -│ ┌──────────────────────────────────────────────────────────────┐ │ -│ │ DapRouter │ │ -│ │ - Proxies DAP to active debug adapter │ │ -│ │ - Augments: logpoints, async stacks, DebuggerDisplay │ │ -│ │ - Manages adapter lifecycle (spawn, health, restart) │ │ -│ │ - Multiplexes multi-process debug sessions │ │ -│ └──────────────────────┬───────────────────────────────────────┘ │ -└─────────────────────────┼──────────────────────────────────────────┘ - │ - ┌───────────────┴───────────────┐ - │ │ - ▼ Phase 4 ▼ Phase 5 -┌─────────────────────┐ ┌──────────────────────────────────┐ -│ netcoredbg │ │ Tier 4: SharpLsp Debug Sidecar │ -│ (external process) │ │ (C# process) │ -│ DAP stdin/stdout │ │ ClrDebug + ICorDebug + DbgShim │ -│ MIT licensed │ │ DAP stdin/stdout │ -└──────────┬──────────┘ └──────────────────┬───────────────┘ - │ │ - └───────────────┬──────────────────────┘ - │ ICorDebug / DbgShim - ▼ - ┌─────────────────────┐ - │ Target .NET Process│ - │ (CoreCLR runtime) │ - └─────────────────────┘ -``` +Editors communicate through DAP with the Rust `DapRouter`, which proxies Phase Four sessions to netcoredbg and Phase Five sessions to the C# Debug Sidecar; both adapters control the target CoreCLR process through ICorDebug/DbgShim. The current Phase Four editor adapter factory and resolution logic are in [`editors/vscode/src/debug.ts`](../../editors/vscode/src/debug.ts), with coarse coverage in [`debug-e2e.test.ts`](../../editors/vscode/src/test/suite/debug-e2e.test.ts); the Rust router and Phase Five sidecar remain target architecture. -### 3.2 Rust DapRouter +### Rust DapRouter `[DEBUG-ARCHITECTURE-ROUTER]` The Rust host runs a `DapRouter` module responsible for: @@ -138,16 +50,16 @@ The Rust host runs a `DapRouter` module responsible for: - **Multi-session management**: maintains a registry of active debug sessions for multi-process/multi-project scenarios - **Hot Reload coordination**: integrates with `dotnet watch` / `MetadataUpdater.ApplyUpdate` for hot reload during debug sessions -### 3.3 netcoredbg Integration (Phase 4) +### netcoredbg Integration (Phase Four) `[DEBUG-ARCHITECTURE-NETCOREDBG]` netcoredbg is managed as an external subprocess: -- **Distribution**: bundled with SharpLsp release artifacts (platform-specific binary), or auto-downloaded on first debug launch if not present (with SHA-256 hash verification) +- **Distribution**: [`debug.ts`](../../editors/vscode/src/debug.ts) resolves a configured path, bundled platform artifact, standard user install, or `PATH`; downloaded artifacts require SHA-256 verification - **Version pinning**: SharpLsp pins a specific netcoredbg release (currently 3.1.3-1062) and upgrades on a tested cadence - **Transport**: DAP over stdin/stdout; DapRouter opens the child process and pipes JSON-RPC - **Launch modes**: - `launch`: spawn a new .NET process - - `attach`: attach to an existing PID (known reliability issues — see §6.3) + - `attach`: attach to an existing PID (known reliability issues; see [DEBUG-GAPS]) - **Platform matrix**: | Platform | Source | Notes | @@ -158,14 +70,14 @@ netcoredbg is managed as an external subprocess: | macOS ARM64 | SharpLsp CI build from source | Samsung does not ship official ARM64 macOS binaries | | Windows x64 | Official Samsung release binary | Full feature set | | Windows ARM64 | Official Samsung release binary | Full feature set | -| Alpine/musl x64 | SharpLsp CI musl-linked build | Workaround for SIGSEGV on musl — see §6.5 | +| Alpine/musl x64 | SharpLsp CI musl-linked build | Workaround for SIGSEGV on musl; see [DEBUG-GAPS] | | Alpine/musl ARM64 | SharpLsp CI musl-linked build | Same musl workaround | -### 3.4 SharpLsp Debug Sidecar (Phase 5) +### SharpLsp Debug Sidecar (Phase Five) `[DEBUG-ARCHITECTURE-SIDECAR]` A new C# process (Tier 4) that implements the full ICorDebug-based debugger natively: -- **Language**: C# 13 on .NET 9+, matching the existing sidecar architecture +- **Runtime**: C# sidecar targeting `net10.0` - **Core dependency**: [`ClrDebug`](https://github.com/lordmilko/ClrDebug) v0.3.4+ — managed type-safe P/Invoke wrappers for every ICorDebug COM interface (MIT). On .NET 8+, uses source-generated COM interop for zero-overhead marshaling. - **Bootstrap**: `Microsoft.Diagnostics.DbgShim` NuGet package (v9.0.661903+, MIT) for runtime discovery and ICorDebug bootstrapping - **Protocol**: DAP over stdin/stdout (same as netcoredbg, fully drop-in from the DapRouter's perspective) @@ -174,13 +86,11 @@ A new C# process (Tier 4) that implements the full ICorDebug-based debugger nati - **Async stack reconstruction**: reads state-machine fields from heap objects via `ICorDebugValue` traversal; reconstructs the logical async continuation chain - **DebuggerDisplay/TypeProxy**: first-class support; evaluates attribute format strings in the debuggee context and returns formatted display values ---- - -## 4. DAP Protocol +## DAP Protocol `[DEBUG-PROTOCOL]` SharpLsp targets **DAP specification version 1.71.0**. -### 4.1 Key Capabilities Used +### Key Capabilities `[DEBUG-PROTOCOL-CAPABILITIES]` | Capability | Phase 4 | Phase 5 | Notes | |---|---|---|---| @@ -206,11 +116,9 @@ SharpLsp targets **DAP specification version 1.71.0**. | `supportsGotoTargetsRequest` | Yes | Yes | Run to cursor via `goto` | | `supportsLocationReference` | No | Yes | DAP 1.68+ location navigation | ---- - -## 5. Feature Specification +## Feature Specification `[DEBUG-FEATURES]` -### 5.1 Launch and Attach +### Launch and Attach `[DEBUG-FEATURES-LAUNCH]` | Feature | DAP Method | Priority | Notes | |---|---|---|---| @@ -221,7 +129,7 @@ SharpLsp targets **DAP specification version 1.71.0**. | Launch with environment variables | `launch` (env) | P1 | | | Launch with custom working directory | `launch` (cwd) | P1 | | | Launch browser for Blazor WASM | `launch` (browser) | P3 | Requires browser devtools bridge | -| Hot Reload enabled launch | `launch` (hotReload: true) | P2 | See §5.9 | +| Hot Reload enabled launch | `launch` (hotReload: true) | P2 | See [DEBUG-FEATURES-HOT-RELOAD] | | Child process auto-attach | `launch` event | P2 | Phase 5: `ICorDebugManagedCallback::CreateProcess` | **Launch configuration schema** (`launch.json` / inline config): @@ -230,7 +138,7 @@ SharpLsp targets **DAP specification version 1.71.0**. { "type": "sharplsp", "request": "launch", - "program": "${workspaceFolder}/bin/Debug/net9.0/MyApp.dll", + "program": "${workspaceFolder}/bin/Debug/net10.0/MyApp.dll", "args": [], "cwd": "${workspaceFolder}", "env": {}, @@ -257,7 +165,7 @@ SharpLsp targets **DAP specification version 1.71.0**. } ``` -### 5.2 Breakpoints +### Breakpoints `[DEBUG-FEATURES-BREAKPOINTS]` | Feature | DAP Method | Priority | Implementation | |---|---|---|---| @@ -277,9 +185,9 @@ netcoredbg does not support logpoints natively. DapRouter intercepts `setBreakpo 2. Calls `System.Diagnostics.Debug.WriteLine(msg)` to emit the output 3. Returns `false` so execution is never paused -Output is captured from the debug output channel and surfaced as a DAP `output` event. This is transparent to the editor. +Output is captured from the debug output channel and surfaced as a DAP `output` event. Hit conditions accept `>`, `>=`, `<`, `<=`, `==`, and `%` against the hit counter. Phase Five implements logpoints with `ICorDebugBreakpoint`, immediate `ICorDebugEval`, and `ICorDebugProcess::Continue` without a visible pause. -### 5.3 Stepping +### Stepping `[DEBUG-FEATURES-STEPPING]` | Feature | DAP Method | Priority | |---|---|---| @@ -296,20 +204,20 @@ Output is captured from the debug output channel and surfaced as a DAP `output` **Smart Step Into (F# Phase 5)**: When a single source line in F# calls multiple functions (pipeline operators, function composition), Smart Step Into presents a list of step targets via the DAP `stepIn` `targetId` mechanism. This requires FCS-provided source location analysis to identify callsites on the current line. -### 5.4 Call Stack +### Call Stack `[DEBUG-FEATURES-STACK]` | Feature | DAP Method | Priority | Notes | |---|---|---|---| | Call stack display | `stackTrace` | P1 | Physical frames | -| Logical async call stack | `stackTrace` (enriched) | P1 | DapRouter + Roslyn reconstruction (§5.4.1) | +| Logical async call stack | `stackTrace` (enriched) | P1 | DapRouter + Roslyn reconstruction ([DEBUG-FEATURES-STACK-ASYNC]) | | Navigate to source from frame | `source` | P1 | | | Load symbols on demand | — | P2 | PDB loading, symbol server | | Decompiled source navigation | — | P2 | ICSharpCode.Decompiler in C# sidecar | | Parallel Stacks data | custom `sharplsp/parallelStacks` | P2 | Phase 5: enumerate all thread stacks | -#### 5.4.1 Async Call Stack Reconstruction +#### Async Call Stack Reconstruction `[DEBUG-FEATURES-STACK-ASYNC]` -This is the most impactful gap in netcoredbg. When code is paused inside an async state machine, the physical call stack only shows the `MoveNext` frame — not the logical chain of `await` continuations. +Inside an async state machine, netcoredbg exposes the physical `MoveNext` frame rather than the logical `await` chain. **Reconstruction algorithm (implemented in C# sidecar, called by DapRouter):** @@ -320,24 +228,11 @@ This is the most impactful gap in netcoredbg. When code is paused inside an asyn 5. C# sidecar walks the continuation chain by reading `_continuation`/`MoveNextRunner` from the `AsyncTaskMethodBuilder._builder` field to find the next logical frame 6. Reconstructed logical frames are injected into the `stackTrace` response before forwarding to the editor -This reconstruction is best-effort: degrades gracefully (shows physical stack unchanged) when compiler-generated fields cannot be resolved. +If compiler-generated fields cannot be resolved, the response retains the physical stack unchanged. **Phase 5 improvement**: Debug Sidecar reads continuation chains directly via `ICorDebugProcess::ReadMemory` without requiring a Roslyn compilation model, making reconstruction faster and more reliable. -#### 5.4.2 F# Async Stack Reconstruction - -F# `async { }` computation expressions and `task { }` resumable state machines require separate handling. - -**F# PDB limitations** (confirmed gaps in F# compiler, tracked in dotnet/fsharp): -- `StateMachineMethod` table not emitted — debugger cannot map `MoveNext` frames to source without extra heuristics (dotnet/fsharp#12000) -- `StateMachineHoistedLocalScopes` table not emitted — hoisted local variable scopes unavailable - -**SharpLsp approach:** -- `task { }` (resumable state machines, F# 6+): use same async stack reconstruction as C# with type name pattern matching adjusted for F# compiler-generated names -- `async { }` (legacy CPS-based): best-effort reconstruction; degrade gracefully to physical stack where continuation chains cannot be followed -- Phase 5: contribute `StateMachineMethod` PDB table emission to dotnet/fsharp, or implement workaround via FCS symbol analysis - -### 5.5 Variables and Inspection +### Variables and Inspection `[DEBUG-FEATURES-VARIABLES]` | Feature | DAP Method | Priority | |---|---|---| @@ -376,15 +271,9 @@ netcoredbg does not render `[DebuggerDisplay]`. DapRouter intercepts `variables` | T3 | Generic type inference in expressions | Fails | Works | | T3 | `dynamic` type evaluation | Fails | Partial | -The Debug Sidecar achieves T3 by delegating expression compilation to the C# sidecar (Roslyn `CSharpScriptCompilation`), receiving compiled IL, loading it as an in-memory assembly into the debuggee, and evaluating via `ICorDebugEval`. This is the same approach as vsdbg. - -**F# discriminated union inspection (Phase 4):** +The Debug Sidecar achieves T3 by delegating `CSharpScriptCompilation` to the C# sidecar, loading the compiled IL into the debuggee, and evaluating it through `ICorDebugEval`. -F# DUs compile to class hierarchies in IL. Without F# semantic knowledge, debuggers show raw compiler-generated fields (`_tag`, `_value`, etc.). SharpLsp addresses this via: -- Phase 4: DapRouter queries FCS sidecar to decode DU case names from the type's compiled representation, rewriting the variable display name to match F# syntax (e.g., `Some(42)`) -- Phase 5: Debug Sidecar calls FCS sidecar for full DU-aware variable formatting - -### 5.6 Exception Handling +### Exception Handling `[DEBUG-FEATURES-EXCEPTIONS]` | Feature | Priority | |---|---| @@ -398,24 +287,9 @@ F# DUs compile to class hierarchies in IL. Without F# semantic knowledge, debugg Configuration via `setExceptionBreakpoints` with `filterOptions` and `exceptionOptions` per the DAP 1.71.0 specification. -### 5.7 Conditional Breakpoints and Logpoints - -**Conditional breakpoints:** +### Hot Reload During Debug `[DEBUG-FEATURES-HOT-RELOAD]` -- C# expression evaluated in the context of the paused frame -- Phase 4: expression passed verbatim to netcoredbg's built-in evaluator (T1/T2 tier — see §5.5) -- Phase 5: expression compiled by Roslyn (C# sidecar) and evaluated via `ICorDebugEval` — full T3 support including LINQ -- Hit condition: `>`, `>=`, `<`, `<=`, `==`, `%` operators against hit counter - -**Logpoints:** - -- Interpolated string with `{expression}` placeholders evaluated in frame context -- Phase 4: DapRouter emulation — conditional breakpoint with `always-continue` semantics (see §5.2) -- Phase 5: native implementation — `ICorDebugBreakpoint` + immediate `ICorDebugEval` + `ICorDebugProcess::Continue`, zero pause visible to user - -### 5.8 Hot Reload During Debug - -Hot Reload allows modifying method bodies at runtime without restarting the debug session. SharpLsp uses `.NET Hot Reload` (`MetadataUpdater.ApplyUpdate`), not legacy Edit and Continue (`ICorDebugModule2::ApplyChanges`). This distinction is critical: +SharpLsp uses `.NET Hot Reload` (`MetadataUpdater.ApplyUpdate`), not legacy Edit and Continue (`ICorDebugModule2::ApplyChanges`): - `MetadataUpdater.ApplyUpdate` is cross-platform (Linux, macOS, Windows) since .NET 6 - Classic EnC via `ICorDebugModule2::ApplyChanges` requires the debugger to generate delta files; no open-source client generates these deltas for Linux/macOS targets (netcoredbg issue #214) @@ -442,9 +316,7 @@ Hot Reload allows modifying method bodies at runtime without restarting the debu | Modify lambda captured variables | No — requires restart | | Change inheritance hierarchy | No — requires restart | -**Note on classic EnC (out of scope):** The .NET 8+ runtime supports EnC on Linux/macOS (dotnet/runtime#12409 closed Sept 2023). However, generating the delta files requires IDE tooling that no open-source project currently provides for non-Windows targets. If this gap is closed upstream, SharpLsp will adopt it. Until then, Hot Reload is the cross-platform path. - -### 5.9 Multi-Process and Multi-Project Debugging +### Multi-Process and Multi-Project Debugging `[DEBUG-FEATURES-MULTIPROCESS]` | Feature | Priority | |---|---| @@ -456,9 +328,9 @@ Hot Reload allows modifying method bodies at runtime without restarting the debu **Implementation:** DapRouter maintains a `DebugSessionRegistry` indexed by session ID. Each session owns an independent adapter process. The editor communicates with multiple sessions via session-ID-prefixed DAP messages. Compound launch configs define multiple named configurations that start simultaneously. -### 5.10 Remote Debugging +### Remote Debugging `[DEBUG-FEATURES-REMOTE]` -SharpLsp manages SSH tunnel setup transparently. The debug adapter always runs locally (against a forwarded socket), avoiding the complexity of cross-machine DAP transport. +SharpLsp creates the SSH tunnel; DapRouter connects to its local forwarded socket. | Step | Action | |---|---| @@ -486,7 +358,7 @@ SharpLsp manages SSH tunnel setup transparently. The debug adapter always runs l } ``` -### 5.11 Test Debugging +### Test Debugging `[DEBUG-FEATURES-TESTS]` | Feature | Protocol | Priority | |---|---|---| @@ -499,9 +371,9 @@ SharpLsp manages SSH tunnel setup transparently. The debug adapter always runs l **Test host process attach**: `dotnet test` spawns a separate test host process (`testhost.exe`/`dotnet-testhost`). SharpLsp must attach to the child test host, not the parent `dotnet test` process. The `VSTEST_HOST_DEBUG=1` environment variable causes the test host to pause and wait for a debugger attach before executing tests. SharpLsp sets this variable in the test debug launch and attaches to the waiting process. -### 5.12 Diagnostic Tools Integration +### Diagnostic Tools Integration `[DEBUG-FEATURES-DIAGNOSTICS]` -Debugging and diagnostics are complementary. SharpLsp integrates the .NET diagnostic tools (all MIT, dotnet/diagnostics v9.0.661903+) alongside the debugger. +SharpLsp exposes dotnet/diagnostics `9.0.661903+` tools through DAP custom messages: | Feature | Tool | DAP Integration | Priority | |---|---|---|---| @@ -512,17 +384,13 @@ Debugging and diagnostics are complementary. SharpLsp integrates the .NET diagno | Process dump on crash | `dotnet-dump` | Auto-triggered on unhandled exception | P3 | | Dump analysis | `dotnet-dump analyze` + SOS | `sharplsp/analyzeDump` custom request | P3 | -These are exposed as DAP custom events/notifications, surfaced in the editor as a diagnostics panel alongside the debugger. See `PROFILER-SPEC.md` for full profiler specification. +The editor presents these events in a diagnostics panel. See [`PROFILER-SPEC.md`](PROFILER-SPEC.md) for profiling behavior. -**Note on musl/Alpine support**: `Microsoft.Diagnostics.NETCore.Client` (the backing library for all diagnostic tools) ships musl/Alpine builds as part of the dotnet/diagnostics release. This is a broader platform support story than netcoredbg. Diagnostic tools work on Alpine even when netcoredbg does not. +`Microsoft.Diagnostics.NETCore.Client` ships musl/Alpine builds, so diagnostic tools MUST remain available there even when netcoredbg cannot start. ---- +## F# Behavior `[DEBUG-FSHARP]` -## 6. F# Debugging: First-Class Status - -F# debugging requires dedicated investment beyond what C# infrastructure provides automatically. - -### 6.1 F# Compiler PDB Gaps +### Compiler PDB Gaps `[DEBUG-FSHARP-PDB]` The F# compiler does not emit the following PDB tables that debuggers rely on: @@ -537,15 +405,15 @@ The F# compiler does not emit the following PDB tables that debuggers rely on: - Phase 4: implement heuristic PDB mapping for F# state machines via FCS sidecar symbol analysis - Phase 5: contribute `StateMachineMethod` table emission to dotnet/fsharp; until accepted, maintain SharpLsp-local patch or workaround -### 6.2 Computation Expression Stepping +### Computation Expression Stepping `[DEBUG-FSHARP-STEPPING]` F# `async { }` desugars into CPS (continuation-passing style) library calls. Stepping behavior reflects the desugared form, not the source. This is documented as a known limitation. -`task { }` (resumable state machines since F# 6) behaves significantly better due to inlining and more predictable PDB mapping. Prefer `task {}` over `async {}` in internal SharpLsp test code. +`task { }` resumable state machines use the C# reconstruction algorithm with F#-specific generated-name matching. Legacy CPS-based `async { }` reconstruction is best-effort and retains the physical stack when its continuation chain cannot be followed. Internal SharpLsp debug tests SHOULD prefer `task { }`. **Smart Step Into (Phase 5)**: Uses DAP `stepIn` with `targetId` to let users choose which function to step into when F# pipelines or function composition calls multiple functions on one line. -### 6.3 Discriminated Union Inspection +### Discriminated Union Inspection `[DEBUG-FSHARP-UNIONS]` DUs compile to class hierarchies. Without F# semantic knowledge, a variable `Some 42` displays as `FSharpOption`1 { Tag = 1, Value = 42 }` instead of `Some(42)`. @@ -554,91 +422,34 @@ SharpLsp addresses this in three layers: 2. **Phase 5 Debug Sidecar**: native DU-aware `variables` formatting via FCS sidecar channel 3. **Longer term**: contribute `[DebuggerDisplay]` attribute emission in F# compiler for DU cases -### 6.4 F# Mailbox Processor Debugging +### Mailbox Processor Inspection `[DEBUG-FSHARP-MAILBOX]` -`MailboxProcessor<'Msg>` actors are a common F# pattern. SharpLsp exposes: +For `MailboxProcessor<'Msg>`, SharpLsp exposes: - Current message queue depth as a pseudo-variable in the variables panel (Phase 5) - Ability to inspect pending messages (Phase 5, best-effort) -### 6.5 F# Expression Evaluation +### Expression Evaluation `[DEBUG-FSHARP-EVALUATION]` -F# expression evaluation in the watch/immediate window: - Phase 4: limited to T1/T2 tier (same as C#; F# syntax not supported — user must use compiled IL names) - Phase 5: route `evaluate` requests to FCS sidecar for F# expression compilation, then evaluate via `ICorDebugEval` ---- - -## 7. Known Gaps and Closure Strategy - -### 7.1 Async Call Stack (Phase 4 partial, Phase 5 complete) +## Gap Closure `[DEBUG-GAPS]` -**Gap:** netcoredbg shows physical call stack only. - -**Closure:** DapRouter + C# sidecar enrichment (§5.4.1). Phase 4 ships best-effort. Phase 5 ships full reconstruction. - -### 7.2 Expression Evaluation (Phase 4 limited, Phase 5 full) - -**Gap:** netcoredbg fails on LINQ, complex lambdas. - -**Closure:** Phase 5 Roslyn ScriptingWorkspace → ICorDebugEval pipeline. - -### 7.3 DebuggerDisplay/TypeProxy (Phase 4 emulated, Phase 5 native) - -**Gap:** netcoredbg does not render `[DebuggerDisplay]`, `[DebuggerTypeProxy]`, or `[DebuggerBrowsable]`. - -**Closure:** Phase 4 DapRouter emulation via C# sidecar evaluation. Phase 5 Debug Sidecar implements natively (same as SharpDbg). - -### 7.4 Process Attach Reliability (Phase 4 improved, Phase 5 fixed) - -**Gap:** netcoredbg `attach` mode returns `0x80070057` error (issue #205). - -**Closure:** SharpLsp contributes fix upstream. DapRouter implements retry with exponential backoff. Phase 5 Debug Sidecar uses `DbgShim.RegisterForRuntimeStartup` for reliable race-free attach. - -### 7.5 macOS ARM64 (Phase 4 fixed, Phase 5 native) - -**Gap:** Samsung does not ship macOS ARM64 binaries for netcoredbg. - -**Closure:** SharpLsp CI builds netcoredbg from source for `darwin-arm64`. Phase 5 Debug Sidecar is managed .NET 9 code — no native compilation issues on ARM64. - -### 7.6 musl/Alpine (Phase 4 worked around, Phase 5 native) - -**Gap:** netcoredbg SIGSEGV on musl due to CoreCLR `EnsureStackSize` overrunning musl's fixed 1.5MB thread stack (dotnet/runtime#103741). This is a CoreCLR bug, not a netcoredbg bug. - -**Closure:** SharpLsp CI maintains a musl-linked netcoredbg build with patched stack size pre-reservation. Contribute fix to dotnet/runtime. Phase 5 Debug Sidecar runs as managed code; the musl issue affects the C++ ICorDebug shim layer, which ClrDebug wraps but does not eliminate. Monitor dotnet/runtime#103741 for upstream fix. - -### 7.7 Logpoints (Phase 4 emulated, Phase 5 native) - -**Gap:** netcoredbg has no logpoint support. - -**Closure:** DapRouter emulation ships in Phase 4. Phase 5 implements native zero-pause logpoints. - -### 7.8 Edit and Continue (cross-platform, Phase 5+) - -**Gap:** .NET 8+ runtime supports EnC on Linux/macOS, but no open-source client generates delta files for these platforms. - -**Closure:** SharpLsp uses Hot Reload (`MetadataUpdater.ApplyUpdate`) which is fully cross-platform. Classic EnC is explicitly out of scope until an upstream open-source delta generator exists. SharpLsp will adopt immediately if/when that gap closes. - -### 7.9 Return Value Display (Phase 5) - -**Gap:** netcoredbg does not show method return values on step-over. - -**Closure:** Phase 5 Debug Sidecar captures return values via `ICorDebugILFrame::GetReturnValueForILOffset` and synthesizes a `returnValue` pseudo-variable in the `variables` response under a dedicated `Return Value` scope (per DAP 1.67+ `returnValue` presentation hint). - -### 7.10 Data Breakpoints (Phase 5) - -**Gap:** netcoredbg does not support data breakpoints. - -**Closure:** Phase 5 Debug Sidecar implements via field value polling on `StepComplete` events or hardware watchpoints via platform-specific APIs where available. - -### 7.11 F# PDB Tables (Phase 4 heuristic, Phase 5 contribution) - -**Gap:** F# compiler does not emit `StateMachineMethod` or `StateMachineHoistedLocalScopes` PDB tables. - -**Closure:** Phase 4 uses FCS sidecar heuristics. Phase 5 contributes PDB table emission to dotnet/fsharp; maintains fallback heuristics indefinitely. - ---- - -## 8. Security Considerations +| Area | Phase Four | Phase Five or later | +|---|---|---| +| Async stacks | Best-effort DapRouter and C# sidecar enrichment per [DEBUG-FEATURES-STACK-ASYNC] | Direct continuation traversal | +| Expression evaluation | netcoredbg T1/T2 | Roslyn `ScriptingWorkspace` to `ICorDebugEval` | +| Debugger attributes | DapRouter emulates `[DebuggerDisplay]` | Native `[DebuggerDisplay]`, `[DebuggerTypeProxy]`, and `[DebuggerBrowsable]` | +| Attach error `0x80070057` | Retry with exponential backoff and contribute issue #205 upstream | Race-free `DbgShim.RegisterForRuntimeStartup` | +| macOS ARM64 | CI-built `darwin-arm64` netcoredbg | Managed sidecar | +| musl/Alpine SIGSEGV | CI build patches stack-size pre-reservation; track dotnet/runtime#103741 | Keep the patch while the wrapped C++ ICorDebug shim remains affected | +| Logpoints | DapRouter evaluate/log/continue emulation | Native zero-visible-pause implementation | +| Cross-platform EnC | Use `MetadataUpdater.ApplyUpdate` Hot Reload | Classic EnC remains out of scope until an open-source delta generator exists | +| Return values | Unavailable | `ICorDebugILFrame::GetReturnValueForILOffset` exposed in a `Return Value` scope with DAP `returnValue` presentation hint | +| Data breakpoints | Unavailable | Field polling on `StepComplete`, or hardware watchpoints where available | +| F# PDB tables | FCS heuristics | Contribute missing tables to dotnet/fsharp and retain fallback heuristics | + +## Security Considerations `[DEBUG-SECURITY]` - The debug adapter runs as the same user as the target process; SharpLsp does not elevate privileges - Remote debugging SSH keys are user-managed; SharpLsp does not store credentials @@ -647,9 +458,7 @@ F# expression evaluation in the watch/immediate window: - `dotnet-dump` output may contain sensitive heap data; SharpLsp stores dumps in user-specified paths only - `ICorDebugEval` expression evaluation executes arbitrary code in the debuggee — scope is limited to the current debug session; no cross-session execution ---- - -## 9. Performance Targets +## Performance Targets `[DEBUG-PERFORMANCE]` | Metric | Target | |---|---| @@ -665,9 +474,7 @@ F# expression evaluation in the watch/immediate window: | Attach to running process | <3s | | DapRouter proxy overhead (added latency) | <5ms per message | ---- - -## 10. Dependencies +## Dependencies `[DEBUG-DEPENDENCIES]` | Dependency | Version | License | Use | |---|---|---|---| @@ -679,27 +486,20 @@ F# expression evaluation in the watch/immediate window: | [FSharp.Compiler.Service](https://www.nuget.org/packages/FSharp.Compiler.Service) | 43.12+ | MIT | F# expression compilation + DU analysis | | DAP specification | 1.71.0 | CC-BY 4.0 | Protocol reference | ---- - -## 11. Reference Documents +## Reference Documents `[DEBUG-REFERENCES]` - [Debug Adapter Protocol Specification 1.71.0](https://microsoft.github.io/debug-adapter-protocol/specification) -- [DAP Changelog](https://microsoft.github.io/debug-adapter-protocol/changelog.html) - [Samsung/netcoredbg — GitHub](https://github.com/Samsung/netcoredbg) -- [netcoredbg Features Wiki](https://github.com/Samsung/netcoredbg/wiki/Features) -- [netcoredbg Issue Tracker](https://github.com/Samsung/netcoredbg/issues) - [ClrDebug — Managed ICorDebug Wrappers](https://github.com/lordmilko/ClrDebug) - [SharpDbg — C# DAP Debugger](https://github.com/MattParkerDev/sharpdbg) - [ICorDebug Interface — Microsoft Learn](https://learn.microsoft.com/en-us/dotnet/core/unmanaged-api/debugging/icordebug/icordebug-interface) - [Microsoft.Diagnostics.DbgShim NuGet](https://www.nuget.org/packages/Microsoft.Diagnostics.DbgShim/) - [.NET Hot Reload — MetadataUpdater](https://learn.microsoft.com/en-us/dotnet/api/system.reflection.metadata.metadataupdater) - [dotnet/diagnostics — GitHub](https://github.com/dotnet/diagnostics) -- [Microsoft.Diagnostics.NETCore.Client docs](https://learn.microsoft.com/en-us/dotnet/core/diagnostics/microsoft-diagnostics-netcore-client) - [F# Debug Emit Guide](https://fsharp.github.io/fsharp-compiler-docs/debug-emit.html) - [dotnet/fsharp#12000 — StateMachineMethod PDB table](https://github.com/dotnet/fsharp/issues/12000) - [dotnet/runtime#103741 — musl SIGSEGV in netcoredbg](https://github.com/dotnet/runtime/issues/103741) - [dotnet/runtime#12409 — Linux EnC support (closed)](https://github.com/dotnet/runtime/issues/12409) - [Samsung/netcoredbg#214 — Cross-platform EnC](https://github.com/Samsung/netcoredbg/issues/214) -- [Samsung/netcoredbg#201 — musl SIGSEGV](https://github.com/Samsung/netcoredbg/issues/201) - [SHARPLSP-SPEC.md](./SHARPLSP-SPEC.md) — parent specification - [PROFILER-SPEC.md](./PROFILER-SPEC.md) — performance profiling specification diff --git a/docs/specs/DEFINITION-SPEC.md b/docs/specs/DEFINITION-SPEC.md index 57ba1860..577da9a2 100644 --- a/docs/specs/DEFINITION-SPEC.md +++ b/docs/specs/DEFINITION-SPEC.md @@ -1,24 +1,16 @@ -# Go to Definition Specification +# Go to Definition Specification `[DEFINITION-NAVIGATION]` **Parent:** [SHARPLSP-SPEC.md](SHARPLSP-SPEC.md) -## 1. Overview +## Overview `[DEFINITION-OVERVIEW]` Go to Definition navigates the user from a symbol usage to its declaration site. SharpLsp implements `textDocument/definition` ([LSP 3.17 §3.17.4](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_definition)), `textDocument/typeDefinition` ([§3.17.7](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_typeDefinition)), `textDocument/declaration` ([§3.17.3](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_declaration)), and `textDocument/implementation` ([§3.17.8](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_implementation)) for both C# and F# as equal first-class citizens. -All four navigation methods are **P0** (launch blocker) and target Phase 2 delivery. +## LSP Protocol `[DEFINITION-PROTOCOL]` -## 2. LSP Protocol +All four methods accept their corresponding `DefinitionParams`, `TypeDefinitionParams`, `DeclarationParams`, or `ImplementationParams`, each containing a `TextDocumentIdentifier` and `Position`. -### 2.1 textDocument/definition - -``` -method: textDocument/definition -params: DefinitionParams { - textDocument: TextDocumentIdentifier - position: Position -} -``` +### `textDocument/definition` `[DEFINITION-PROTOCOL-DEFINITION]` ```typescript result: Definition | DefinitionLink[] | null @@ -38,43 +30,19 @@ interface DefinitionLink { - `DefinitionLink[]` when the server advertises `definitionProvider: { linkSupport: true }` and the client supports it — provides richer origin/target ranges for peek preview. - `null` when no definition can be resolved (unresolved symbol, error recovery). -### 2.2 textDocument/typeDefinition - -``` -method: textDocument/typeDefinition -params: TypeDefinitionParams { - textDocument: TextDocumentIdentifier - position: Position -} -``` +### `textDocument/typeDefinition` `[DEFINITION-PROTOCOL-TYPE]` Same response shape as `textDocument/definition`. Navigates to the type of the symbol at the cursor rather than the symbol itself. For example, on a variable `var x = new Foo()`, go-to-definition navigates to the constructor; go-to-type-definition navigates to `class Foo`. -### 2.3 textDocument/declaration - -``` -method: textDocument/declaration -params: DeclarationParams { - textDocument: TextDocumentIdentifier - position: Position -} -``` +### `textDocument/declaration` `[DEFINITION-PROTOCOL-DECLARATION]` Same response shape. Navigates to the declaration site (interface member, partial declaration, abstract method) rather than the implementation. -### 2.4 textDocument/implementation - -``` -method: textDocument/implementation -params: ImplementationParams { - textDocument: TextDocumentIdentifier - position: Position -} -``` +### `textDocument/implementation` `[DEFINITION-PROTOCOL-IMPLEMENTATION]` Same response shape. Navigates from an interface member or abstract/virtual method to all concrete implementations. Returns `Location[]` when multiple implementations exist. -## 3. Request Routing +## Request Routing `[DEFINITION-ROUTING]` All four definition-family requests are **semantic** requests. The Rust host routes them to the appropriate sidecar based on document language. @@ -88,9 +56,11 @@ All four definition-family requests are **semantic** requests. The Rust host rou The Rust host MAY use tree-sitter to pre-validate the position (e.g., reject whitespace, comments, string literals) and short-circuit with `null` before dispatching to the sidecar. -## 4. C# Implementation (Roslyn) +Implementations: `src/main.rs`, `src/semantic.rs`, and `src/syntax.rs`. + +## C# Implementation (Roslyn) `[DEFINITION-CSHARP]` -### 4.1 textDocument/definition +### `textDocument/definition` `[DEFINITION-CSHARP-DEFINITION]` 1. Obtain `Document` from the current `Solution` snapshot for the given URI. 2. Get the source text and convert `(line, character)` to an absolute position via [`SourceText.Lines.GetPosition()`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.text.textlinecollection.getposition). @@ -101,28 +71,28 @@ The Rust host MAY use tree-sitter to pre-validate the position (e.g., reject whi 7. For each resolved symbol, extract source locations from [`ISymbol.Locations`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.isymbol.locations) where `Location.IsInSource` is true. 8. Map each `Location` back to `(filePath, line, character)` via the location's `SourceSpan` and `SyntaxTree`. -### 4.2 textDocument/typeDefinition +### `textDocument/typeDefinition` `[DEFINITION-CSHARP-TYPE]` 1. Steps 1–4 as above. 2. Get the type via [`SemanticModel.GetTypeInfo()`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.semanticmodel.gettypeinfo). 3. Use `TypeInfo.Type` (or `TypeInfo.ConvertedType` as fallback). -4. Navigate to the type symbol's `Locations` as in §4.1 step 7. +4. Navigate to the type symbol's `Locations` as in step seven of [DEFINITION-CSHARP-DEFINITION]. -### 4.3 textDocument/declaration +### `textDocument/declaration` `[DEFINITION-CSHARP-DECLARATION]` -1. Steps 1–6 as in §4.1. +1. Resolve the symbol through steps one through six of [DEFINITION-CSHARP-DEFINITION]. 2. For the resolved symbol, find the declaration that is an interface member or partial declaration: - If the symbol is an override, navigate to the base virtual/abstract member via `IMethodSymbol.OverriddenMethod` or `IPropertySymbol.OverriddenProperty`. - If the symbol implements an interface member, navigate to the interface member via [`ISymbol.FindImplementationForInterfaceMember()`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.isymbol) (reverse lookup via `INamedTypeSymbol.Interfaces`). - If the symbol is a partial method/class, navigate to the defining partial declaration via [`IMethodSymbol.PartialDefinitionPart`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.imethodsymbol.partialdefinitionpart). -### 4.4 textDocument/implementation +### `textDocument/implementation` `[DEFINITION-CSHARP-IMPLEMENTATION]` -1. Steps 1–6 as in §4.1. +1. Resolve the symbol through steps one through six of [DEFINITION-CSHARP-DEFINITION]. 2. Use [`SymbolFinder.FindImplementationsAsync()`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.findusages.symbolfinder.findimplementationsasync) to find all concrete implementations. 3. Return `Location[]` with one entry per implementation. -### 4.5 Special Cases +### Special Cases `[DEFINITION-CSHARP-CASES]` | Symbol at Cursor | definition | typeDefinition | declaration | implementation | |---|---|---|---|---| @@ -139,16 +109,18 @@ The Rust host MAY use tree-sitter to pre-validate the position (e.g., reject whi | Metadata symbol (no source) | Decompiled source (P1) | Decompiled source (P1) | Same as definition | N/A | | Implicitly declared symbol | Generated source (if available) | Type definition | Same as definition | N/A | -### 4.6 Metadata and Decompiled Source Navigation +### Metadata and Decompiled Source Navigation `[DEFINITION-CSHARP-METADATA]` When a symbol's definition is in metadata (referenced assembly, NuGet package) rather than source: 1. **Phase 2 (MVP):** Return `null` — no navigation for metadata symbols. 2. **Phase 3 (P1):** Use [ICSharpCode.Decompiler](https://github.com/icsharpcode/ILSpy) to decompile the containing type, write it to a temporary file, and return a `Location` pointing to the decompiled source. Use the custom `sharplsp/decompileSource` method to serve decompiled content on demand. -## 5. F# Implementation (FCS) +Implementation: `sidecars/SharpLsp.Sidecar.CSharp/Workspace/WorkspaceManager.cs`. + +## F# Implementation (FCS) `[DEFINITION-FSHARP]` -### 5.1 textDocument/definition +### `textDocument/definition` `[DEFINITION-FSHARP-DEFINITION]` 1. Get `FSharpCheckFileResults` for the document via `FSharpChecker.CheckFileInProject()`. 2. Call [`GetDeclarationLocation(line, col, lineText, names)`](https://fsharp.github.io/fsharp-compiler-docs/) to obtain the declaration location. @@ -157,7 +129,7 @@ When a symbol's definition is in metadata (referenced assembly, NuGet package) r - `FindDeclResult.DeclNotFound(reason)` — return `null`. - `FindDeclResult.ExternalDecl(assembly, fullName)` — decompilation target (Phase 3). -### 5.2 textDocument/typeDefinition +### `textDocument/typeDefinition` `[DEFINITION-FSHARP-TYPE]` 1. Get `FSharpCheckFileResults`. 2. Call `GetSymbolUseAtLocation(line, col, lineText, names)` to obtain `FSharpSymbolUse`. @@ -167,18 +139,18 @@ When a symbol's definition is in metadata (referenced assembly, NuGet package) r - For `FSharpEntity`: use the entity itself. 4. Navigate to the type's declaration range. -### 5.3 textDocument/declaration +### `textDocument/declaration` `[DEFINITION-FSHARP-DECLARATION]` 1. Same as definition for most F# symbols (F# does not have partial classes). 2. For interface implementations, navigate to the interface member declaration. -### 5.4 textDocument/implementation +### `textDocument/implementation` `[DEFINITION-FSHARP-IMPLEMENTATION]` 1. Use `GetSymbolUseAtLocation()` to find the symbol. 2. For abstract members in abstract classes or interfaces, search the project for implementing types. 3. Return `Location[]` for each implementation found. -### 5.5 F#-Specific Cases +### F#-Specific Cases `[DEFINITION-FSHARP-CASES]` | Symbol at Cursor | Behavior | |---|---| @@ -190,9 +162,11 @@ When a symbol's definition is in metadata (referenced assembly, NuGet package) r | Module function | Navigate to the `let` binding | | Pattern binding (`let (x, y) = ...`) | Navigate to the binding site | -## 6. Cross-Language Navigation `[DEFINITION-CROSSLANG]` +Implementation: `sidecars/SharpLsp.Sidecar.FSharp/FSharpWorkspace.fs`. -When a C# project references an F# project (or vice versa), go-to-definition crosses the language boundary. Each engine sees the *other* language only as a compiled assembly — Roslyn's `MSBuildWorkspace` has no F# language service, and FCS does not resolve a `` to a C# project — so the mechanism is **metadata-as-source**: wire the referenced project's built output DLL into the resolving engine, then decompile the target type to a navigable location. This is what mature IDEs do for a compiled cross-language reference, and it needs no cross-sidecar symbol index. +## Cross-Language Navigation `[DEFINITION-CROSSLANG]` + +When a C# project references an F# project or vice versa, each engine sees the other language as a compiled assembly. Cross-language navigation therefore wires the referenced output DLL into the resolving engine and decompiles the target type to a metadata-as-source location; it requires no cross-sidecar symbol index. | Scenario | Approach | |---|---| @@ -201,9 +175,9 @@ When a C# project references an F# project (or vice versa), go-to-definition cro Both directions decompile through the shared `MetadataDecompiler` (`SharpLsp.Sidecar.Common`), and the referenced project must be built (its output DLL must exist) for resolution to succeed. Requirement: navigating from a use site in one language onto a symbol defined in the other resolves to a decompiled metadata-as-source location for that symbol's type. -**Not yet implemented (P2, Phase 4):** *source-to-source* cross-language navigation — landing in the original `.fs`/`.cs` file rather than decompiled metadata — which requires the Rust host to maintain a cross-sidecar symbol index. +Source-to-source cross-language navigation into the original `.fs` or `.cs` file is not implemented; it requires a Rust-host cross-sidecar symbol index. -## 7. Caching Strategy +## Caching Strategy `[DEFINITION-CACHE]` Definition results are cached via the [salsa](https://salsa-rs.github.io/salsa/) incremental computation database in the Rust host. @@ -216,7 +190,7 @@ The `method` component distinguishes between `definition`, `typeDefinition`, `de The Rust host SHOULD cache the most recent result per document per method and return it immediately if the position and version match. Stale requests for superseded document versions MUST be cancelled. -## 8. Performance Requirements +## Performance Requirements `[DEFINITION-PERFORMANCE]` | Metric | Target | Measurement | |---|---|---| @@ -226,7 +200,7 @@ The Rust host SHOULD cache the most recent result per document per method and re | Tree-sitter pre-validation | <1ms | Whitespace/comment/literal rejection | | Find implementations (100 impls) | <500ms | Time to enumerate all implementations | -## 9. Error Handling +## Error Handling `[DEFINITION-ERRORS]` | Condition | Response | |---|---| @@ -235,14 +209,14 @@ The Rust host SHOULD cache the most recent result per document per method and re | Symbol resolution fails | Return `null` | | Symbol is in metadata (no source, Phase 2) | Return `null` | | Symbol is in metadata (Phase 3+) | Return decompiled source location | -| Sidecar crashes during request | Return `null`, trigger crash recovery (see SHARPLSP-SPEC §5) | +| Sidecar crashes during request | Return `null` and trigger [SIDECAR-RECOVERY](SIDECAR-LIFECYCLE-SPEC.md) | | Multiple partial definitions | Return `Location[]` with all partial sites | Definition requests MUST NOT block, hang, or return errors to the client. On any failure, return `null`. -## 10. Wire Types (IPC) +## Wire Types (IPC) `[DEFINITION-IPC]` -### 10.1 Request +### Request `[DEFINITION-IPC-REQUEST]` Reuses `PositionRequest` shared with hover: @@ -256,7 +230,7 @@ public class PositionRequest } ``` -### 10.2 Response +### Response `[DEFINITION-IPC-RESPONSE]` ```csharp [MessagePackObject] @@ -278,7 +252,7 @@ public class LocationListResult } ``` -### 10.3 IPC Methods +### IPC Methods `[DEFINITION-IPC-METHODS]` | IPC Method | LSP Method | Response Type | |---|---|---| @@ -286,21 +260,3 @@ public class LocationListResult | `textDocument/typeDefinition` | `textDocument/typeDefinition` | `LocationResult` or `LocationListResult` | | `textDocument/declaration` | `textDocument/declaration` | `LocationResult` or `LocationListResult` | | `textDocument/implementation` | `textDocument/implementation` | `LocationListResult` | - -## 11. Competitive Parity Matrix - -| Feature | VS | CDK | Rider | SharpLsp Target | Priority | -|---|---|---|---|---|---| -| Go to definition (in-source) | ✓ | ✓ | ✓ | ✓ | P0 | -| Go to definition (metadata) | ✓ | ✓ | ✓ | ✓ | P1 | -| Go to type definition | ✓ | ✓ | ✓ | ✓ | P0 | -| Go to declaration | ✓ | ✓ | ✓ | ✓ | P0 | -| Go to implementation | ✓ | ✓ | ✓ | ✓ | P0 | -| Go to base member | ✓ | ✗ | ✓ | ✓ | P1 | -| Partial class navigation | ✓ | ✓ | ✓ | ✓ | P0 | -| Cross-language (C#↔F#) | ✗ | ✗ | ✓* | ✓ | P2 | -| Decompiled source navigation | ✓ | ✓ | ✓ | ✓ | P1 | -| Source generator output navigation | ✓ | ✓ | ✗ | ✓ | P2 | -| Peek definition (editor-side) | ✓ | ✓ | ✓ | ✓ (via DefinitionLink) | P0 | - -*\* Rider supports both languages but via proprietary code, not LSP.* diff --git a/docs/specs/DESIGN-SYSTEM.md b/docs/specs/DESIGN-SYSTEM.md index 293e9233..44f81e4f 100644 --- a/docs/specs/DESIGN-SYSTEM.md +++ b/docs/specs/DESIGN-SYSTEM.md @@ -1,173 +1,119 @@ -# Design System +# [WEB-DESIGN] Design System -SharpLsp's visual identity and component guidelines. All colors were generated via random color wheel selection — zero hand-picked "designer" colors, zero LLM defaults. +## [WEB-DESIGN-PRINCIPLES] Principles -## Color Palette +- Use strong type hierarchy, whitespace, borders, and restrained surfaces to organize content. +- Use green as the only brand accent. Do not use purple, gradients, glows, decorative noise, or competing accent colors. +- Keep shadows functional and infrequent: menus and major product imagery only. +- Name classes for what an element is, minimize class count, and reuse existing components. +- Store colors and shared dimensions in custom properties. Component rules consume tokens rather than hardcoded colors. -### Generation Method +## [WEB-DESIGN-CSS] CSS Architecture -Primary hue selected by RNG from 0-359 color wheel (excluding 240-330 to avoid purple/magenta). Accent hue offset by a random triadic interval. Neutrals are desaturated tints of the primary. +The site has three CSS layers, loaded in this order: -### Primary — Hue 151 (Teal-Green) +1. `styles.css` — tokens, reset/base rules, navigation, buttons, shared headings, and footer. +2. `pages.css` — homepage, blog index, releases, grids, cards, and page-specific composition. +3. `prose.css` — long-form docs, blog posts, release notes, and documentation navigation. -| Token | Hex | Usage | -|-------|-----|-------| -| `--color-primary-300` | `#84d6ae` | Hover backgrounds, light accents | -| `--color-primary-400` | `#49d491` | Secondary buttons, links on dark bg | -| `--color-primary-500` | `#19d078` | **Primary brand color**, buttons, links | -| `--color-primary-600` | `#14a35e` | Hover state for primary actions | -| `--color-primary-700` | `#0f7f49` | Active/pressed states, dark accents | +Shared primitives belong in `styles.css`; page composition belongs in `pages.css`; rendered Markdown and its supporting article/docs components belong in `prose.css`. Do not duplicate rules across layers. -### Accent — Hue 16 (Burnt Sienna) +## [WEB-DESIGN-COLOR] Color -| Token | Hex | Usage | -|-------|-----|-------| -| `--color-accent-400` | `#c67456` | Hover state for accent elements | -| `--color-accent-500` | `#b54f2a` | **Accent color**, callouts, badges | -| `--color-accent-600` | `#8c3d20` | Hover state for accent actions | +Light and dark themes use the same semantic tokens. `data-theme="dark"` on `` supplies the dark values and `color-scheme` informs browser controls. -### Neutrals +| Token | Light | Dark | Purpose | +|---|---:|---:|---| +| `--color-bg` | `#f6f7f7` | `#0d110f` | Page canvas | +| `--color-surface` | `#ffffff` | `#131916` | Cards and menus | +| `--color-surface-subtle` | `#eef0ef` | `#19211d` | Quiet grouping and hover states | +| `--color-surface-strong` | `#dee1e0` | `#25302a` | Stronger neutral surface | +| `--color-text` | `#161c19` | `#f1f4f2` | Primary text | +| `--color-muted` | `#58625d` | `#aab4af` | Supporting text | +| `--color-soft` | `#78827d` | `#87938d` | De-emphasized text | +| `--color-border` | `#d4dad7` | `#28332d` | Standard dividers | +| `--color-border-strong` | `#aeb8b3` | `#46554d` | Emphasized boundaries | +| `--color-primary` | `#0f7f49` | `#49d491` | Links, focus, labels, primary actions | +| `--color-primary-hover` | `#09663a` | `#84d6ae` | Primary hover state | +| `--color-primary-soft` | `#dcefe5` | `#183a29` | Selected and quiet accent surfaces | +| `--color-on-primary` | `#ffffff` | `#07110b` | Text on primary | +| `--color-code` | `#101613` | `#080c0a` | Code-block surface | +| `--color-code-text` | `#e7ece9` | `#e7ece9` | Code-block text | -| Token | Hex | Usage | -|-------|-----|-------| -| `--color-neutral-50` | `#f6f7f7` | Page background (light) | -| `--color-neutral-100` | `#eef0ef` | Card/surface background (light) | -| `--color-neutral-200` | `#dee1e0` | Borders (light) | -| `--color-neutral-300` | `#c4c9c6` | Disabled text, subtle borders | -| `--color-neutral-400` | `#8b928e` | Muted text, placeholders | -| `--color-neutral-500` | `#6c7370` | Secondary text | -| `--color-neutral-600` | `#48504c` | Body text (dark mode) | -| `--color-neutral-700` | `#2a312e` | Headings (dark mode), borders (dark) | -| `--color-neutral-800` | `#161c19` | Surface background (dark) | -| `--color-neutral-900` | `#0d110f` | Page background (dark) | +## [WEB-DESIGN-TYPE] Typography and Icons -### Semantic +Use system fonts only. The UI stack is `-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif`; the code stack is `"SFMono-Regular", "Cascadia Code", "Liberation Mono", Consolas, monospace`. -| Token | Hex | Usage | -|-------|-----|-------| -| `--color-success` | `#249c64` | Success states, passing tests | -| `--color-warning` | `#e29d12` | Warnings, deprecation notices | -| `--color-error` | `#c72e23` | Errors, breaking changes | -| `--color-info` | `#277cb9` | Informational callouts | +Body copy is `1rem/1.65`. Display headings use responsive `clamp()` sizing, tight negative letter spacing, and compact line height. Long-form prose uses a more relaxed `1.78` line height. -## Typography +Do not request web fonts or external icon fonts. Use existing local assets, text symbols, or small accessible inline SVGs with `currentColor` for interface icons. -### Font Stack +## [WEB-DESIGN-SPACING] Spacing and Shape -```css -font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, - "Helvetica Neue", Arial, sans-serif; -``` +Spacing follows a 4px base scale: -System fonts only. No external font requests. Instant rendering. +| Token | Value | +|---|---:| +| `--space-1` | `0.25rem` | +| `--space-2` | `0.5rem` | +| `--space-3` | `0.75rem` | +| `--space-4` | `1rem` | +| `--space-6` | `1.5rem` | +| `--space-8` | `2rem` | +| `--space-12` | `3rem` | +| `--space-16` | `4rem` | -### Monospace Stack +Radii are `0.35rem`, `0.65rem`, and `1rem` through `--radius-sm`, `--radius-md`, and `--radius-lg`. Prefer borders over elevation. `--shadow-sm` and `--shadow-lg` are neutral shadows, never colored glows. -```css -font-family: "SF Mono", "Cascadia Code", "Fira Code", Consolas, - "Liberation Mono", Menlo, monospace; -``` +## [WEB-DESIGN-LAYOUT] Layout -### Scale +| Context | Token | Limit | +|---|---|---:| +| Main shell | `--max-width` | `1120px` | +| Docs and article prose | `--content-width` | `72ch` | +| Docs sidebar | `--sidebar-width` | `16rem` | +| Site header | `--header-height` | `4rem` | -| Element | Size | Weight | Line Height | -|---------|------|--------|-------------| -| `h1` | 2rem | 700 | 1.2 | -| `h2` | 1.4rem | 700 | 1.3 | -| `h3` | 1.1rem | 600 | 1.4 | -| Body | 1rem | 400 | 1.7 | -| Small / Muted | 0.925rem | 400 | 1.6 | -| Code | 0.875rem | 400 | 1.6 | +Main page sections center within the shell and retain fluid side gutters. Prose stays at the readable measure even when its docs layout has a sidebar. Images are responsive by default, and wide code blocks, diagrams, and tables scroll within their own bounds rather than widening the page. -## Spacing +## [WEB-DESIGN-COMPONENTS] Components -All spacing uses a 4px base unit. Prefer multiples: 4, 8, 12, 16, 24, 32, 48, 64. +Primary and secondary actions use `.button` with `.primary` or `.secondary`; `.nav-button` shares the same control geometry. Controls have a minimum height of `2.75rem` (44px), a visible border or fill, and a clear hover/focus state. -| Token | Value | Usage | -|-------|-------|-------| -| `--space-1` | 0.25rem (4px) | Tight gaps, inline padding | -| `--space-2` | 0.5rem (8px) | Button padding, small gaps | -| `--space-3` | 0.75rem (12px) | Card padding, nav items | -| `--space-4` | 1rem (16px) | Standard spacing | -| `--space-6` | 1.5rem (24px) | Section gaps | -| `--space-8` | 2rem (32px) | Page padding | -| `--space-12` | 3rem (48px) | Section padding | -| `--space-16` | 4rem (64px) | Major section breaks | +Cards use a neutral surface, one-pixel border, restrained radius, and content-driven spacing. Hover may strengthen the border but must not add movement or spectacle. Grids use `minmax(0, 1fr)` where content could otherwise overflow. -## Border Radius +## [WEB-DESIGN-PROSE] Prose Contract -| Token | Value | Usage | -|-------|-------|-------| -| `--radius-sm` | 3px | Code inline, small badges | -| `--radius-md` | 6px | Buttons, inputs | -| `--radius-lg` | 8px | Cards, code blocks | +Every documentation page, blog post, and other long-form rendered body must use the `.prose` wrapper. Markdown typography must never rely on unscoped element selectors. -## Layout +Within `.prose`: -### Max Widths +- headings, paragraphs, lists, links, quotes, media, tables, and code receive the canonical reading styles; +- links are visibly underlined and use `--color-primary`; +- inline code and code blocks remain visually distinct and horizontally safe; +- tables are scrollable, and media never exceeds the content width; +- article callouts, author metadata, related content, and docs navigation use the supporting rules in `prose.css`. -| Context | Width | -|---------|-------| -| Content area | 1100px | -| Blog / prose | 700px | -| Docs sidebar | 240px | +Page-level grids and marketing card styles must not leak into `.prose`. Do not recreate prose styling in templates or `pages.css`. -### Breakpoints +## [WEB-DESIGN-RESPONSIVE] Responsive Behavior -| Name | Width | Behavior | -|------|-------|----------| -| Mobile | ≤768px | Single column, collapsed nav | -| Desktop | >768px | Multi-column, full nav | +Design mobile-first: content order, meaning, and actions must work in a single column without hover. `768px` is the primary responsive boundary; at and below it: -## Components +- navigation becomes an explicit menu with stacked links and actions; +- multi-column workflows, reasons, releases, blog cards, and language sections collapse to one column; +- featured posts return to normal card flow; +- primary action rows stack to full-width controls where needed; +- the docs sidebar becomes an off-canvas panel opened by a full-width menu control; +- prose and shell gutters reduce to `1rem`. -### Buttons +At `380px`, proof items become one column and dense release metadata stacks. New components must remain usable at 320px without horizontal page overflow. -```html -Primary Action -Secondary Action -``` +## [WEB-DESIGN-ACCESSIBILITY] Accessibility -- Primary: `--color-primary-500` background, white text -- Secondary: transparent background, border, current text color -- All buttons: 6px radius, 600 weight, 0.75rem/1.75rem padding - -### Feature Cards - -```html -
-

Title

-

Description

-
-``` - -- 1px border using `--color-border` -- 8px radius -- 1.5rem padding -- Surface background - -### Code Blocks - -- Background: `--color-code-bg` -- 1px border -- 6px radius -- Syntax highlighting via Prism (eleventy-plugin-syntaxhighlight) - -## Dark Mode - -Dark mode is toggled via `data-theme="dark"` on ``. The theme toggle persists to `localStorage`. - -Every color token must have both light and dark values defined in `:root` and `[data-theme="dark"]` respectively. Never hardcode hex values outside CSS custom properties. - -## Favicon - -SVG favicon at `/assets/favicon.svg`. The `site.json` data file drives the `` tag via the theme's `base.njk` template. - -## Rules - -1. **No purple.** Not even a little. Not even "it's more of a violet." No. -2. **No external font/icon CDN requests.** System fonts, inline SVGs. -3. **Name CSS classes after what the element IS**, not what section it's in. -4. **Minimize CSS classes.** Consolidate where possible. -5. All colors via CSS custom properties. Zero hardcoded hex in component styles. -6. Mobile-first: single column is the default, multi-column is the enhancement. +- Preserve semantic HTML, logical heading order, and meaningful link/control labels. +- Preserve the skip link and the `3px` `:focus-visible` outline with a `3px` offset. +- Interactive targets must be at least 44px in the constrained dimension and remain keyboard operable. +- Do not communicate state by color alone. Maintain readable contrast in both themes. +- Decorative SVGs are hidden from assistive technology; meaningful images and icons require accessible text. +- Honor `prefers-reduced-motion`; essential information must never depend on animation or hover. diff --git a/docs/specs/DIAGNOSTICS-SPEC.md b/docs/specs/DIAGNOSTICS-SPEC.md index ae227061..8c01bd11 100644 --- a/docs/specs/DIAGNOSTICS-SPEC.md +++ b/docs/specs/DIAGNOSTICS-SPEC.md @@ -1,10 +1,10 @@ -# DIAGNOSTICS-SPEC +# [DIAG] Diagnostics Specification -Diagnostics are the core feedback loop for developers. SharpLsp must surface all compiler errors, warnings, and analyzer diagnostics across the entire solution in real-time, matching Visual Studio's Solution-Wide Error Analysis (SWEA) from day one — **without ever lying about compilation state**. +SharpLsp MUST surface compiler errors, warnings, and analyzer diagnostics across the solution without reporting stale compilation state. -## 1. Architecture +## [DIAG-ARCHITECTURE] Architecture -SharpLsp uses the **LSP 3.17 pull-diagnostics model with workspace refresh**, mirroring `Microsoft.CodeAnalysis.LanguageServer` (the engine behind C# Dev Kit). This is the only architecture that produces correct diagnostics during workspace load. +SharpLsp uses the LSP 3.17 pull-diagnostics model with workspace refresh. ``` Editor ←→ Rust LSP Host ←→ C#/F# Sidecar (Roslyn / FCS) @@ -15,58 +15,35 @@ Editor ←→ Rust LSP Host ←→ C#/F# Sidecar (Roslyn / FCS) diagnostic←pull ``` -### 1.1 The Pull + Refresh Cycle +### [DIAG-ARCHITECTURE-PULL-REFRESH] Pull and Refresh Cycle -**SharpLsp never proactively asserts diagnostics.** It does not push errors during workspace load, because at that moment Roslyn cannot tell the truth — NuGet may be restoring, source generators are lazy, cross-project `CompilationReference`s are still resolving. Pushing during this window produces phantom CS0246/CS0234 errors that contradict `dotnet build`. SharpLsp does not lie. +SharpLsp MUST NOT push errors during workspace load while NuGet restore, source generators, or cross-project `CompilationReference` resolution is incomplete; doing so can produce phantom CS0246/CS0234 errors. Instead: -1. **Workspace open**: Rust host opens the workspace in the sidecar. Sidecar runs the NuGet restore gate (see §6) BEFORE creating `MSBuildWorkspace`. Once the workspace is created, the sidecar subscribes to `Workspace.RegisterWorkspaceChangedHandler` and seeds a monotonic `global_state_version: u64`. +1. **Workspace open**: Rust host opens the workspace in the sidecar. Sidecar runs [DIAG-RESTORE] before creating `MSBuildWorkspace`. Once the workspace is created, the sidecar subscribes to `Workspace.RegisterWorkspaceChangedHandler` and seeds a monotonic `global_state_version: u64`. 2. **Server advertises pull**: capabilities include `diagnosticProvider.workspaceDiagnostics: true` and `interFileDependencies: true`. 3. **Editor pulls**: editor sends `textDocument/diagnostic` (per file) and/or `workspace/diagnostic` (whole workspace) on its own schedule. Each request includes any `previousResultId` it has cached. 4. **Sidecar answers per-document**: for each pull, the sidecar calls `Project.GetCompilationAsync().GetSemanticModel(tree).GetDiagnostics()` (and `CompilationWithAnalyzers` for analyzer diagnostics) for **just the requested document(s)**. Roslyn's lazy compilation transparently forces topological resolution of the requested project's dependencies. 5. **Result identity**: response carries `resultId = "{project_version}:{doc_version}:{global_state_version}"`. If the editor's `previousResultId` matches, the server returns `DiagnosticReport.Unchanged` (per LSP 3.17) and skips re-computation. 6. **Refresh on change**: any sidecar-side `WorkspaceChanged` event (`ProjectAdded`, `ProjectReloaded`, `SolutionChanged`, `DocumentChanged`, restore completion) bumps `global_state_version` and emits a `diagnostics/refresh` IPC notification. Rust host coalesces these via a 2000ms debounced batch (matching Roslyn LSP's `AsyncBatchingWorkQueue`) and sends LSP `workspace/diagnostic/refresh` to the editor. The editor re-pulls — diagnostics converge to truth. -This is the **only** way to give correct diagnostics during multi-second workspace loads. OmniSharp (event-driven push) and Roslyn LSP (pull + refresh) both refuse to assert correctness at any single instant; they converge via invalidation. SharpLsp does the same. +### [DIAG-ARCHITECTURE-EAGER-SCAN] No Eager Solution Scan -### 1.2 Why no eager solution scan +The server MUST NOT scan `Solution.Projects` eagerly with `GetCompilationAsync()` during load: consumer projects can compile before dependencies become cached `CompilationReference`s, and source generators or restore can still be incomplete. It also MUST NOT simulate a verification pass by sending unchanged text through `textDocument/didChange`; `WithDocumentText` does not rebuild metadata references or generator state. Pull responses report the current snapshot, and a later `global_state_version` bump causes the editor to re-pull. -Earlier versions of this spec described a one-shot solution-wide scan on workspace load, followed by a "verification pass" that re-checked files with errors. **Both have been removed.** They are incompatible with not lying: +### [DIAG-PUSH-GATE] Push Convergence Guarantee -- The eager scan iterates `Solution.Projects` and calls `GetCompilationAsync()` on each. The first compilation a consumer project produces — before its dependencies have been cached as `CompilationReference`s — is missing types and emits phantom CS0246s. Topological iteration only partially mitigates this; source generators and NuGet restore still produce wrong-then-right state transitions during load. -- The verification pass tried to repair stale diagnostics by sending `textDocument/didChange` with the same disk text and re-fetching. Roslyn's `WithDocumentText` creates a new immutable `Solution` snapshot, but it does not re-run source generators, re-resolve NuGet, or rebuild metadata references — the underlying compilation is still incomplete, so the same phantom errors come back. It was a band-aid on the wrong premise. +Editors without pull support receive `textDocument/publishDiagnostics` pushes triggered by `didOpen`/`didChange`. Because a push persists until replaced, the Rust host version-gates every push: -The pull model removes the failure mode entirely: there is no moment at which SharpLsp proactively claims a file has errors. The editor asks; SharpLsp answers with whatever Roslyn currently knows. When Roslyn learns more, the `global_state_version` bumps and the editor re-asks. +1. Each `didOpen`/`didChange`/`didClose` registers a monotonically increasing push generation for the document URI. +2. A completed sidecar fetch publishes only if its generation is still newest; older results are dropped. +3. A failed fetch for the newest generation is retried at 1s intervals with a bounded budget long enough for a sidecar kill and respawn, until it publishes or a newer generation supersedes it. Dropping the fetch could leave the previous publication on screen indefinitely. +4. Generations are never reused after `didClose`, preventing an old in-flight fetch from matching a new document generation. -### 1.3 [DIAG-PUSH-GATE] Push Convergence Guarantee +The last publication for a document MUST reflect its newest known text. -Editors without pull support still receive `textDocument/publishDiagnostics` -pushes triggered by `didOpen`/`didChange`. Because a push asserts state until -the *next* push replaces it, the push pipeline must never let a result for -older text stand as the final published state. The Rust host therefore -version-gates every push: - -1. Each `didOpen`/`didChange`/`didClose` for a document registers a new, - monotonically increasing **push generation** for that URI. -2. A completed sidecar fetch publishes **only if its generation is still the - newest** — a slower fetch for older text is dropped, never published. -3. A **failed** fetch for the newest generation is **retried** (1s interval, - bounded budget that outlasts a sidecar kill + respawn) until it publishes - or a newer generation supersedes it. Dropping it would strand the previous - publication — possibly an error set for text that no longer exists — on - screen forever. (Hardening found while investigating GitHub #160; that - issue's actual root cause was `_._` placeholder references poisoning FCS — - see [PKG-ASSETS-FS](PACKAGE-MAINTENANCE-SPEC.md).) -4. Generations are never reused: reusing a counter after `didClose` would let - an ancient in-flight fetch match a fresh generation and publish stale - results. - -The guarantee: **the last publication for a document always reflects its -newest known text** — phantom diagnostics cannot outlive the edit that -resolved them. - -### 1.4 Analysis Scope +### [DIAG-ARCHITECTURE-SCOPE] Analysis Scope | Mode | Scope | Default | Use Case | |------|-------|---------|----------| @@ -74,9 +51,9 @@ resolved them. | **Open files only** | Editor only pulls `textDocument/diagnostic` for documents it has opened | Optional | Editors that don't issue `workspace/diagnostic` | | **Per-project filter** | `workspace/diagnostic` partial-result handler restricts to filtered projects | Optional | Focus analysis on active development targets | -Solution-wide analysis is the default because developers need to see errors **everywhere**. The C# Dev Kit limitation is not that it lacks SWEA semantically — it serves `workspace/diagnostic` — but that VS Code's UI doesn't surface workspace diagnostics until the file is opened. SharpLsp's VS Code extension explicitly drives the workspace pull and renders results in the Problems panel before files are opened. This is the SWEA win. +Solution-wide analysis is the default. The VS Code extension explicitly drives the workspace pull and renders results in the Problems panel before files are opened. -## 2. Configuration +## [DIAG-CONFIG] Configuration ```toml # sharplsp.toml @@ -115,7 +92,7 @@ refresh_debounce_ms = 2000 auto_restore_on_open = true ``` -### 2.1 Project Filter +### [DIAG-CONFIG-PROJECT-FILTER] Project Filter The `project_filter` field accepts glob patterns matched against project names or relative paths: @@ -127,32 +104,15 @@ project_filter = ["MyApp.Core", "MyApp.Api", "MyApp.Tests.*"] When empty (default), every project in the solution is included. Per-document pulls (`textDocument/diagnostic`) are never filtered — the editor asked for that file specifically, so the server always answers. -### 2.2 Runtime Reconfiguration +### [DIAG-CONFIG-RELOAD] Runtime Reconfiguration Diagnostics settings are hot-reloadable via `workspace/didChangeConfiguration`. Changing `solution_wide_analysis`, `project_filter`, or `min_severity` bumps `global_state_version` and triggers `workspace/diagnostic/refresh` so the editor re-pulls under the new policy. -### 2.3 [ANALYZERS-MONOREPO-GATE] Static Analyzer Monorepo Gate - -SharpLsp-owned static analyzers are specified in -[DIAGNOSTICS-STATIC-ANALYZERS-SPEC.md](DIAGNOSTICS-STATIC-ANALYZERS-SPEC.md). -Unused-public-code analyzers for C# and F# run only when the workspace is -explicitly configured as a monorepo: - -```toml -[workspace] -repository_kind = "monorepo" - -[diagnostics.static_analyzers] -enabled = true -unused_public_symbols = true -``` - -The default `repository_kind` is `"standard"`, which disables unused-public-code -diagnostics even if ordinary compiler/analyzer diagnostics are enabled. +Static analyzer configuration and its monorepo-only gate are specified by [ANALYZERS-MONOREPO-GATE](DIAGNOSTICS-STATIC-ANALYZERS-SPEC.md#analyzers-monorepo-gate). -## 3. Diagnostic Categories +## [DIAG-CATEGORIES] Diagnostic Categories -### 3.1 Compiler Diagnostics (P0) +### [DIAG-CATEGORIES-COMPILER] Compiler Diagnostics | Source | C# (Roslyn) | F# (FCS) | |--------|------------|----------| @@ -161,7 +121,7 @@ diagnostics even if ordinary compiler/analyzer diagnostics are enabled. | Missing references | `CS0246`, `CS0103`, ... | `FS0039`, ... | | Nullable warnings | `CS8600`–`CS8798` | N/A (F# uses `option`) | -### 3.2 Analyzer Diagnostics (P0) +### [DIAG-CATEGORIES-ANALYZER] Analyzer Diagnostics | Source | API | Examples | |--------|-----|----------| @@ -171,25 +131,19 @@ diagnostics even if ordinary compiler/analyzer diagnostics are enabled. | FSharp.Analyzers.SDK | Plugin-based analyzers | Community F# analyzers | | SharpLsp static analyzers | Solution-wide symbol/reference index | Monorepo-only unused public C#/F# code elements | -### 3.3 [ANALYZERS-UNUSED-PUBLIC] Monorepo-Only Unused Public Code +Monorepo-only unused-public-code behavior is specified by [ANALYZERS-UNUSED-PUBLIC](DIAGNOSTICS-STATIC-ANALYZERS-SPEC.md#analyzers-unused-public). -SharpLsp reports unused public C# and F# symbols only when the workspace is -configured as a monorepo. The analyzer is solution-wide, uses compiler symbol -APIs rather than text matching, and reports through `workspace/diagnostic` -partial results. See -[DIAGNOSTICS-STATIC-ANALYZERS-SPEC.md](DIAGNOSTICS-STATIC-ANALYZERS-SPEC.md). +### [DIAG-CATEGORIES-LIVE] Live Squiggles -### 3.4 Live Squiggles (P0) - -Live diagnostics flow through the **pull + refresh cycle** described in §1.1: +Live diagnostics flow through [DIAG-ARCHITECTURE-PULL-REFRESH]: - **On document change**: editor's pull-diagnostic client sends `textDocument/diagnostic` after its own debounce. Sidecar's `LspWorkspaceManager` change handler bumps `global_state_version`, host emits debounced `workspace/diagnostic/refresh`, editor re-pulls anything else that may have been affected by inter-file dependencies. - **On project change**: sidecar's `Workspace.RegisterWorkspaceChangedHandler` fires for `ProjectReloaded` / `ProjectAdded`. Sidecar bumps `global_state_version` and signals `diagnostics/refresh`. - **On workspace load**: NO eager analysis. After NuGet restore + workspace open complete, the sidecar fires `diagnostics/refresh` once. The editor pulls — that pull is the first diagnostic computation, and it is correct because restore has finished. -## 4. LSP Protocol +## [DIAG-LSP] LSP Protocol -### 4.1 Server Capabilities +### [DIAG-LSP-CAPABILITIES] Server Capabilities ```json { @@ -203,7 +157,7 @@ Live diagnostics flow through the **pull + refresh cycle** described in §1.1: `workspaceDiagnostics: true` is mandatory — it is how the editor knows it can ask SharpLsp for solution-wide errors. `identifier: "sharplsp"` lets the editor distinguish SharpLsp's diagnostics from other servers. -### 4.2 Pull Model (PRIMARY: `textDocument/diagnostic`, `workspace/diagnostic`) +### [DIAG-LSP-PULL] Pull Model (`textDocument/diagnostic`, `workspace/diagnostic`) LSP 3.17 pull diagnostics is the **primary** model. The server returns whatever Roslyn currently knows for the requested document(s); it never preemptively asserts. @@ -246,7 +200,7 @@ Per-document request: Workspace request (`workspace/diagnostic`) is supported with partial-result streaming so large solutions don't block on a single response. -### 4.3 Refresh Notifications (`workspace/diagnostic/refresh`) +### [DIAG-LSP-REFRESH] Refresh Notifications (`workspace/diagnostic/refresh`) When sidecar state changes invalidate cached diagnostics, the host sends: @@ -264,13 +218,13 @@ Refresh triggers (sidecar → host IPC notification `diagnostics/refresh` carryi - `.editorconfig` file change inside the solution - Analyzer reference added/removed -### 4.4 Push Model (FALLBACK: `textDocument/publishDiagnostics`) +### [DIAG-LSP-PUSH] Push Model (`textDocument/publishDiagnostics`) Push exists only as a fallback for editors that do not advertise `textDocument.diagnostic.dynamicRegistration` (i.e. older LSP clients that predate 3.17 pull). When push is the only option, the host treats every refresh trigger as a per-document publish, reusing the same per-document analysis pipeline. SharpLsp's VS Code extension always negotiates pull. Push fallback exists for editor coverage (some Vim plugins, older Eclipse JDT-LSP-style clients), not as the canonical path. -### 4.4 Severity Mapping +### [DIAG-LSP-SEVERITY] Severity Mapping | Roslyn Severity | LSP DiagnosticSeverity | |-----------------|----------------------| @@ -279,9 +233,9 @@ SharpLsp's VS Code extension always negotiates pull. Push fallback exists for ed | `Info` | 3 (Information) | | `Hidden` | 4 (Hint) | -## 5. Sidecar IPC Messages +## [DIAG-IPC] Sidecar IPC Messages -### 5.1 Request: `workspace/diagnostics` +### [DIAG-IPC-DOCUMENT-REQUEST] Request: `workspace/diagnostics` Per-document pull. Called by the Rust host in response to LSP `textDocument/diagnostic`. @@ -296,9 +250,9 @@ class DiagnosticsRequest } ``` -Response: `DiagnosticResult[]` (see §5.2) plus `ResultId` and a `Changed` flag. When `Changed = false`, the items array is empty and the host returns `{ kind: "unchanged" }` to the editor. +Response: `DiagnosticResult[]` (see [DIAG-IPC-DOCUMENT-RESPONSE]) plus `ResultId` and a `Changed` flag. When `Changed = false`, the items array is empty and the host returns `{ kind: "unchanged" }` to the editor. -### 5.2 Response: `DiagnosticResult[]` +### [DIAG-IPC-DOCUMENT-RESPONSE] Response: `DiagnosticResult[]` ```csharp [MessagePackObject] @@ -315,13 +269,13 @@ class DiagnosticResult } ``` -### 5.3 Workspace Pull: `workspace/diagnostics/pull` +### [DIAG-IPC-WORKSPACE-PULL] Workspace Pull: `workspace/diagnostics/pull` Called by the Rust host in response to LSP `workspace/diagnostic`. The sidecar streams per-document results (one `WorkspaceDocumentDiagnosticReport` per document) so the editor sees results progressively. Results omit unchanged documents (matching `DiagnosticReport.Unchanged` semantics). -The legacy `workspace/diagnostics/all` bulk RPC has been **removed**. It eagerly iterated every project and ran `GetCompilationAsync` synchronously, producing the phantom CS0246s described in §1.2. There is no replacement — workspace-wide analysis happens lazily via per-document pulls. +The legacy `workspace/diagnostics/all` bulk RPC MUST NOT be restored; workspace-wide analysis happens lazily through per-document pulls as specified by [DIAG-ARCHITECTURE-EAGER-SCAN]. -### 5.4 Notification: `diagnostics/refresh` +### [DIAG-IPC-REFRESH] Notification: `diagnostics/refresh` Sidecar → host notification fired when any input invalidates cached diagnostics. Payload: @@ -336,22 +290,22 @@ class RefreshNotification The host coalesces refreshes via a 2000ms debounced batch and emits LSP `workspace/diagnostic/refresh`. -### 5.5 Notification: `workspace/initializationComplete` +### [DIAG-IPC-INITIALIZED] Notification: `workspace/initializationComplete` Sidecar → host notification fired exactly once after NuGet restore + `MSBuildWorkspace.OpenSolutionAsync` complete. The host forwards as the LSP custom notification `workspace/projectInitializationComplete` (matching `Microsoft.CodeAnalysis.LanguageServer`'s contract). Editors use this to dismiss "Loading projects…" UI. -## 6. NuGet Restore Gate +## [DIAG-RESTORE] NuGet Restore Gate -Phantom CS0246 for NuGet types is the most common false-positive class. SharpLsp mirrors `Microsoft.CodeAnalysis.LanguageServer.HostWorkspace.ProjectDependencyHelper`: +Before workspace creation, SharpLsp applies this restore gate: 1. Before calling `MSBuildWorkspace.OpenSolutionAsync`, the sidecar inspects each project's `obj/project.assets.json`. 2. If `assets.json` is missing, older than the `.csproj`, or its `PackageReference` set differs from the `.csproj`, the sidecar shells `dotnet restore ` via a `DotnetCliHelper` equivalent. Restore progress is reported via LSP `$/progress` (work-done token established at workspace open). 3. Only after restore completes does the sidecar create `MSBuildWorkspace`. 4. Restore completion bumps `global_state_version` and triggers an initial `diagnostics/refresh`. -Without this gate, the editor's first pull happens against a workspace with unresolved `` items, producing CS0246/CS0234 for every NuGet type. The gate is non-optional — `dotnet restore` may take several seconds, but the alternative is a lie. +The gate is mandatory because unresolved `` items can produce CS0246/CS0234 diagnostics on the first pull. -## 7. Performance Targets +## [DIAG-PERFORMANCE] Performance Targets | Metric | Target | |--------|--------| @@ -364,51 +318,27 @@ Without this gate, the editor's first pull happens against a workspace with unre | NuGet restore (cold) | bounded only by `dotnet restore` itself; surface via `$/progress` | | Memory overhead (per-document caching) | <200MB additional for 50-project solution | -## 8. Competitive Analysis - -**Legend:** VS = Visual Studio, CDK = C# Dev Kit, R = Rider. ✓ = the tool has this feature. - -| Feature | VS | CDK | R | SharpLsp | Priority | Phase | -|---|---|---|---|---|---|---| -| Compiler errors and warnings | ✓ | ✓ | ✓ | **P0** | P0 | 2 | -| Roslyn analyzer diagnostics | ✓ | ✓ | ✓ | **P0** | P0 | 2 | -| Solution-wide error analysis (SWEA) | ✓ | ✗ | ✓ | **P0 (default on)** | P0 | 2 | -| Unused using/open detection | ✓ | ✓ | ✓ | **P0** | P0 | 2 | -| Monorepo-only unused public code detection | ✗ | ✗ | ✓ | **P0** | P0 | 4 | -| Nullable reference analysis | ✓ | ✓ | ✓ | **P1** | P1 | 3 | -| Code style enforcement (.editorconfig) | ✓ | ✓ | ✓ | **P1** | P1 | 3 | -| Third-party NuGet analyzers | ✓ | ✓ | ✓ | **P1** | P1 | 4 | -| FSharp.Analyzers.SDK support | ✗ | ✗ | ✗ | **P1** | P1 | 4 | -| Code metrics (cyclomatic complexity) | ✓ | ✗ | ✓ | **P2** | P2 | 4 | -| Value tracking / data flow | ✓ | ✗ | ✓ | **P2** | P2 | 4 | -| IL inspection / viewer | ✓ | ✗ | ✓ | **P3** | P3 | 5 | -| Heap allocation viewer | ✗ | ✗ | ✓ | **P3** | P3 | 5 | - -Key differentiators: - -- **SWEA surfaced in Problems panel without opening files.** C# Dev Kit's underlying server (`Microsoft.CodeAnalysis.LanguageServer`) implements `workspace/diagnostic` correctly — the gap is the VS Code extension UX, which doesn't drive the workspace pull. SharpLsp's extension does, so SWEA actually works for the user. -- **Pull + refresh from day one.** SharpLsp ships LSP 3.17 pull diagnostics as the primary path. OmniSharp uses event-driven push (correct semantics, but every editor sees the convergence flicker). SharpLsp uses pull, so editors with cached `previousResultId`s avoid the flicker entirely. -- **No phantom errors.** SharpLsp's NuGet restore gate (§6) and pull-only model (§1.1) eliminate the false-positive class that haunts every other LSP-based .NET tool. +## [DIAG-SCOPE] Supported Scope -## 9. Background Analysis Strategy +This specification covers compiler errors and warnings, Roslyn and F# analyzer diagnostics, solution-wide analysis, unused using/open detection, nullable analysis, `.editorconfig`, third-party NuGet analyzers, and monorepo-gated unused public code. Code metrics, value tracking, IL inspection, and heap-allocation viewing are outside this specification. -### 9.1 Pull-driven, lazy by construction +## [DIAG-ANALYSIS] Background Analysis Strategy -There is no background scan thread. Roslyn analysis happens **only when the editor pulls**. The `Microsoft.CodeAnalysis.LanguageServer` model proves this is sufficient: editors pull aggressively for visible documents, lazily for the rest, and the server amortizes computation across pulls. Adding a background scanner on top would either duplicate work or race with pulls. +### [DIAG-ANALYSIS-PULL] Pull-Driven Analysis -What replaces the old "background scan": +There is no background scan thread. Roslyn analysis happens only when the editor pulls: - **Lazy compilation**: `Project.GetCompilationAsync()` is invoked on demand for the project of the document being pulled. Roslyn topologically resolves and caches dependency compilations as `CompilationReference`s. Subsequent pulls within the same `Solution` snapshot reuse the cache — the second pull on any file in the same project completes in milliseconds. -- **Caching by `resultId`**: per §4.2, repeat pulls for unchanged documents return `{ kind: "unchanged" }` without re-running Roslyn. The cache key includes `global_state_version`, so any workspace mutation invalidates the entire cache atomically. +- **Caching by `resultId`**: per [DIAG-LSP-PULL], repeat pulls for unchanged documents return `{ kind: "unchanged" }` without re-running Roslyn. The cache key includes `global_state_version`, so any workspace mutation invalidates the entire cache atomically. - **Workspace event subscription**: the sidecar's `Workspace.RegisterWorkspaceChangedHandler` is the only active background work. It mutates `global_state_version` and emits `diagnostics/refresh`. It does not analyze anything itself. -### 9.2 Cancellation +### [DIAG-ANALYSIS-CANCELLATION] Cancellation - The Rust host cancels in-flight per-document IPC requests when the editor sends a fresh pull for the same document with a higher `previousResultId`-implied version (or a different `previousResultId`). - The sidecar passes the IPC `CancellationToken` straight into `GetSemanticModelAsync` / `GetAnalyzerSemanticDiagnosticsAsync`. - A `WorkspaceChanged` event mid-pull does not cancel the pull. The pull completes against its snapshot, returns its `resultId`, and the bumped `global_state_version` causes the next refresh to invalidate it. This matches `AbstractPullDiagnosticHandler`'s snapshot-isolation behavior in `dotnet/roslyn`. -### 9.3 Incremental updates +### [DIAG-ANALYSIS-INCREMENTAL] Incremental Updates When a file changes: @@ -416,23 +346,15 @@ When a file changes: - The host's debounced refresh queue collapses bursts; the LSP `workspace/diagnostic/refresh` notification fires once per debounce window. - The editor re-pulls. Files unaffected by the change return `{ kind: "unchanged" }` cheaply because their `resultId` (which incorporates project version) hasn't moved. -Roslyn's `Compilation` is immutable — there is no incremental analyzer state to manage on our side. Roslyn handles fork-and-cache internally. +## [DIAG-TRUTH] Truth Guarantees -## 10. Truth Guarantees (No False Positives) - -**SharpLsp does not lie.** Every diagnostic shown to the developer must reflect Roslyn's current best understanding of the workspace. - -### 10.1 What we promise +### [DIAG-TRUTH-GUARANTEES] Guarantees - If `dotnet build` succeeds with zero errors against the same source, the next pull (after refresh debounce + restore completion) returns zero Error-severity diagnostics. - A diagnostic in the Problems panel corresponds to a real Roslyn compiler or analyzer diagnostic from the current `Solution` snapshot. - A workspace mutation that changes a file's diagnostics produces an LSP `workspace/diagnostic/refresh` within 2000ms (the debounce window). Editors converge to truth one pull cycle after that. -### 10.2 What we do not promise +### [DIAG-TRUTH-LIMITS] Limits -- We do not promise that the **first** pull during workspace load is complete. NuGet restore may still be running for some projects; source generators may not yet have produced output. The pull will return whatever Roslyn knows at that instant — which after the §6 restore gate is correct for project-reference and NuGet types, but may be missing generator output. +- The first pull during workspace load may be incomplete. The response reflects the current snapshot; after [DIAG-RESTORE], project-reference and NuGet types are resolved, but source-generator output may still be missing. - The remedy for "incomplete but not wrong" is `workspace/diagnostic/refresh`. Generator output materializing fires a `WorkspaceChanged` event → refresh → re-pull → complete result. - -### 10.3 Why the previous "verification pass" is gone - -Earlier revisions of this spec mandated a low-priority verification pass that re-checked files with errors and cleared false positives. **It has been deleted.** The pass was based on a wrong premise: it assumed re-sending `textDocument/didChange` with the same disk text would cause Roslyn to re-resolve missing references. It does not — `Solution.WithDocumentText` invalidates only the per-document syntax tree, not the metadata-reference graph or the generator-driver state. The pass therefore re-fetched the same phantom errors. The pull + refresh model removes the pass's reason to exist: SharpLsp no longer asserts diagnostics until the editor pulls, so there is nothing stale to repair. diff --git a/docs/specs/DIAGNOSTICS-STATIC-ANALYZERS-SPEC.md b/docs/specs/DIAGNOSTICS-STATIC-ANALYZERS-SPEC.md index 1ae7f751..4d794506 100644 --- a/docs/specs/DIAGNOSTICS-STATIC-ANALYZERS-SPEC.md +++ b/docs/specs/DIAGNOSTICS-STATIC-ANALYZERS-SPEC.md @@ -1,30 +1,16 @@ -# DIAGNOSTICS-STATIC-ANALYZERS-SPEC +# Static Analyzer Specification `[ANALYZERS-STATIC]` -SharpLsp-owned static analyzers fill gaps left by compiler diagnostics, Roslyn -IDE analyzers, FSharpLint, and third-party analyzer packages. They are part of -the diagnostics pipeline, but they are solution-wide by design: an analyzer that -needs repository context must never pretend that an open-file-only answer is -complete. +SharpLsp-owned analyzers run through the diagnostics pipeline. Repository-aware analyzers MUST use the loaded solution graph, never present open-file analysis as complete. ## [ANALYZERS-GOAL] Goal -The first SharpLsp static analyzers detect unused public code elements in C# and -F# at the configured solution/repository boundary. +The analyzers detect unused public C# and F# elements at the configured solution boundary. In monorepo mode, public symbols with no references in the loaded graph can be reported because the repository is the declared API boundary. -These diagnostics exist because public code is normally treated as externally -reachable by compilers and ordinary IDE analyzers. In a monorepo, the repository -can be the real API boundary, so SharpLsp can report public surface area that is -not referenced anywhere in the loaded solution graph. - -The feature is opt-in for monorepos only. Standard repositories must not receive -unused-public-code diagnostics, because a public symbol may be consumed by an -unloaded external product, package consumer, plugin, or runtime integration. +Standard repositories MUST NOT receive unused-public diagnostics because unloaded external consumers may exist. ## [ANALYZERS-MONOREPO-GATE] Monorepo Gate -SharpLsp classifies the workspace from explicit configuration, never from -directory shape, project count, Git remotes, naming conventions, or solution -size. +SharpLsp classifies the workspace from explicit configuration, never from directory shape, project count, Git remotes, naming conventions, or solution size. ```toml [workspace] @@ -48,21 +34,13 @@ The unused-public-code analyzers run only when all of these are true: - `diagnostics.static_analyzers.enabled == true` - `diagnostics.static_analyzers.unused_public_symbols == true` -The default `repository_kind` is `"standard"`. This means the analyzer is off by -default even though `diagnostics.analyzers_enabled` remains on by default for -ordinary compiler and package analyzer diagnostics. +`repository_kind` defaults to `"standard"`, so unused-public analysis is off while ordinary compiler and package analyzers remain enabled. -Changing the monorepo gate or static analyzer settings via -`workspace/didChangeConfiguration` bumps `global_state_version` and triggers -`workspace/diagnostic/refresh`. +Changing the monorepo gate or static analyzer settings via `workspace/didChangeConfiguration` bumps `global_state_version` and triggers `workspace/diagnostic/refresh`. ### [ANALYZERS-CONFIG-IMPL] Implemented Configuration (F#) -The F# sidecar gate is live. The Rust host reads an `[analyzers]` table from -`sharplsp.toml` and pushes the flags to each sidecar via the `analyzers/configure` -request immediately after `workspace/open` (see -[config.rs](../../src/config.rs) `AnalyzersConfig` and -[main.rs](../../src/main.rs) `configure_analyzers`): +The F# sidecar gate is live. The Rust host reads an `[analyzers]` table from `sharplsp.toml` and pushes the flags to each sidecar via the `analyzers/configure` request immediately after `workspace/open` (see [config.rs](../../src/config.rs) `AnalyzersConfig` and [main.rs](../../src/main.rs) `configure_analyzers`): ```toml [analyzers] @@ -74,57 +52,34 @@ dead_code = true monorepo = false ``` -`analyzers/configure` carries a positional MessagePack payload -(`AnalyzerConfigRequest`: `[Key(0)] DeadCode`, `[Key(1)] Monorepo`). A sidecar -keeps the flags as mutable state across re-opens. This `[analyzers]` table is the -shipping schema; the richer `[workspace] repository_kind` / `[diagnostics.static_analyzers]` -form above is the forward-compatible target the loader will also accept. +`analyzers/configure` carries a positional MessagePack payload (`AnalyzerConfigRequest`: `[Key(0)] DeadCode`, `[Key(1)] Monorepo`). A sidecar keeps the flags as mutable state across re-opens. This `[analyzers]` table is the shipping schema; the richer `[workspace] repository_kind` / `[diagnostics.static_analyzers]` form above is the forward-compatible target the loader will also accept. ## [ANALYZERS-SOLUTION-SCOPE] Solution-Wide Scope -Static analyzer diagnostics are IDE-level workspace diagnostics. They are -computed from the complete loaded solution graph and surfaced through -`workspace/diagnostic` partial results. +Static analyzer diagnostics are IDE-level workspace diagnostics. They are computed from the complete loaded solution graph and surfaced through `workspace/diagnostic` partial results. -`textDocument/diagnostic` may include cached static analyzer diagnostics for the -requested file after a solution-wide snapshot has been computed. It must not -start a local-only unused-public-code analysis, because that would create false -positives for symbols referenced outside the open document. +`textDocument/diagnostic` may include cached static analyzer diagnostics for the requested file after a solution-wide snapshot has been computed. It must not start a local-only unused-public-code analysis, because that would create false positives for symbols referenced outside the open document. -The initial implementation scope is every C# and F# project loaded from the -configured `.sln` or `.slnx`. If SharpLsp later supports multi-solution -workspaces, the analysis universe becomes every loaded project in the configured -workspace solution set. +The analysis scope is every loaded C# and F# project in the configured `.sln` or `.slnx`. ## [ANALYZERS-UNUSED-PUBLIC] Unused Public Code Elements -A public code element is unused when it has a declaration in the loaded -solution graph and no non-declaration semantic references anywhere in that same -graph. +A public code element is unused when it has a declaration in the loaded solution graph and no non-declaration semantic references anywhere in that same graph. -Declaration candidates are collected from compiler symbol APIs, not string -matching: +Declaration candidates are collected from compiler symbol APIs, not string matching: | Language | Candidate symbols | |---|---| | C# | Public named types, delegates, enums, records, interfaces, constructors, methods, properties, indexers, events, fields, operators, conversion operators, and extension methods | | F# | Public modules, types, union cases, record fields, values/functions, members, active patterns, delegates, interfaces, and members exposed through `.fsi` signature files | -For C#, "public" means symbols whose Roslyn accessibility makes them callable -from another assembly, including public members and protected/protected-internal -members on externally visible inheritable types. +For C#, "public" means symbols whose Roslyn accessibility makes them callable from another assembly, including public members and protected/protected-internal members on externally visible inheritable types. -For F#, implicit public accessibility counts as public unless the declaration is -hidden by `private`, `internal`, a signature file, or compiler visibility rules. -When a `.fsi` signature file exists, the signature file defines the public -surface and diagnostics are reported at the signature declaration when possible. +For F#, implicit public accessibility counts as public unless the declaration is hidden by `private`, `internal`, a signature file, or compiler visibility rules. When a `.fsi` signature file exists, the signature file defines the public surface and diagnostics are reported at the signature declaration when possible. -When an enclosing public type/module is already reported unused, nested public -members are suppressed in that diagnostic batch to avoid noisy cascades. +When an enclosing public type/module is already reported unused, nested public members are suppressed in that diagnostic batch to avoid noisy cascades. -The Rust tree-sitter indexes may prefilter declaration ranges and file scopes for -speed, but Roslyn/FCS symbol identity is the source of truth for every reported -diagnostic. +The Rust tree-sitter indexes may prefilter declaration ranges and file scopes for speed, but Roslyn/FCS symbol identity is the source of truth for every reported diagnostic. ## [ANALYZERS-REFERENCE-MODEL] Reference Model @@ -132,23 +87,17 @@ References must be semantic references: - C# uses Roslyn symbols and `SymbolFinder.FindReferencesAsync`. - F# uses FSharp.Compiler.Service parse/check results and symbol-use APIs. -- Cross-language references through project references are counted by metadata - identity where Roslyn and FCS expose a stable assembly/type/member identity. -- Generated code, `obj/`, `bin/`, package cache files, and metadata-only - assemblies are not diagnostic targets. +- Cross-language references through project references are counted by metadata identity where Roslyn and FCS expose a stable assembly/type/member identity. +- Generated code, `obj/`, `bin/`, package cache files, and metadata-only assemblies are not diagnostic targets. The following count as uses: -- Construction, invocation, member access, field/property/event access, and - delegate conversion. -- Inheritance, interface implementation, override binding, and attribute - application. -- Pattern matching, union-case construction, record construction/update, and - active-pattern use in F#. +- Construction, invocation, member access, field/property/event access, and delegate conversion. +- Inheritance, interface implementation, override binding, and attribute application. +- Pattern matching, union-case construction, record construction/update, and active-pattern use in F#. - References from test projects in the loaded solution. -Declaration syntax, XML documentation text, comments, and unbound identifier text -do not count as uses. +Declaration syntax, XML documentation text, comments, and unbound identifier text do not count as uses. ## [ANALYZERS-SUPPRESSION] Suppression And Known Entry Points @@ -156,24 +105,16 @@ The analyzer must support normal IDE suppression mechanisms: - `.editorconfig` severity for the SharpLsp diagnostic code. - C# `#pragma warning disable` and `SuppressMessageAttribute`. -- F# `#nowarn` for the SharpLsp diagnostic code where supported by the F# sidecar - mapping. +- F# `#nowarn` for the SharpLsp diagnostic code where supported by the F# sidecar mapping. - SharpLsp config entries for project/path exclusions. -The analyzer must also avoid known entry points and convention-bound public -surface: +The analyzer must also avoid known entry points and convention-bound public surface: -- Program entry points, top-level program artifacts, source-generated entry - points, and test framework entry points. -- Overrides and interface implementations when the base/interface contract is - outside the loaded repo graph. -- Symbols annotated with recognized framework/reflection preservation attributes - such as `DynamicallyAccessedMembers`, `DynamicDependency`, `JsonConstructor`, - dependency injection attributes, routing attributes, serializer attributes, or - JetBrains `PublicAPI`/`UsedImplicitly`. +- Program entry points, top-level program artifacts, source-generated entry points, and test framework entry points. +- Overrides and interface implementations when the base/interface contract is outside the loaded repo graph. +- Symbols annotated with recognized framework/reflection preservation attributes such as `DynamicallyAccessedMembers`, `DynamicDependency`, `JsonConstructor`, dependency injection attributes, routing attributes, serializer attributes, or JetBrains `PublicAPI`/`UsedImplicitly`. -The attribute list is configurable so teams can add framework-specific public -entry points without changing SharpLsp. +The attribute list is configurable so teams can add framework-specific public entry points without changing SharpLsp. ## [ANALYZERS-DIAGNOSTICS] Diagnostic Shape @@ -192,9 +133,7 @@ Public {kind} '{symbol}' has no references in the configured monorepo. ### [ANALYZERS-DEADCODE-SEVERITY] Severity (implemented, F#) -By project decision the F# dead-code analyzer (`SLSPF0101`) escalates severity in -monorepo mode — an unreferenced symbol in a declared monorepo is a hard error, not -a hint, because nothing outside the repo can be the missing consumer: +By project decision the F# dead-code analyzer (`SLSPF0101`) escalates severity in monorepo mode — an unreferenced symbol in a declared monorepo is a hard error, not a hint, because nothing outside the repo can be the missing consumer: | Mode | Private/internal dead code | Public dead code | |---|---|---| @@ -207,9 +146,7 @@ outside its assembly, so its deadness is sound without the monorepo gate. ### [ANALYZERS-FSAC-PARITY] File-Local Analyzers (F#, FSAC parity) -The F# sidecar also runs two always-on file-local analyzers via FCS -`EditorServices`, surfaced as `Hint` diagnostics so editors grey the range and can -offer the matching code fix (parity with FsAutoComplete / Ionide): +The F# sidecar also runs two always-on file-local analyzers via FCS `EditorServices`, surfaced as `Hint` diagnostics so editors grey the range and can offer the matching code fix (parity with FsAutoComplete / Ionide): | Code | Source rule | Message | |---|---|---| @@ -218,49 +155,25 @@ offer the matching code fix (parity with FsAutoComplete / Ionide): These are independent of the monorepo gate and the `dead_code` flag. -Diagnostics include a stable symbol identity in `Diagnostic.data` so future code -actions can offer safe-delete, visibility reduction, or suppression insertion. +Diagnostics include stable symbol identity in `Diagnostic.data`. -The raw FCS findings (`open` ranges and `(range, relativeName)` simplifications) -are computed once in [FSharpLocalAnalysis.fs](../../sidecars/SharpLsp.Sidecar.FSharp/FSharpLocalAnalysis.fs) -(`getFileAnalyzerFindings`) and shared by both the hint producer above and the -code fixes below, so the greyed range and the offered fix can never disagree. +The raw FCS findings (`open` ranges and `(range, relativeName)` simplifications) are computed once in [FSharpLocalAnalysis.fs](../../sidecars/SharpLsp.Sidecar.FSharp/FSharpLocalAnalysis.fs) (`getFileAnalyzerFindings`) and shared by both the hint producer above and the code fixes below, so the greyed range and the offered fix can never disagree. #### [FS-CODEFIX-UNUSEDOPEN] "Remove unused open" code fix -The `textDocument/codeAction` handler turns each `SLSPF0102` finding overlapping -the request range into a `Remove unused open` quick fix -([FSharpCodeFixes.fs](../../sidecars/SharpLsp.Sidecar.FSharp/FSharpCodeFixes.fs) -`removeUnusedOpenActions`). Resolving it deletes the whole `open` line — from the -start of its first line through the start of the line after its last — matching -FsAutoComplete. E2E: the F# sidecar IPC suite (`code action offers -remove-unused-open …`) and the VSIX suite (`F# LSP — Code Fixes`). +The `textDocument/codeAction` handler turns each `SLSPF0102` finding overlapping the request range into a `Remove unused open` quick fix ([FSharpCodeFixes.fs](../../sidecars/SharpLsp.Sidecar.FSharp/FSharpCodeFixes.fs) `removeUnusedOpenActions`). Resolving it deletes the whole `open` line — from the start of its first line through the start of the line after its last — matching FsAutoComplete. E2E: the F# sidecar IPC suite (`code action offers remove-unused-open …`) and the VSIX suite (`F# LSP — Code Fixes`). #### [FS-CODEFIX-SIMPLIFYNAME] "Simplify name" code fix -Each `SLSPF0103` finding overlapping the request range becomes a `Simplify name` -quick fix (`simplifyNameActions`). FCS reports the simplifiable `Range` as the -**redundant qualifier prefix including its trailing dot**, so the fix deletes that -span (e.g. `System.DateTime.MinValue` → `DateTime.MinValue` when `System` is open). -E2E: the IPC suite (`code action offers simplify-name …`) and the VSIX suite. +Each `SLSPF0103` finding overlapping the request range becomes a `Simplify name` quick fix (`simplifyNameActions`). FCS reports the simplifiable `Range` as the **redundant qualifier prefix including its trailing dot**, so the fix deletes that span (e.g. `System.DateTime.MinValue` → `DateTime.MinValue` when `System` is open). E2E: the IPC suite (`code action offers simplify-name …`) and the VSIX suite. #### [FS-CODEFIX-INTERFACESTUB] "Implement interface" code fix -A type-informed (not analyzer-driven) code action that completes the F# stub trio — -union cases, record fields, **interface members**. When the cursor is on an -`interface IFoo …` declaration with unimplemented members, FCS -`InterfaceStubGenerator` (`TryFindInterfaceDeclaration` → `GetImplementedMemberSignatures` -→ `FormatInterface`) generates `member _.X … = failwith "…"` stubs for the missing -members ([FSharpCodeActions.fs](../../sidecars/SharpLsp.Sidecar.FSharp/FSharpCodeActions.fs) -`tryGenerateInterfaceStub`, wired into `getCodeActions` Phase 4). E2E: the IPC suite -(`code action offers implement-interface stub …`) and the VSIX suite -(`F# LSP — Implement Interface`). +A type-informed code action handles missing **interface members**. When the cursor is on an `interface IFoo …` declaration with unimplemented members, FCS `InterfaceStubGenerator` (`TryFindInterfaceDeclaration` → `GetImplementedMemberSignatures` → `FormatInterface`) generates `member _.X … = failwith "…"` stubs for the missing members ([FSharpCodeActions.fs](../../sidecars/SharpLsp.Sidecar.FSharp/FSharpCodeActions.fs) `tryGenerateInterfaceStub`, wired into `getCodeActions` Phase 4). E2E: the IPC suite (`code action offers implement-interface stub …`) and the VSIX suite (`F# LSP — Implement Interface`). ## [ANALYZERS-PERFORMANCE] Performance And Caching -Static analyzers are lower priority than compiler diagnostics. A -`workspace/diagnostic` request must stream compiler/analyzer diagnostics first -and static analyzer diagnostics as later partial results. +Static analyzers are lower priority than compiler diagnostics. A `workspace/diagnostic` request must stream compiler/analyzer diagnostics first and static analyzer diagnostics as later partial results. Each sidecar owns a language-specific static analysis index keyed by: @@ -270,9 +183,7 @@ Each sidecar owns a language-specific static analysis index keyed by: - `global_state_version`. - Static analyzer config hash. -Workspace changes invalidate only affected project indexes when possible. A -full invalidation is required when project references, analyzer config, -signature files, or workspace kind changes. +Workspace changes invalidate only affected project indexes when possible. A full invalidation is required when project references, analyzer config, signature files, or workspace kind changes. Targets: @@ -287,12 +198,7 @@ Targets: The analyzer must prefer silence over false positives: -- If the workspace is not explicitly configured as a monorepo, return no - unused-public-code diagnostics. -- If a project is unloaded or failed to load, return no unused-public-code - diagnostics for symbols that could be referenced by that project. -- If cross-language identity cannot be proven for a symbol, do not report it as - unused. -- If the analyzer cannot distinguish a framework entry point from ordinary public - API, suppress the diagnostic and emit structured trace logging for future rule - tuning. +- If the workspace is not explicitly configured as a monorepo, return no unused-public-code diagnostics. +- If a project is unloaded or failed to load, return no unused-public-code diagnostics for symbols that could be referenced by that project. +- If cross-language identity cannot be proven for a symbol, do not report it as unused. +- If the analyzer cannot distinguish a framework entry point from ordinary public API, suppress the diagnostic and emit structured trace logging for future rule tuning. diff --git a/docs/specs/DISTRIBUTION-SPEC.md b/docs/specs/DISTRIBUTION-SPEC.md index e6911c14..c014a439 100644 --- a/docs/specs/DISTRIBUTION-SPEC.md +++ b/docs/specs/DISTRIBUTION-SPEC.md @@ -1,12 +1,6 @@ -# Distribution Specification +# [DIST] Distribution Specification -This document is the canonical specification for how SharpLsp is distributed. -All statements below are normative requirements, not suggestions. - -Every section has a hierarchical ID per CLAUDE.md (`[GROUP-TOPIC]` / -`[GROUP-TOPIC-DETAIL]`, uppercase, hyphen-separated, never numbered). Code -that implements a section MUST reference its ID in a comment. Cross-references -inside this spec MUST use IDs, never numbers. +This is the normative specification for SharpLsp distribution. ## [DIST-COMPONENTS] @@ -22,7 +16,7 @@ All three are verified by Shipwright on every VS Code activation via `activation ## [DIST-DEBUGGER-BUNDLE] -Debugging uses **netcoredbg** — the managed-code DAP adapter the `sharplsp-coreclr` debug type launches (`editors/vscode/src/debug.ts`, `SharpLspDebugAdapterFactory`). It is bundled in the VSIX so debugging works out of the box, mirroring how C# Dev Kit ships its own debugger. +Debugging uses **netcoredbg**, the managed-code DAP adapter launched for the `sharplsp-coreclr` debug type by `SharpLspDebugAdapterFactory` in `editors/vscode/src/debug.ts`. It is bundled in the VSIX. | Aspect | Requirement | |---|---| @@ -38,9 +32,9 @@ Unlike the three [DIST-COMPONENTS], a missing netcoredbg degrades **only** the d ## [DIST-RUNTIME-ACQUIRE] -The sidecars are framework-dependent .NET assemblies that target `net10.0`. They require a .NET 10 **SDK** — not merely a runtime — because the C# sidecar runs an in-process MSBuild design-time build and locates MSBuild via `MSBuildLocator.QueryVisualStudioInstances(options)` (see `sidecars/SharpLsp.Sidecar.CSharp/MSBuildInstanceSelector.cs` and [DIST-SDK-DISCOVERY] for why the query is workspace-independent), which **only enumerates installed SDKs**. A machine with a runtime alone — or with only an older SDK such as the .NET 9 SDK — has no MSBuild whose Roslyn matches the bundled `Microsoft.CodeAnalysis`, so every project load fails (`FUSION_E_REF_DEF_MISMATCH`) or MSBuild cannot be located at all. SharpLsp therefore acquires the **SDK** automatically via Microsoft's [`ms-dotnettools.vscode-dotnet-runtime`](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.vscode-dotnet-runtime) extension (the .NET Install Tool) — the same mechanism used by C# Dev Kit, the C# extension, .NET MAUI, Unity, CMake, and Bicep. +The framework-dependent `net10.0` sidecars require a .NET 10 SDK, not merely a runtime. The C# sidecar performs an in-process MSBuild design-time build and `MSBuildLocator.QueryVisualStudioInstances(options)` enumerates installed SDKs; a runtime-only or older-SDK machine cannot provide matching MSBuild/Roslyn and project load fails with `FUSION_E_REF_DEF_MISMATCH` or no MSBuild. SharpLsp therefore acquires the SDK through Microsoft's [`ms-dotnettools.vscode-dotnet-runtime`](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.vscode-dotnet-runtime) extension. See `sidecars/SharpLsp.Sidecar.CSharp/MSBuildInstanceSelector.cs` and [DIST-SDK-DISCOVERY]. -> **Reference — how other extensions do this.** The .NET Install Tool exposes `dotnet.acquire` (local *runtime*), `dotnet.acquireGlobalSDK` (system-wide *SDK*), and `dotnet.findPath` (discover an existing install). C# Dev Kit ([`ms-dotnettools.csdevkit`](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csdevkit)) declares the tool via `extensionDependencies` in its `package.json`. Authoritative API documentation lives at . SharpLsp follows this exact pattern — there is no Anthropic / Nimblesite-specific mechanism here, and any future maintainer asking "how do other VS Code extensions install .NET silently?" should land on this section and the linked docs. +> The .NET Install Tool exposes `dotnet.acquire` for a local runtime, `dotnet.acquireGlobalSDK` for a system-wide SDK, and `dotnet.findPath` for discovery. Its API contract is documented at . **Hard rules:** @@ -64,7 +58,7 @@ Shipwright continues to verify sidecar startup via `verifyStartup: true`. With ` The C# sidecar enumerates installed SDKs to pick the one whose Roslyn matches its bundled `Microsoft.CodeAnalysis` ([DIST-RUNTIME-ACQUIRE]). That enumeration MUST be **independent of the opened workspace**. MSBuildLocator resolves an SDK from a *working directory* via `hostfxr_resolve_sdk2`, which honours any `global.json` at or above that directory. The sidecar process inherits the workspace root as its working directory, so a naïve `MSBuildLocator.QueryVisualStudioInstances()` resolves the *workspace's* `global.json`. When that file pins a `version`/`rollForward` band with no installed match (e.g. Fantomas pins `10.0.100` on a box that has only `10.0.203`), `hostfxr_resolve_sdk2` throws `InvalidOperationException` ("A compatible .NET SDK was not found"). -Before this rule the throw was fatal: `Program.cs` caught it and called `Environment.Exit(1)` *before* the `READY:` handshake, so the Rust host saw "sidecar exited before READY" and restarted forever. That crash-loop broke every C#-sidecar request — including `solution/read`, which needs no MSBuild at all (it uses `Microsoft.VisualStudio.SolutionPersistence`), so the Solution Explorer failed even for **pure-F# solutions** that never touch Roslyn. Captured in issue #134. +Discovery failure before the `READY:` handshake can cause an endless sidecar restart loop and block MSBuild-free requests such as `solution/read`, including for pure-F# solutions. It MUST therefore follow the degraded path below (issue #134). **Hard rules:** @@ -97,12 +91,7 @@ Every call SharpLsp makes to the .NET Install Tool MUST include all four require `architecture` is derived from Node's `process.arch` and mapped as: `x64` → `x64`, `arm64` → `arm64`, `ia32` → `x86`, default → `x64`. This mapping lives in `editors/vscode/src/dotnetRuntime.ts`. -**Reasoning — why architecture is non-optional.** -The first SharpLsp v0.1.0 release omitted `architecture` from the `dotnet.findPath` payload. The .NET Install Tool rejected the request with `"The find path request was missing required information: a mode, version, architecture, and requestingExtensionId."` — a runtime error that our code silently swallowed via `try/catch`, falling through to `dotnet.acquire` (which also lacked `architecture` but happened to succeed because the install path uses different defaulting). This produced misleading log messages and would have failed entirely on architectures without a default. The lesson: every required field in the upstream API contract is a hard precondition, even when an "optional" code path papers over the omission. - -This applies symmetrically to `dotnet.findPath` — its `acquireContext` MUST include `architecture` for the same reason. - -**Verification:** Confirmed against the upstream contract at and against the live extension's own error message captured in the SharpLsp activation log on 2026-04-30. +The .NET Install Tool rejects a `dotnet.findPath` payload missing `mode`, `version`, `architecture`, or `requestingExtensionId`; `acquireContext` MUST contain all four fields. See the upstream contract at . ## [DIST-FAILURE-UX] @@ -117,9 +106,6 @@ Whenever activation cannot deliver a working language server — for any reason, 5. **The error message MUST name the failure mode in plain language** ("required binaries are missing or version-mismatched", ".NET 10 install failed", "language server crashed during startup") — never just dump a stack trace into the toast. The full diagnostic text goes to the output channel reachable via `[Show Log]`. 6. **Recovery commands MUST be registered** so the user can re-attempt without uninstalling. Examples: `sharplsp.retryDotnetAcquisition`, `sharplsp.restartServer`. These appear in the command palette under the `SharpLsp:` category. -**Reasoning — why this rule exists.** -The first v0.1.0 release threw out of `activate()` when bundled binaries were missing or had a version mismatch. VS Code logged the failure to its developer console — invisible to the user. The user opened a `.csproj` folder, saw absolutely nothing happen, and had no way to discover the problem without manually inspecting the extension log file. This is the worst possible UX: the extension is broken, the user does not know it is broken, and there is no in-product hint that anything went wrong. This section makes that mode of failure a normative bug going forward. Captured from the activation log on 2026-04-30: every error path now MUST produce a visible toast and an actionable command. - **Implementation reference:** - `editors/vscode/src/result.ts` — `Result`, `ok`, `err`. - `editors/vscode/src/extension.ts` — outer `activate()` catch surfaces the toast; inner `activateInner()` step paths return early with toast + degraded API instead of throwing. @@ -132,13 +118,10 @@ Editors capture the language server's `stderr` into a user-facing Output panel ( **Hard rules:** 1. **No ANSI escape codes reach the panel.** The captured stream is a pipe, not a TTY, so color/cursor escapes render as garbage. The Rust host gates its `tracing` stderr layer on `std::io::IsTerminal` (`.with_ansi(stderr_is_terminal)`), emitting plain text whenever stderr is not an interactive terminal. The VS Code extension additionally strips ANSI defensively before anything reaches the channel (`createAnsiStrippingChannel`). -2. **Sidecars MUST NOT write diagnostics to `Console.Error` / `eprintfn`.** Per the project logging rule, sidecar diagnostics use structured logging (Serilog) routed to a per-sidecar rolling file under the system temp directory (`sharplsp-logs/sidecar-.log`) — never the inherited stderr. The only legitimate sidecar `stdout`/`stderr` writes are the `READY:` IPC handshake, the `--version` banner, the CLI usage message, and the one-shot actionable SDK-resolution hints ([DIST-RUNTIME-ACQUIRE] portability, below, and [DIST-SDK-DISCOVERY]) — the Roslyn-mismatch, missing-SDK, and unresolvable-`global.json` startup diagnostics, each emitted at most once per process. +2. **Sidecars MUST NOT write routine diagnostics to `Console.Error` / `eprintfn`.** Per the project logging rule, sidecar diagnostics use structured logging (Serilog) routed to a per-sidecar rolling file under the system temp directory (`sharplsp-logs/sidecar-.log`)—never the inherited stderr. The only legitimate sidecar `stdout`/`stderr` writes are the versioned `READY:` IPC handshake, the `--version` banner, the CLI usage message, one sanitized pre-READY `FATAL:` diagnostic required by [SIDECAR-STARTUP-FAILURE](SIDECAR-LIFECYCLE-SPEC.md), and the one-shot actionable SDK-resolution hints ([DIST-RUNTIME-ACQUIRE] portability, below, and [DIST-SDK-DISCOVERY])—the Roslyn-mismatch, missing-SDK, and unresolvable-`global.json` startup diagnostics, each emitted at most once per process. 3. **Per-request chatter goes to the file log, not the panel.** Routine traces (e.g. the router's per-request `[Router] Handling …`) are logged at `Debug` to the rolling file. Genuinely user-facing failures still surface (via the host's `error!` on a failed sidecar request, or a `[Show Log]` action per [DIST-FAILURE-UX]). 4. **A type-load failure is summarized once.** MSBuild surfaces a `ReflectionTypeLoadException` as a diagnostic carrying dozens of identical "Could not load file or assembly" lines, repeated once per project. Repeated lines MUST be collapsed (`SidecarLog.CollapseRepeatedLines`) and duplicate summaries de-duplicated so the log records one distinct, actionable line — not a flood. -**Reasoning — why this rule exists.** -The first releases piped the host's colorized `tracing` output and each sidecar's raw `Console.Error` straight into the Output panel. Activation filled it with `\x1b[2m…\x1b[0m` escape garbage, a per-request `[Router] Handling …` line, and ~200 near-identical type-load lines dumped from a single exception — making the panel unreadable and masking the real failure (a Roslyn version mismatch). Captured in issue #78. - **Implementation reference:** - `src/main.rs` — `IsTerminal`-gated `.with_ansi(…)` on the stderr `tracing` layer. - `editors/vscode/src/output-filter.ts` — `stripAnsi` + `createAnsiStrippingChannel`, wired into the client's `outputChannel` in `editors/vscode/src/client.ts`. @@ -182,20 +165,18 @@ The sidecar binaries are identical across all platform VSIXs — they are manage ## [DIST-VSIX-ASSET-INTEGRITY] -The extension's icon assets (`editors/vscode/icons/`) are tracked as symlinks into `docs/designs/logo/` — a single source of truth for brand assets. On checkouts where Git cannot create symlinks (`core.symlinks=false`, the default on most Windows machines), Git materializes each symlink as a small text file containing the target path. `vsce` packages whatever is on disk, so such a checkout silently produces a VSIX whose Marketplace and activity-bar icons are broken text stubs, and the extension-development host renders broken icons. +The extension's icon assets in `editors/vscode/icons/` are symlinks into `docs/designs/logo/`. With `core.symlinks=false`, Git materializes target paths as text files, which `vsce` would package as broken icons. 1. Every image asset referenced by the extension manifest MUST be packaged as real image content. A VSIX containing symlink text stubs is broken. 2. `scripts/resolve-symlink-stubs.mjs` rewrites stub files in place with their target's content. It MUST leave real OS symlinks untouched (macOS/Linux, and Windows checkouts with `core.symlinks=true`), making it a cross-platform no-op wherever symlinks work. It only rewrites plain files whose entire content is a relative POSIX path resolving to an existing file. 3. The resolver MUST run automatically before packaging (`vscode:prepublish`) and before the e2e suite (`pretest`), so both the packaged VSIX and the extension-development host load real images. The e2e suite asserts the invariant (`bundled-binary.test.ts`). 4. Resolved stubs modify the working tree and MUST NOT be committed — Git would record the binary content as the symlink's target text, corrupting the symlink for every other platform. Restore with `git restore editors/vscode/icons`. -CI and releases are unaffected: GitHub's hosted runners (including `windows-latest`) check out with working symlinks, and published VSIXs contain real icons (verified against the `v0.13.0` `win32-x64` asset). - ## [DIST-RESOLUTION] Resolution is driven by the `sources` array per component in `shipwright.json`. The `activateDeploymentToolkit` call verifies all three on activation. Failure to resolve any required component triggers [DIST-FAILURE-UX] (degraded mode + toast), not a host-crashing throw. -## [DIST-RESOLUTION-LSP] +### [DIST-RESOLUTION-LSP] `sharplsp` (LSP server — native binary). @@ -209,7 +190,7 @@ Sources: `["user-setting", "env", "bundled", "path", "pkgmgr"]` | 4 | `path` | `sharplsp` on `$PATH`; exact version match required | | 5 | `pkgmgr` | Shows modal prompt: `brew install nimblesite/tap/sharplsp` / `scoop install nimblesite/sharplsp` | -## [DIST-RESOLUTION-CSHARP] +### [DIST-RESOLUTION-CSHARP] `sharplsp-sidecar-csharp` (C# Roslyn sidecar — .NET assembly). @@ -224,7 +205,7 @@ Sources: `["user-setting", "env", "bundled", "path"]` **If bundled binary is missing the VSIX is broken — fix the build, not the resolution.** Surface per [DIST-FAILURE-UX]. -## [DIST-RESOLUTION-FSHARP] +### [DIST-RESOLUTION-FSHARP] `sharplsp-sidecar-fsharp` (F# FCS sidecar — .NET assembly). @@ -276,6 +257,12 @@ The VS Code extension uses `@nimblesite/shipwright-vscode` (`activateDeploymentT 6. **Acquire the .NET 10 SDK at activation start** via `dotnet.acquireGlobalSDK` from the .NET Install Tool extension (see [DIST-RUNTIME-ACQUIRE]). Show a non-interactive progress notification + status-bar spinner. SharpLsp's own UI never prompts or blocks on user action. 7. **Use `Result` everywhere** per [DIST-FAILURE-UX]. No `throw` inside extension code; no unhandled rejections out of `activate()`. +## [DIST-WORKSPACE-TRUST] + +An untrusted workspace MUST NOT select an executable or inject process arguments. `editors/vscode/package.json` declares `capabilities.untrustedWorkspaces.supported: "limited"` and restricts `sharplsp.lspPath`, `sharplsp.csharpSidecarPath`, `sharplsp.fsharpSidecarPath`, `sharplsp.server.extraArgs`, `sharplsp.fsi.extraArgs`, and `sharplsp.debug.netcoredbgPath`. + +While `workspace.isTrusted` is false, the runtime guards in `editors/vscode/src/config.ts` MUST return no custom LSP path, server arguments, or FSI arguments, leaving Shipwright's bundled binaries in use. When `workspace.onDidGrantWorkspaceTrust` fires, `editors/vscode/src/extension.ts` MUST restart the language client so newly trusted path and argument settings take effect without a window reload. + ## [DIST-PATH-INSTALL] Users who want `sharplsp` on their system PATH outside VS Code may install via: @@ -296,7 +283,7 @@ Tag-triggered (`v*`). Jobs: ## [DIST-CI-LAYOUT] -The PR pipeline is split across reusable workflows (`on: workflow_call`) rather than one monolith, so no CI file outgrows comprehension and each leg is readable and editable in isolation: +The PR pipeline uses reusable workflows (`on: workflow_call`): | Workflow | Leg | |---|---| @@ -328,7 +315,7 @@ All CI jobs that run `vsce package` or `vsce publish` MUST use `node-version: '2 Stable toolchain. Cross-compilation targets must be added via `dtolnay/rust-toolchain@stable` with explicit `targets:`. -## [DIST-CI-RUST-SHARDS] +### [DIST-CI-RUST-SHARDS] The Rust e2e suite runs single-threaded (`RUST_TEST_THREADS=1` — tests spawn real Roslyn/FCS sidecars), so its wall time scales with test count, not runner cores. CI therefore splits it into `SHARD_COUNT` nextest **hash partitions** (`make _test-rust-shard SHARD=`, i.e. `--partition hash:/`), run as a `test-rust` job matrix. @@ -343,17 +330,17 @@ Invariants: `tokio::net::UnixStream` is **unix-only** and MUST NOT be used unconditionally. All sidecar transport code MUST be gated: - `#[cfg(unix)]` — use `tokio::net::UnixStream` -- `#[cfg(windows)]` — use TCP loopback (`127.0.0.1:0`) or `tokio::net::windows::named_pipe` +- `#[cfg(windows)]` — use `tokio::net::windows::named_pipe`; TCP loopback is not an IPC fallback Both the Rust host and the .NET sidecar MUST use the same transport on each platform. Win32 builds failing to compile due to `UnixStream` is a hard blocker. The .NET sidecars are platform-neutral assemblies shipped identically in every VSIX ([DIST-VSIX-LAYOUT]), so **their transport selection MUST be a runtime decision keyed on the endpoint shape**: an endpoint starting with `\\.\pipe\` selects a named pipe server/client; anything else selects a Unix domain socket. Compile-time gating (`#if WINDOWS`) is forbidden in sidecar transport code — the symbol is never defined for the platform-neutral `net10.0` build, which silently compiles the Unix branch into the Windows VSIX and makes the sidecars exit before READY (GitHub #110). -Both listener flavors MUST restrict the endpoint to the current user: `0600` on the Unix domain socket, `PipeOptions.CurrentUserOnly` on the named pipe server. The endpoint names are deterministic, so an unrestricted endpoint is claimable/connectable by any co-located local user. CI MUST run the sidecar transport tests on a Windows runner — an ubuntu-only matrix never executes the named-pipe arm, which is how GitHub #110 shipped. +Both listener flavors MUST restrict the endpoint to the current user: `0600` on the Unix domain socket, `PipeOptions.CurrentUserOnly` on the named pipe server. Endpoint names MUST also be unpredictable and unique per spawn per [SIDECAR-STARTUP-ENDPOINT](SIDECAR-LIFECYCLE-SPEC.md), preventing concurrent hosts or an orphaned prior generation from intentionally sharing a name. Current-user restriction remains mandatory defense in depth. CI MUST run the sidecar transport tests on a Windows runner—an Ubuntu-only matrix never executes the named-pipe arm, which is how GitHub #110 shipped. ## [DIST-CI-WIN-VSIX] -The transport tests ([DIST-CI-WIN-TRANSPORT]) prove the named pipes carry frames; they do NOT prove the whole editor experience works on top of them. CI MUST therefore run the VS Code end-to-end suite's **whole feature surface** on Windows runners (`ci-vsix-windows.yml`, driven by the `_test-vsix-win` Make target), through the REAL LSP — release-built `sharplsp` host plus the Roslyn and FCS sidecars — inside the actual VS Code extension host over win32 named-pipe IPC. A grep-selected smoke subset is NOT sufficient: the features most likely to break on Windows are the ones that shell out to platform-specific executables (`netcoredbg.exe`, `dotnet-trace`, `dotnet test`, `dotnet new`) and manipulate Windows paths, none of which a completion/hover subset touches. +CI MUST run the VS Code end-to-end suite's whole feature surface on Windows runners through `ci-vsix-windows.yml` and `_test-vsix-win`: the release-built `sharplsp` host, Roslyn and FCS sidecars, actual VS Code extension host, and win32 named-pipe IPC. [DIST-CI-WIN-TRANSPORT] covers frames only, while Windows-specific executables (`netcoredbg.exe`, `dotnet-trace`, `dotnet test`, `dotnet new`) and paths require full feature coverage; a grep-selected smoke subset is insufficient. The suite is sliced into **feature chunks**, one Windows CI job each, run with `fail-fast: false` so one failing feature area never hides the state of the others: @@ -376,8 +363,6 @@ Invariants: - **Ubuntu owns coverage.** Windows chunks run **without** `--coverage` and enforce no coverage gate — one chunk can never meet the line threshold. The Ubuntu `test-vsix` job owns the full single-process run plus the ratcheted gate, and is the only job that runs the `real-repo-*` stress suites (each clones and restores a pinned third-party repository; that is repo ingestion, not platform behaviour). - **No PATH leakage.** Every VS Code job runs `scripts/purge-path-binaries.sh` first, so the test host can only resolve the freshly-staged bundled binaries. A dev copy on `PATH` would substitute itself for the artifact under test and turn a broken bundle green. -Two assertion rules follow from running on win32 at all, and both are load-bearing — each one silently passed on Ubuntu for the life of the suite and failed on the first Windows run: - - **Compare paths case-insensitively on Windows.** VS Code lowercases the drive letter whenever a path travels through `Uri.fsPath`, while `extensionPath` and `os.tmpdir()` preserve the original casing, so the same file legitimately has two spellings. Any assertion comparing a `Uri`-derived path against a directly-constructed one MUST go through `comparablePath()` (`test-helpers.ts`), which lowercases on win32 only — POSIX paths stay case-sensitive, because there `/tmp/A` and `/tmp/a` really are different files. - **Suites MUST be order-independent.** Chunking changes which suites share an extension host, so no suite may depend on state another suite left in a shared singleton. Fixture identifiers that feed a shared registry — notably test method names discovered into the `SharpLspTestController` — MUST be unique per suite, or a test asserting "nothing matches" passes or fails on whichever suite's discovery won the race. diff --git a/docs/specs/HOVER-SPEC.md b/docs/specs/HOVER-SPEC.md index 87a7db2f..12cc2b41 100644 --- a/docs/specs/HOVER-SPEC.md +++ b/docs/specs/HOVER-SPEC.md @@ -1,16 +1,14 @@ -# Hover / Quick Info Specification +# [HOVER] Hover / Quick Info Specification **Parent:** [SHARPLSP-SPEC.md](SHARPLSP-SPEC.md) -## 1. Overview +## [HOVER-OVERVIEW] Overview -Hover (Quick Info) provides rich tooltip information when the user hovers over a symbol or keyword. SharpLsp implements `textDocument/hover` ([LSP 3.17 §3.17.5](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_hover)) for both C# and F# as equal first-class citizens. +SharpLsp implements P0 `textDocument/hover` ([LSP 3.17 §3.17.5](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_hover)) for C# and F#. -This feature is **P0** (launch blocker) and targets Phase 2 delivery. +## [HOVER-PROTOCOL] LSP Protocol -## 2. LSP Protocol - -### 2.1 Request +### [HOVER-PROTOCOL-REQUEST] Request ``` method: textDocument/hover @@ -20,7 +18,7 @@ params: HoverParams { } ``` -### 2.2 Response +### [HOVER-PROTOCOL-RESPONSE] Response ``` result: Hover | null @@ -38,7 +36,7 @@ interface Hover { SharpLsp MUST return `MarkupContent` with `kind: "markdown"`. Plain-text fallback is not supported — all LSP 3.17 clients support Markdown. -## 3. Request Routing +## [HOVER-ROUTING] Request Routing Hover is a **semantic** request. The Rust host routes it to the appropriate sidecar based on document language. @@ -51,9 +49,9 @@ Hover is a **semantic** request. The Rust host routes it to the appropriate side The Rust host MAY use tree-sitter to pre-validate the hovered position (e.g., skip hover for whitespace/comments) and short-circuit with `null` before dispatching to the sidecar. -## 4. C# Implementation (Roslyn) +## [HOVER-CSHARP] C# Implementation -### 4.1 Symbol Resolution +### [HOVER-CSHARP-RESOLUTION] Symbol Resolution 1. Obtain `Document` from the current `Solution` snapshot for the given URI. 2. Get `SemanticModel` via [`Document.GetSemanticModelAsync()`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.document.getsemanticmodelasync). @@ -62,7 +60,7 @@ The Rust host MAY use tree-sitter to pre-validate the hovered position (e.g., sk 5. If `GetSymbolInfo()` returns no symbol, fall back to [`SemanticModel.GetTypeInfo()`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.semanticmodel.gettypeinfo) for implicit types and expressions. 6. For keywords (`var`, `await`, `async`, `nameof`, etc.), provide keyword-specific documentation. -### 4.2 Markdown Rendering +### [HOVER-CSHARP-RENDERING] Markdown Rendering The hover response MUST include: @@ -76,7 +74,7 @@ The hover response MUST include: | Accessibility | `public`, `internal`, `protected`, etc. | Yes | | Deprecation | `[Obsolete]` message | Yes (if present) | -#### XML Documentation Rendering +#### [HOVER-CSHARP-RENDERING-XML] XML Documentation Rendering - `` — Rendered as the primary description paragraph. - `` — Rendered as a parameter list with descriptions. @@ -96,7 +94,7 @@ XML docs are sourced from: 2. XML documentation files from NuGet packages (`.xml` files alongside assemblies). 3. Roslyn's built-in documentation provider as fallback. -### 4.3 Special Cases +### [HOVER-CSHARP-CASES] Special Cases | Hover Target | Behavior | |---|---| @@ -111,15 +109,15 @@ XML docs are sourced from: | Preprocessor directives | Show directive documentation | | `using` alias | Show the aliased type | -## 5. F# Implementation (FCS) +## [HOVER-FSHARP] F# Implementation -### 5.1 Symbol Resolution +### [HOVER-FSHARP-RESOLUTION] Symbol Resolution 1. Get `FSharpCheckFileResults` for the document via `FSharpChecker.CheckFileInProject()`. 2. Call `GetToolTip(line, col, lineText, names, tokenTag)` to obtain `ToolTipText`. 3. `ToolTipText` contains `ToolTipElement[]`, each with a structured layout and XML documentation. -### 5.2 Markdown Rendering +### [HOVER-FSHARP-RENDERING] Markdown Rendering F# hover follows the same Markdown structure as C#: @@ -131,7 +129,7 @@ F# hover follows the same Markdown structure as C#: | Constraints | Generic constraints | Extracted from signature | | Union cases | Case fields and types | `ToolTipElement` for DU cases | -### 5.3 F#-Specific Cases +### [HOVER-FSHARP-CASES] F#-Specific Cases | Hover Target | Behavior | |---|---| @@ -143,43 +141,17 @@ F# hover follows the same Markdown structure as C#: | Discriminated union cases | Show case fields with types | | Record fields | Show field type and containing record | -### 5.4 Live-Buffer Resolution `[FS-DIDCHANGE-OVERLAY]` - -Hover MUST resolve against the editor's **in-memory buffer**, not the on-disk -file. The Rust host forwards `textDocument/didOpen`/`didChange` to the document's -own sidecar (F# → F# sidecar, C# → C# sidecar); routing by language is mandatory, -since a misrouted edit leaves the owning sidecar resolving positions against stale -text. The F# sidecar keeps an in-memory overlay keyed by absolute file path and -every per-file analysis (hover, completion, signature help, …) reads source via -that overlay, falling back to disk only when no open buffer exists. This restores -F# to parity with C#, whose Roslyn workspace is already updated in place on -`didChange`. Without this, F# hover misaligns the moment the buffer diverges from -disk (i.e. as soon as the user types) and returns the wrong symbol or `null`. - -### 5.5 Canonical Check Funnel `[FS-DIDCHANGE-OVERLAY]` - -Every per-file FCS analysis (hover, completion, diagnostics, signature help, -inlay hints, code fixes, file ordering) funnels through **one** canonical -check — `parseAndCheckOnce` (the raw parse+check) and its `checkFileWithParse` -/ `checkFile` views — rather than each call site invoking -`FSharpChecker.ParseAndCheckFileInProject` itself. This keeps overlay-aware -source resolution and `FSharpCheckFileAnswer` handling in exactly one place -(DRY) and guarantees every feature type-checks the **live didChange buffer**, -so a reverted or freshly edited file is always analysed as its newest text -instead of stale on-disk content — the property that lets a reverted buffer -clear its phantom errors on the next pull (GitHub #160). - -The sidecar processes IPC messages strictly sequentially — `SidecarHost` -awaits each handler to completion before reading the next frame — so a -`didChange` never lands while a check is in flight; the source a check reads is -always the newest committed buffer. (Should dispatch ever become concurrent, a -mid-check stability re-read would be needed here; it is deliberately omitted -today because that path is unreachable and cannot be exercised by a -deterministic test.) This is the sidecar-side complement of the Rust host's -push gate `[DIAG-PUSH-GATE]` (DIAGNOSTICS-SPEC §1.3), which guarantees stale -results are never *published*. - -## 6. Caching Strategy +### [FS-DIDCHANGE-OVERLAY] Live-Buffer Resolution + +Hover MUST resolve against the editor's **in-memory buffer**, not the on-disk file. The Rust host routes `textDocument/didOpen` and `didChange` by document language to the owning sidecar. The F# sidecar keeps an overlay keyed by absolute file path, and every per-file analysis (hover, completion, signature help, and others) reads from it, falling back to disk only when no open buffer exists. The C# sidecar updates its Roslyn workspace in place on `didChange`. + +#### [FS-DIDCHANGE-OVERLAY-CHECK] Canonical Check Funnel + +Every per-file FCS analysis (hover, completion, diagnostics, signature help, inlay hints, code fixes, and file ordering) MUST use the canonical `parseAndCheckOnce` operation through its `checkFileWithParse` or `checkFile` view instead of calling `FSharpChecker.ParseAndCheckFileInProject` directly. This centralizes overlay-aware source resolution and `FSharpCheckFileAnswer` handling, ensures checks use the latest `didChange` text, and lets a reverted buffer clear phantom errors on the next pull (GitHub #160). + +`SidecarHost` processes IPC messages sequentially, awaiting each handler before reading the next frame, so `didChange` cannot arrive during a check. If dispatch becomes concurrent, checks MUST re-read buffer stability before publishing. The host-side `[DIAG-PUSH-GATE]` in [DIAGNOSTICS-SPEC.md](DIAGNOSTICS-SPEC.md) independently prevents stale results from being published. + +## [HOVER-CACHING] Caching Strategy Hover results are cached via the [salsa](https://salsa-rs.github.io/salsa/) incremental computation database in the Rust host. @@ -190,7 +162,7 @@ Hover results are cached via the [salsa](https://salsa-rs.github.io/salsa/) incr The Rust host SHOULD cache the most recent hover result per document and return it immediately if the position and version match. Stale hover requests for superseded document versions MUST be cancelled. -## 7. Performance Requirements +## [HOVER-PERFORMANCE] Performance Requirements | Metric | Target | Measurement | |---|---|---| @@ -199,7 +171,7 @@ The Rust host SHOULD cache the most recent hover result per document and return | Hover for cached position | <1ms | salsa cache hit | | Tree-sitter pre-validation | <1ms | Whitespace/comment rejection | -## 8. Error Handling +## [HOVER-ERRORS] Error Handling | Condition | Response | |---|---| @@ -211,11 +183,11 @@ The Rust host SHOULD cache the most recent hover result per document and return Hover MUST NOT block, hang, or return errors to the client. On any failure, return `null`. -## 9. Solution Explorer Tree Hover +## [HOVER-TREE] Solution Explorer Tree Hover The Solution Explorer tree view MUST use the **same hover** as the code editor. When a user hovers over a symbol in the tree, the tooltip MUST be identical to the tooltip shown when hovering over the same symbol in the code editor. -### Implementation +### [HOVER-TREE-IMPLEMENTATION] Implementation Tree item tooltips are resolved via `resolveTreeItem()`, which calls `vscode.executeHoverProvider` at the symbol's source position. This triggers the exact same `textDocument/hover` LSP request pipeline (Rust host -> sidecar -> Roslyn/FCS) used by the code editor. @@ -227,18 +199,4 @@ Tree item tooltips are resolved via `resolveTreeItem()`, which calls `vscode.exe | Project Reference | Static metadata (reference name) | | Solution / Project / Folder | No tooltip | -**Critical invariant:** Tree hover and code hover MUST produce identical content for the same symbol. They are the same code path. Any divergence is a bug. - -## 10. Competitive Parity Matrix - -| Feature | VS | CDK | Rider | SharpLsp Target | Priority | -|---|---|---|---|---|---| -| Basic symbol hover | ✓ | ✓ | ✓ | ✓ | P0 | -| XML doc rendering | ✓ | ✓ | ✓ | ✓ | P0 | -| Inferred type hover (`var`) | ✓ | ✓ | ✓ | ✓ | P0 | -| Exception documentation | ✓ | ✗ | ✓ | ✓ | P1 | -| Nullable annotation display | ✓ | ✓ | ✓ | ✓ | P1 | -| Deprecation warnings | ✓ | ✓ | ✓ | ✓ | P0 | -| NuGet package XML docs | ✓ | ✓ | ✓ | ✓ | P0 | -| Color preview in hover | ✓ | ✗ | ✓ | ✓ | P2 | -| Quick navigation from hover | ✓ | ✗ | ✓ | ✓ | P2 | +Tree hover and code hover MUST produce identical content for the same symbol; any divergence is a bug. diff --git a/docs/specs/NUGET-BROWSER-SPEC.md b/docs/specs/NUGET-BROWSER-SPEC.md index 47ed99ea..5f0dca3a 100644 --- a/docs/specs/NUGET-BROWSER-SPEC.md +++ b/docs/specs/NUGET-BROWSER-SPEC.md @@ -1,8 +1,8 @@ -# NuGet Browser Specification +# [NUGET] NuGet Browser Specification **Parent:** [SHARPLSP-SPEC.md](SHARPLSP-SPEC.md) -## 1. Overview +## [NUGET-OVERVIEW] Overview SharpLsp provides a built-in NuGet package manager UI accessible from the Solution Explorer. Users can search, browse, install, update, and remove NuGet packages for any project in the solution. The UI is a webview panel rendered by the editor extension, but **all NuGet operations are routed through the LSP server** via custom requests. The extension NEVER talks directly to nuget.org or the dotnet CLI. @@ -10,54 +10,39 @@ SharpLsp provides a built-in NuGet package manager UI accessible from the Soluti **Design reference:** `docs/designs/code.html`, `docs/designs/screen.png` -## 2. Architecture +## [NUGET-ARCHITECTURE] Architecture -### 2.1 Component Placement +### [NUGET-ARCHITECTURE-PLACEMENT] Component Placement -NuGet operations live in the **Rust LSP host** (Tier 1). The dotnet CLI runs as a child process managed by the host. No sidecar involvement. +NuGet operations live in the **Rust LSP host** (Tier 1). The host runs `dotnet` as a managed child process and calls the NuGet API directly; sidecars and editor extensions MUST NOT perform either operation. Package management MUST remain available after a sidecar crash. ``` Editor Webview ──postMessage──> Extension ──LSP custom request──> Rust Host ──spawns──> dotnet CLI │ ├── dotnet list package - ├── dotnet add package - ├── dotnet remove package + ├── edit MSBuild package references + ├── dotnet restore └── HTTP fetch to nuget.org API ``` -### 2.2 Why Rust Host, Not Sidecar +## [NUGET-REQUESTS] LSP Custom Requests -- `dotnet` CLI operations are standalone commands, not Roslyn/FCS APIs -- No workspace or compilation context needed for package management -- NuGet.org search API is a simple HTTP GET - no .NET runtime required -- Keeps the extension editor-agnostic: any LSP client (Neovim, Helix, Zed) can consume the same requests -- Sidecar crash must not interfere with package management - -### 2.3 Why NOT the Extension - -- Editor extensions must remain thin LSP clients -- Direct CLI/HTTP calls from the extension make the feature VS Code-only -- Other editors (Neovim, Helix, Zed) cannot reuse extension-side logic -- LSP is the single integration point for all editors - -## 3. LSP Custom Requests - -### 3.0 Target Selection +### [NUGET-REQUESTS-TARGET] Target Selection **Critical:** every NuGet operation MUST be scoped to a concrete install target. The UI cannot assume the "current project" — the user MUST pick one explicitly from a dropdown rendered at the top of the panel (next to the Browse/Installed tabs). Without a selected target, the Install / Uninstall / Update actions MUST be disabled and display a tooltip "Select a target first". -#### 3.0.1 Target kinds +#### [NUGET-REQUESTS-TARGET-KINDS] Target Kinds A target is one of: | Kind | Example path | `dotnet` command | Notes | |------|--------------|------------------|-------| -| `project` | `/repo/src/Foo/Foo.csproj` | `dotnet add package …` | A single `.csproj` / `.fsproj`. | -| `project` | `/repo/src/Bar/Bar.fsproj` | `dotnet add package …` | Same as above for F#. | +| `project` | `/repo/src/Foo/Foo.csproj` | Direct XML edit + `dotnet restore` | A single `.csproj` / `.fsproj`. | +| `project` | `/repo/src/Bar/Bar.fsproj` | Direct XML edit + `dotnet restore` | Same as above for F#. | | `buildProps` | `/repo/Directory.Build.props` | **Direct XML edit** — NOT `dotnet add` | `dotnet add` does not support props files. The Rust host edits the `` block directly, preserving formatting. Requires follow-up `dotnet restore` at the props file's directory. | | `buildProps` | `/repo/src/Directory.Packages.props` | Central Package Management | When CPM is enabled (`ManagePackageVersionsCentrally=true`), version lives in `Directory.Packages.props` as ``, and the `` in the csproj has no `Version=`. The host must detect CPM and route accordingly. | -#### 3.0.2 `sharplsp/nuget/targets` +#### [NUGET-REQUESTS-TARGET-ENUMERATE] `sharplsp/nuget/targets` Enumerate all valid install targets in the currently open solution/workspace. @@ -95,7 +80,7 @@ interface NuGetTarget { - Detect CPM by parsing the nearest `Directory.Packages.props` and checking `ManagePackageVersionsCentrally`. - Persist last-used target per workspace (via extension `Memento` / workspaceState) so the dropdown defaults to it next session. -#### 3.0.3 UI contract +#### [NUGET-REQUESTS-TARGET-UI] UI Contract - A **target dropdown** is rendered in the panel header, to the **right of the tabs, left of the search box**. - The dropdown lists projects first (grouped under a "Projects" header), then props files (grouped under a "Build Props" header). @@ -106,7 +91,7 @@ interface NuGetTarget { - When CPM is enabled, installing to a `project` target MUST transparently update `Directory.Packages.props` (add/update ``) AND the csproj (`` without a version). The host handles this — the UI does not care. - When CPM is enabled AND the user explicitly picks the `Directory.Packages.props` target, the operation is a pure version-management edit (add/update `` only; no `` is touched). -### 3.1 `sharplsp/nuget/search` +### [NUGET-REQUESTS-SEARCH] `sharplsp/nuget/search` Search nuget.org for packages matching a query. @@ -115,7 +100,7 @@ Search nuget.org for packages matching a query. ```typescript interface NuGetSearchParams { query: string; // Search query (empty = popular packages) - target: NuGetTarget; // Target (§ 3.0) — used to resolve isInstalled / installedVersion + target: NuGetTarget; // [NUGET-REQUESTS-TARGET], used to resolve installation state prerelease: boolean; // Include prerelease versions take: number; // Max results (default 50) skip: number; // Pagination offset (default 0) @@ -152,7 +137,7 @@ interface NuGetPackageInfo { - HTTP GET to `https://azuresearch-usnc.nuget.org/query?q={query}&prerelease={prerelease}&take={take}&skip={skip}` - Cache search results for 60s to avoid hammering the API -### 3.2 `sharplsp/nuget/versions` +### [NUGET-REQUESTS-VERSIONS] `sharplsp/nuget/versions` Get all available versions for a specific package. @@ -176,7 +161,7 @@ interface NuGetVersionsResponse { - HTTP GET to `https://api.nuget.org/v3-flatcontainer/{id}/index.json` - Return versions in reverse chronological order (newest first) -### 3.3 `sharplsp/nuget/installed` +### [NUGET-REQUESTS-INSTALLED] `sharplsp/nuget/installed` List installed packages for a target. @@ -184,7 +169,7 @@ List installed packages for a target. ```typescript interface NuGetInstalledParams { - target: NuGetTarget; // § 3.0 + target: NuGetTarget; // [NUGET-REQUESTS-TARGET] } ``` @@ -206,9 +191,9 @@ interface InstalledPackageInfo { - Executes `dotnet list package --format json` - Parses JSON output to extract installed packages across all target frameworks -### 3.4 `sharplsp/nuget/install` +### [NUGET-REQUESTS-INSTALL] `sharplsp/nuget/install` -Install or update a NuGet package against a chosen target (see § 3.0). +Install or update a NuGet package against a [NUGET-REQUESTS-TARGET]. **Request:** @@ -233,15 +218,15 @@ interface NuGetInstallResponse { **Behavior by target kind:** - `target.kind === "project"`: - - **CPM disabled:** `dotnet add package --version `. - - **CPM enabled:** edit `Directory.Packages.props` to add/update ``, then edit the csproj to add `` (no `Version`). Do NOT shell out to `dotnet add` in CPM mode — it writes a `Version=` attribute that violates CPM. + - **CPM disabled:** edit the project XML to add or update ``, preserving trivia, then start `dotnet restore` in the background. + - **CPM enabled:** edit `Directory.Packages.props` to add/update ``, then edit the project to add `` without `Version`, and start background restore. - `target.kind === "buildProps"`: - Parse the props XML (preserving whitespace / comments), locate an `` containing `` (create one if none exists), and add/update ``. When the file is `Directory.Packages.props`, use `` instead of ``. - - After writing, run `dotnet restore` at the props file's directory so the lockfile and `obj/project.assets.json` for every consuming project refresh. + - After writing, start `dotnet restore` at the props file's directory in the background so the lockfile and `obj/project.assets.json` for every consuming project refresh. - On success, trigger sidecar workspace reload for every project that transitively imports the modified file. - Return `modifiedFiles` so the UI can show a toast like `Updated Directory.Build.props`. -### 3.5 `sharplsp/nuget/uninstall` +### [NUGET-REQUESTS-UNINSTALL] `sharplsp/nuget/uninstall` Remove a NuGet package from a target. @@ -266,15 +251,13 @@ interface NuGetUninstallResponse { **Behavior by target kind:** -- `target.kind === "project"`: `dotnet remove package ` (CPM aware — if CPM is on and the package version lives in `Directory.Packages.props`, also prompt the user whether to remove the `` entry). -- `target.kind === "buildProps"`: edit the XML to remove the matching `` / `` node, then `dotnet restore`. +- `target.kind === "project"`: remove the matching `` from the project XML. With CPM, also prompt whether to remove its `` from `Directory.Packages.props`. +- `target.kind === "buildProps"`: edit the XML to remove the matching `` or `` node, then start background `dotnet restore`. - On success, trigger sidecar workspace reload. -## 3A. Loading State & Instant Feedback - -The current UI looks frozen because long-running operations (`dotnet add`, `dotnet restore`, search) give no visible feedback. That is a P0 bug. The spec now hard-requires the following: +## [NUGET-FEEDBACK] Loading and Feedback -### 3A.1 Spinners — every async operation +### [NUGET-FEEDBACK-SPINNERS] Spinners Every LSP round trip MUST show a spinner at a location that tells the user *what* is loading. Spinners use the Material Symbols `progress_activity` icon with a CSS `@keyframes spin` rotation (1 s linear infinite). No emoji, no text-only "Loading…". @@ -287,7 +270,7 @@ Every LSP round trip MUST show a spinner at a location that tells the user *what | `sharplsp/nuget/install` / `update` | Spinner replaces the Install button label ("Installing…" + spinner). Details panel shows a progress strip. | Global non-blocking toast: `Installing into …` | | `sharplsp/nuget/uninstall` | Spinner replaces the Uninstall button label. | Global toast. | -### 3A.2 Optimistic UI +### [NUGET-FEEDBACK-OPTIMISTIC] Optimistic UI Install / uninstall MUST update the UI optimistically: @@ -296,19 +279,19 @@ Install / uninstall MUST update the UI optimistically: 3. On success, swap the spinner for a checkmark for 1.5 s, then clear. 4. On failure, revert the optimistic state AND show an error toast with the LSP error message. -### 3A.3 Cancellation +### [NUGET-FEEDBACK-CANCELLATION] Cancellation Every spinner-bearing operation MUST be cancellable. When the user switches targets, re-types in the search box, or navigates away, any in-flight request for the previous state MUST be cancelled via LSP `$/cancelRequest`. The Rust host MUST honor cancellation — in particular, `dotnet` child processes spawned for a cancelled request MUST be killed. -### 3A.4 Install latency budget +### [NUGET-FEEDBACK-LATENCY] Install Latency Budget -`dotnet add` on a warm machine typically takes 2–8 s because of NuGet restore. That's **not acceptable as a blocking modal**. The contract is: +Install and restore MUST NOT block the UI: -- **< 100 ms**: optimistic UI update is visible (§ 3A.2 step 1). -- **< 500 ms**: spinner + toast visible (§ 3A.1). -- **Host-side fast path**: for `kind: "project"` without CPM, the host MUST edit the csproj XML directly to add the `` first, *then* fire `dotnet restore` in the background. The LSP `install` response returns as soon as the XML edit is committed (typically < 50 ms). The subsequent restore is reported via a separate `sharplsp/nuget/restoreProgress` notification (see § 3.6) so the UI can keep its spinner until restore finishes, without blocking the user from clicking Install on the next package. +- **< 100 ms**: the [NUGET-FEEDBACK-OPTIMISTIC] update is visible. +- **< 500 ms**: the [NUGET-FEEDBACK-SPINNERS] spinner and toast are visible. +- **Host-side fast path**: for `kind: "project"` without CPM, the host MUST edit the project XML to add the ``, then run `dotnet restore` in the background. The `install` response returns after the XML edit commits, typically in <50 ms. [NUGET-FEEDBACK-RESTORE] keeps the spinner active until restore finishes without blocking further package operations. -### 3.6 `sharplsp/nuget/restoreProgress` (server → client notification) +### [NUGET-FEEDBACK-RESTORE] `sharplsp/nuget/restoreProgress` ```typescript interface NuGetRestoreProgress { @@ -320,19 +303,11 @@ interface NuGetRestoreProgress { Fired by the Rust host while `dotnet restore` runs in the background after a fast-path XML edit. The extension routes these to the webview so the spinner can stay alive and the toast updates (`Restoring…` → `Restored` / `Restore failed`). -## 4. Webview UI +## [NUGET-WEBVIEW] Webview UI -### 4.1 Design +### [NUGET-WEBVIEW-DESIGN] Design -The NuGet browser uses a webview panel rendered by the editor extension. The design follows the Material Design 3 dark theme specified in `docs/designs/code.html`. - -> ⚠️ **CRITICAL — Read [`docs/designs/DESIGN.md`](../designs/DESIGN.md) § 0 -> before touching this UI.** The mockups in `code.html` and `screen.png` show -> a full IDE window for context. The activity bar (left icon column) and -> status bar (blue bar at the bottom of the mockup) belong to **VS Code -> itself** and **MUST NOT** be reimplemented in the webview panel. The panel -> renders **only** the header (tabs + search + refresh), package list, and -> details panel — nothing else. +The NuGet browser uses a webview panel rendered by the editor extension and the Material Design 3 dark theme in `docs/designs/code.html`. The mockups also show VS Code's activity and status bars for context; the webview MUST render only its header, package list, and details panel. See [`docs/designs/DESIGN.md`](../designs/DESIGN.md). **Key design requirements:** - Material Symbols Outlined icons (NOT emoji) @@ -340,22 +315,15 @@ The NuGet browser uses a webview panel rendered by the editor extension. The des - M3 dark color tokens (see `docs/designs/code.html` tailwind config) - Two-column layout: package list | details panel - Tabs: Browse | Installed -- **Target dropdown** (§ 3.0.3) between tabs and search — lists projects AND `Directory.Build.props` / `Directory.Packages.props` -- **Spinners** for every async op (§ 3A.1) — no blank/frozen states ever -- **NO** activity bar (VS Code provides one) -- **NO** status bar (VS Code provides one) +- **Target dropdown** ([NUGET-REQUESTS-TARGET-UI]) between tabs and search — lists projects AND `Directory.Build.props` / `Directory.Packages.props` +- **Spinners** for every async operation ([NUGET-FEEDBACK-SPINNERS]) - **NO** decorative buttons without real handlers -### 4.2 Layout Structure - -The panel renders only what's inside the editor area. Activity bar and -status bar shown below are **VS Code's own chrome** — drawn here for -orientation only, NOT part of the panel. +### [NUGET-WEBVIEW-LAYOUT] Layout Structure ``` -[VS Code activity bar — NOT part of panel] +-----------------------------------------------------------------+ -| Header: [logo] [Browse|Installed] [Target ▾] [search] [refresh] | ← panel starts +| Header: [logo] [Browse|Installed] [Target ▾] [search] [refresh] | +---------------------------+-------------------------------------+ | Package List | Details Panel | | | | @@ -364,8 +332,7 @@ orientation only, NOT part of the panel. | [Package Item] | [Description] | | [Package Item] | [Info Grid] | | | [Tags] | -+---------------------------+-------------------------------------+ ← panel ends -[VS Code status bar — NOT part of panel] ++---------------------------+-------------------------------------+ ``` Target dropdown contents (example): @@ -380,7 +347,7 @@ Build Props src/Directory.Packages.props (CPM) ``` -### 4.3 Extension Responsibilities +### [NUGET-WEBVIEW-EXTENSION] Extension Responsibilities The extension is responsible ONLY for: 1. Creating and managing the webview panel lifecycle @@ -395,20 +362,20 @@ The extension MUST NOT: - Parse .csproj/.fsproj files - Perform any NuGet logic -### 4.4 Message Flow +### [NUGET-WEBVIEW-FLOW] Message Flow ``` User clicks "Install" in webview -> webview postMessage({ command: "install", data: { packageId, version } }) -> extension receives message - -> extension sends LSP request: sharplsp/nuget/install { projectPath, packageId, version } - -> Rust host executes dotnet add ... - -> Rust host returns { success: true, message: "..." } + -> extension sends LSP request: sharplsp/nuget/install { target, packageId, version } + -> Rust host edits the target XML and starts background restore + -> Rust host returns { success: true, message: "...", modifiedFiles: [...] } -> extension forwards result to webview -> webview updates UI ``` -## 5. Error Handling +## [NUGET-ERRORS] Error Handling All LSP responses use `Result` semantics: - Success: return the typed response @@ -418,9 +385,9 @@ The extension displays errors via: - `vscode.window.showErrorMessage()` for critical failures - Inline error state in the webview for recoverable errors (e.g., search timeout) -## 6. Performance Targets +## [NUGET-PERFORMANCE] Performance Targets -Every target below is **end-to-end, user-perceived** — measured from click to UI update, not just from LSP send to LSP response. Spinners (§ 3A.1) MUST appear within the "first paint" budget of each row. +Every target below is end-to-end, measured from click to UI update. [NUGET-FEEDBACK-SPINNERS] MUST appear within each row's first-paint budget. | Operation | First paint (spinner/optimistic) | LSP response | Full completion | Method | |-----------|----------------------------------|--------------|-----------------|--------| @@ -433,11 +400,9 @@ Every target below is **end-to-end, user-perceived** — measured from click to | Install (buildProps) | < 100 ms (optimistic) | **< 200 ms** (XML edit) | restore < 10 s (background) | Host edits props XML, then background restore at the props directory. | | Uninstall | < 100 ms (optimistic) | < 200 ms (XML edit) | restore < 10 s (background) | Same fast-path model as install. | -**Non-negotiable:** the user must never wait > 200 ms for the Install button to visibly respond. If the restore is slow, the spinner keeps spinning in the background — the user is free to keep browsing, installing other packages, or close the panel. - -## 7. Testing +## [NUGET-TESTS] Testing -### 7.1 Rust LSP Host Tests (E2E) +### [NUGET-TESTS-HOST] Rust LSP Host Tests - [ ] `sharplsp/nuget/targets` enumerates all `.csproj`, `.fsproj`, `Directory.Build.props`, `Directory.Packages.props` in workspace - [ ] `sharplsp/nuget/targets` detects Central Package Management @@ -460,7 +425,7 @@ Every target below is **end-to-end, user-perceived** — measured from click to - [ ] Error handling: nonexistent package returns error - [ ] Error handling: malformed `Directory.Build.props` returns a structured parse error -### 7.2 Extension Tests (VSIX) +### [NUGET-TESTS-EXTENSION] Extension Tests - [ ] NuGet browser panel opens from command - [ ] Panel reuses existing instance (singleton) @@ -482,7 +447,7 @@ Every target below is **end-to-end, user-perceived** — measured from click to - [ ] Tab switching triggers correct data reload - [ ] Panel disposes cleanly -## 8. Editor Support Matrix +## [NUGET-EDITORS] Editor Support Matrix | Editor | NuGet Search | Install/Remove | Browse UI | |--------|-------------|----------------|-----------| diff --git a/docs/specs/PACKAGE-MAINTENANCE-SPEC.md b/docs/specs/PACKAGE-MAINTENANCE-SPEC.md index 399c775f..5745168c 100644 --- a/docs/specs/PACKAGE-MAINTENANCE-SPEC.md +++ b/docs/specs/PACKAGE-MAINTENANCE-SPEC.md @@ -1,155 +1,80 @@ -# Package Maintenance Spec +# [PKG] Package Maintenance Specification -Solution-Explorer context-menu actions that keep a .NET solution's NuGet -references tidy. Two operations, both editor-agnostic (the logic lives in the -Rust host / sidecars; the VS Code extension is a thin shell). +The Rust host and sidecars implement two NuGet-maintenance operations exposed through Solution Explorer; the VS Code extension is a thin client. -All NuGet plumbing reuses the existing `src/nuget/` module — `xml_edit` -(trivia-preserving `PackageReference`/`PackageVersion` edits), `targets` -(workspace/solution enumeration + CPM detection), `cli` (`dotnet list` / -`restore`) and the `sharplsp/nuget/*` request family. No new XML editor, no new -restore pipeline. +Both operations MUST reuse `src/nuget/`: `xml_edit` for trivia-preserving `PackageReference`/`PackageVersion` edits, `targets` for workspace/solution enumeration and CPM detection, `cli` for `dotnet list`/`restore`, and the `sharplsp/nuget/*` request family. They MUST NOT introduce another XML editor or restore pipeline. -C# and F# are equal first-class citizens: unused-package detection is wired for -both Roslyn (`.csproj`) and FSharp.Compiler.Service (`.fsproj`). +Unused-package detection MUST support Roslyn `.csproj` and FSharp.Compiler.Service `.fsproj` projects. ## [PKG-UNUSED] Remove Unused Packages -Remove direct `` entries whose assemblies are not referenced -by any code in the project. Available on **project** nodes and on the -**solution** node (where it runs across every project). +Remove direct `` entries whose assemblies are not referenced by project code. The action is available on project nodes and on the solution node, where it runs across every project. ### [PKG-UNUSED-DETECT-CS] C# detection (Roslyn) -For a `.csproj`, the C# sidecar resolves the project in its loaded -`MSBuildWorkspace`, builds the `Compilation`, and calls -`Compilation.GetUsedAssemblyReferences()` — Roslyn's canonical "which references -are actually used" API. Every `PortableExecutableReference` is classified as -used / unused; assembly file paths are mapped back to packages -(see [PKG-UNUSED-MAP]). A package is **unused** iff it contributes at least one -compile-time assembly to the compilation and **none** of those assemblies is in -the used set. +For a `.csproj`, the C# sidecar resolves the project in its loaded `MSBuildWorkspace`, builds the `Compilation`, and calls `Compilation.GetUsedAssemblyReferences()`. Every `PortableExecutableReference` is classified as used or unused, and assembly paths are mapped to packages through [PKG-UNUSED-MAP]. A package is unused iff it contributes at least one compile-time assembly and none of those assemblies is in the used set. ### [PKG-UNUSED-DETECT-FS] F# detection (FCS) -For an `.fsproj`, the F# sidecar resolves the package's compile assemblies from -`obj/project.assets.json`, builds an **isolated** `FSharpProjectOptions` that -includes those `-r:` references (the persistent workspace options are left -untouched so other F# features are unaffected), runs `ParseAndCheckProject` -with `keepAssemblyContents = true`, and walks the typed assembly contents to -collect the set of assemblies whose entities are actually referenced. The -used/unused classification and package mapping are identical to the C# path. +For an `.fsproj`, the F# sidecar resolves package compile assemblies from `obj/project.assets.json`, builds isolated `FSharpProjectOptions` containing those `-r:` references without modifying persistent workspace options, runs `ParseAndCheckProject` with `keepAssemblyContents = true`, and walks typed assembly contents to collect assemblies whose entities are referenced. Classification and package mapping then follow the C# path. ### [PKG-ASSETS-FS] Restored-package reference resolution -`FSharpAssets` is the single source of truth for turning `obj/project.assets.json` -into FCS `-r:` reference arguments, shared by the persistent workspace options -and the unused-package analysis so the compiler sees one reference set across -diagnostics, hover, and usage. +`FSharpAssets` is the single source of truth for turning `obj/project.assets.json` into FCS `-r:` arguments. Persistent workspace options and unused-package analysis MUST use it so diagnostics, hover, and usage share one reference set. Rules: -- **Fail-safe**: a missing or malformed assets file yields no references (the - caller falls back to framework-only options) rather than an error. -- **Existence-gated**: compile paths that do not exist on disk are dropped — - a missing-assembly reference would itself surface as a false diagnostic. -- **Placeholders are never references**: NuGet emits `_._` placeholder files - ("no assemblies for this TFM") **path-qualified** in the compile section - (e.g. `lib/netstandard1.0/_._` from `netstandard.library`), and the - placeholder physically exists inside the package folder. The filter must - match the *filename component*, not the whole compile key — handing `_._` - to FCS as a reference attaches FS0229/FS3160 startup errors to **every** - checked file: standing phantom errors no edit can clear (GitHub #160, - observed against FsToolkit.ErrorHandling). +- **Fail-safe**: a missing or malformed assets file yields no references; the caller falls back to framework-only options. +- **Existence-gated**: compile paths that do not exist on disk are dropped because a missing-assembly reference would produce a false diagnostic. +- **Placeholders are never references**: NuGet emits path-qualified `_._` compile placeholders (for example `lib/netstandard1.0/_._`) that can exist physically. The filter MUST compare the filename component, not the whole compile key; passing `_._` to FCS attaches FS0229/FS3160 startup errors to every checked file (GitHub #160). ### [PKG-UNUSED-MAP] Assembly → package mapping -NuGet restores package assemblies under the global packages folder as -`///lib//.dll`. The package -id is the path segment immediately under the global packages root. The mapping -is a pure function over the assembly path and is therefore unit-testable without -a live compilation. Assemblies that do not resolve to a package (framework -reference assemblies, project-to-project references) are ignored — they are -never reported as unused packages. +NuGet restores package assemblies as `///lib//.dll`; the package ID is the segment immediately below the global packages root. Assemblies outside a package, including framework and project references, are ignored and never reported as unused packages. -Conservatism is mandatory: a package is only ever reported unused when it has a -resolvable compile assembly that is provably not used. Packages contributing no -compile-time assembly (analyzers, build/tooling, MSBuild-only, runtime -metapackages) are **never** flagged, because their usage cannot be proven from -the compilation reference set. +A package is reported unused only when it has a resolvable compile assembly that is provably unused. Packages with no compile-time assembly, including analyzers, build/tooling, MSBuild-only packages, and runtime metapackages, are never flagged. ### [PKG-UNUSED-REQUEST] Request flow -`sharplsp/nuget/unused` (host request): params carry the project path (and -optional solution-wide flag). The host picks the sidecar by file extension, -forwards a `project/unusedPackages` sidecar request, intersects the returned -candidate ids with the project's direct `` ids (a transitive -dependency is never in the project file and must never be "removed"), and -returns `{ projectPath, unused: [{ id, version }] }`. +`sharplsp/nuget/unused` params carry the project path and optional solution-wide flag. The host selects the sidecar by file extension, forwards `project/unusedPackages`, intersects returned candidate IDs with direct `` IDs so transitive dependencies are never removed, and returns `{ projectPath, unused: [{ id, version }] }`. -Removal reuses the existing `sharplsp/nuget/uninstall` request per package id — -trivia-preserving XML removal plus a background restore. The host does not -invent a second removal path. +Removal MUST reuse `sharplsp/nuget/uninstall` per package ID, including trivia-preserving XML removal and a background restore. ### [PKG-UNUSED-UI] UX -- Command `sharplsp.removeUnusedPackages`, shown on `viewItem == project` and - `viewItem == solution` in `sharplsp.solutionExplorer`. -- Detect first; if none are unused, inform and stop. Otherwise show a modal - listing the packages to be removed and require explicit confirmation - (destructive, behaviour-changing). -- On the solution node, detection + confirmation aggregate across all projects; - the confirmation names each project and its unused packages. +- Command `sharplsp.removeUnusedPackages`, shown on `viewItem == project` and `viewItem == solution` in `sharplsp.solutionExplorer`. +- Detect first; if none are unused, inform and stop. Otherwise show a modal listing the packages to remove and require explicit confirmation. +- On the solution node, detection and confirmation aggregate across all projects; the confirmation names each project and its unused packages. - After removal the Solution Explorer refreshes reactively. ## [PKG-CONSOLIDATE] Consolidate Shared Packages to Directory.Build.props -Hoist NuGet packages that are referenced by **two or more** projects in the -solution into a single solution-root `Directory.Build.props`, declaring each -once and removing the per-project `` entries. Available on the -**solution** node. +Hoist NuGet packages referenced by two or more projects into a solution-root `Directory.Build.props`, declare each once, and remove their per-project `` entries. The action is available on the solution node. ### [PKG-CONSOLIDATE-SCAN] Scan -Enumerate every project under the solution directory (reuse `targets`). Parse -each project's direct `` ids + versions. A package is -**shared** when it appears in ≥ 2 projects. When versions differ across -projects the highest (by semantic ordering, lexical fallback) is chosen and the -divergence is reported. +Enumerate every project under the solution directory through `targets` and parse direct `` IDs and versions. A package is shared when it appears in at least two projects. When versions differ, select the highest by semantic ordering with lexical fallback and report the divergence. ### [PKG-CONSOLIDATE-APPLY] Apply -1. Ensure a `Directory.Build.props` exists at the solution root (create a - minimal `` if absent). -2. For each shared package, add it to `Directory.Build.props` and remove it from - every project that declared it, via `xml_edit`. -3. CPM-aware: when the solution has Central Package Management - (`Directory.Packages.props` with `ManagePackageVersionsCentrally=true`), the - hoisted `Directory.Build.props` entry is written **versionless** and the - version is ensured in `Directory.Packages.props` (``), matching - the existing install behaviour. +1. Ensure a `Directory.Build.props` exists at the solution root; create a minimal `` if absent. +2. For each shared package, use `xml_edit` to add it to `Directory.Build.props` and remove it from every declaring project. +3. With Central Package Management (`Directory.Packages.props` and `ManagePackageVersionsCentrally=true`), write a versionless `Directory.Build.props` entry and ensure its `` in `Directory.Packages.props`. 4. Fire a single background restore for the modified files. -Hoisting to `Directory.Build.props` makes a package apply solution-wide; the -result message states exactly which packages moved, at which version, and which -projects were edited so the behaviour change is explicit and auditable. +Because hoisted packages apply solution-wide, the result MUST name each moved package, selected version, and edited project. ### [PKG-CONSOLIDATE-REQUEST] Request flow -`sharplsp/nuget/consolidate` (host request): params carry the solution path -(and/or workspace root). Pure Rust — no sidecar. Returns -`{ moved: [{ id, version, fromProjects: [...] }], propsFile, modifiedFiles }`. +`sharplsp/nuget/consolidate` params carry the solution path and/or workspace root. The Rust host handles it without a sidecar and returns `{ moved: [{ id, version, fromProjects: [...] }], propsFile, modifiedFiles }`. ### [PKG-CONSOLIDATE-UI] UX - Command `sharplsp.consolidatePackages`, shown on `viewItem == solution`. -- Scan first; if nothing is shared, inform and stop. Otherwise show a modal - summarising what will move, then apply on confirmation and refresh. +- Scan first; if nothing is shared, inform and stop. Otherwise show a modal summarising what will move, then apply on confirmation and refresh. -## Non-goals +## [PKG-NONGOALS] Non-Goals - Transitive / framework / analyzer package pruning (cannot be proven unused). - Rewriting version ranges, floating versions, or condition-bearing references. -- Per-`` metadata (`PrivateAssets`, `IncludeAssets`) merging - beyond a straight hoist — references carrying item metadata are reported and - skipped rather than silently flattened. +- Per-`` metadata (`PrivateAssets`, `IncludeAssets`) merging beyond a straight hoist; references carrying item metadata are reported and skipped rather than silently flattened. diff --git a/docs/specs/PROFILER-SPEC.md b/docs/specs/PROFILER-SPEC.md index 4446a6b8..f9de77ec 100644 --- a/docs/specs/PROFILER-SPEC.md +++ b/docs/specs/PROFILER-SPEC.md @@ -1,18 +1,16 @@ -# Profiler Integration Specification +# [PROFILER] Profiler Integration Specification **Parent:** [SHARPLSP-SPEC.md](SHARPLSP-SPEC.md) -## 1. Overview +## [PROFILER-OVERVIEW] Overview -SharpLsp integrates .NET diagnostic tools (`dotnet-trace`, `dotnet-counters`, `dotnet-dump`) directly into the editor via LSP custom requests, giving developers a simple UI around the standard .NET diagnostics CLI. No external tools, no terminal juggling — profile, trace, and analyze memory leaks from your editor. +SharpLsp exposes `dotnet-trace`, `dotnet-counters`, and `dotnet-dump` through LSP custom requests and editor UI. **Reference:** [dotnet-trace documentation](https://learn.microsoft.com/en-us/dotnet/core/diagnostics/dotnet-trace) -**Priority:** P2 (Phase 5 — Beyond Parity) +## [PROFILER-TOOLS] Diagnostic Tools -## 2. Diagnostic Tools - -### 2.1 dotnet-trace +### [PROFILER-TOOLS-TRACE] dotnet-trace Collects performance traces from running .NET processes using EventPipe. Produces `.nettrace` files convertible to Chromium/SpeedScope formats for visualization. @@ -23,7 +21,7 @@ Collects performance traces from running .NET processes using EventPipe. Produce | Stop trace | Ctrl+C equivalent | Gracefully stop collection | | Convert trace | `dotnet-trace convert` | Convert `.nettrace` to `.speedscope.json` or Chromium format | -### 2.2 dotnet-counters +### [PROFILER-TOOLS-COUNTERS] dotnet-counters Real-time monitoring of .NET runtime performance counters (GC, CPU, exceptions, thread pool). @@ -33,7 +31,7 @@ Real-time monitoring of .NET runtime performance counters (GC, CPU, exceptions, | Monitor counters | `dotnet-counters monitor -p ` | Stream live counter values | | Collect counters | `dotnet-counters collect -p ` | Record counters to CSV/JSON | -### 2.3 dotnet-dump (Memory Leak Tracing) +### [PROFILER-TOOLS-DUMP] dotnet-dump Captures and analyzes process dumps for memory leak investigation without a native debugger. @@ -45,9 +43,9 @@ Captures and analyzes process dumps for memory leak investigation without a nati | GC roots | `gcroot ` | Trace GC root references for an object | | Object references | `dumpobj ` | Inspect individual managed objects | -## 3. Architecture +## [PROFILER-ARCHITECTURE] Architecture -### 3.1 Component Placement +### [PROFILER-ARCHITECTURE-PLACEMENT] Component Placement Profiler integration lives in the **Rust LSP host** (Tier 1). The diagnostic CLI tools run as child processes managed by the host — no sidecar involvement. @@ -59,14 +57,14 @@ Editor ──LSP custom request──▶ Rust Host ──spawns──▶ dot └── Output parsing + streaming to editor ``` -### 3.2 Why Rust Host, Not Sidecar +### [PROFILER-ARCHITECTURE-HOST] Rust Host Ownership - Diagnostic tools are standalone CLI executables, not Roslyn/FCS APIs - No workspace or compilation context needed - Direct process spawning from Rust is simpler and lower latency - Sidecar crash must not kill profiling sessions -### 3.3 Tool Discovery +### [PROFILER-ARCHITECTURE-DISCOVERY] Tool Discovery On startup (lazy, first use), the host locates diagnostic tools: @@ -76,11 +74,11 @@ On startup (lazy, first use), the host locates diagnostic tools: | 2 | Check `dotnet tool list -g` output | — | | 3 | If missing, prompt user to install via `dotnet tool install -g` | Return error with install instructions | -## 4. LSP Custom Requests +## [PROFILER-PROTOCOL] LSP Custom Requests All profiler requests use the `sharplsp/` namespace. -### 4.1 Process Discovery +### [PROFILER-PROCESS-LIST] Process Discovery **Method:** `sharplsp/profiler/listProcesses` @@ -102,7 +100,7 @@ type ListProcessesResult = DotNetProcess[]; Calls `dotnet-trace ps` and parses output. Returns all discoverable .NET processes. -### 4.2 Trace Session +### [PROFILER-TRACE] Trace Session **Method:** `sharplsp/profiler/startTrace` @@ -147,9 +145,9 @@ interface StopTraceResult { } ``` -### 4.2.1 Trace File Conversion +#### [PROFILER-TRACE-CONVERSION] Trace File Conversion -A `.nettrace` file is not directly viewable — it must be converted to SpeedScope JSON (or Chromium JSON) before it can be opened in a visualizer. SharpLsp exposes an explicit conversion entrypoint so that any trace file on disk (including orphaned files from a previous session, a colleague's dump, or a CI artifact) can be opened in SharpLsp without re-recording. +A `.nettrace` file MUST be converted to SpeedScope or Chromium JSON before visualization. The conversion request accepts any trace file on disk and does not require a live session. **Method:** `sharplsp/profiler/convertTrace` @@ -180,9 +178,9 @@ Invokes `dotnet-trace convert --format `. The resulting sibling | `speedscope` | `.speedscope.json` | | `chromium` | `.chromium.json` | -Stopping a trace session (`sharplsp/profiler/stopTrace`) already runs this conversion automatically when the session produced data. `convertTrace` is for files where no live session exists — for example, when the editor was closed during recording, or when opening a `.nettrace` the user recorded elsewhere. +`sharplsp/profiler/stopTrace` automatically converts a session that produced data; `convertTrace` handles files with no live session. -### 4.3 Counter Monitoring +### [PROFILER-PROTOCOL-COUNTERS] Counter Monitoring **Method:** `sharplsp/profiler/startCounters` @@ -232,7 +230,7 @@ interface StopCountersParams { } ``` -### 4.4 Memory Dump Collection +### [PROFILER-PROTOCOL-DUMP-COLLECT] Memory Dump Collection **Method:** `sharplsp/profiler/collectDump` @@ -255,7 +253,7 @@ interface CollectDumpResult { } ``` -### 4.5 Memory Dump Analysis +### [PROFILER-PROTOCOL-DUMP-ANALYZE] Memory Dump Analysis **Method:** `sharplsp/profiler/analyzeHeap` @@ -311,11 +309,11 @@ interface GCRootNode { type FindGCRootsResult = GCRootChain[]; ``` -## 5. Memory Leak Tracing Workflow +## [PROFILER-LEAKS] Memory Leak Tracing Workflow Memory leak investigation follows a structured workflow exposed through the UI: -### 5.1 Baseline → Exercise → Compare +### [PROFILER-LEAKS-WORKFLOW] Baseline → Exercise → Compare | Step | Action | Tool | |------|--------|------| @@ -326,7 +324,7 @@ Memory leak investigation follows a structured workflow exposed through the UI: | 5 | Identify growing types | Editor diff view of heap stats | | 6 | Trace GC roots of suspect objects | `sharplsp/profiler/findGCRoots` | -### 5.2 Live Counter Monitoring for Leak Detection +### [PROFILER-LEAKS-COUNTERS] Live Counter Monitoring Monitor `System.Runtime` counters to detect leaks in real-time: @@ -339,11 +337,11 @@ Monitor `System.Runtime` counters to detect leaks in real-time: The editor highlights counters that show sustained growth patterns. -### 5.3 Automated Leak Detection +### [PROFILER-LEAKS-AUTOMATION] Automated Leak Detection -SharpLsp automatically detects memory leaks by comparing two heap snapshots taken at different points in time. The user triggers "Baseline → Exercise → Compare" and SharpLsp does the analysis automatically. +Automated leak detection compares baseline and comparison heap snapshots. -#### 5.3.1 Heap Snapshot Diffing +#### [PROFILER-LEAKS-AUTOMATION-DIFF] Heap Snapshot Diffing **Method:** `sharplsp/profiler/diffHeapSnapshots` @@ -396,7 +394,7 @@ interface LeakSuspect { } ``` -#### 5.3.2 Leak Classification Heuristics +#### [PROFILER-LEAKS-AUTOMATION-HEURISTICS] Leak Classification Heuristics SharpLsp classifies leak suspects by combining snapshot diff data with heuristics: @@ -411,7 +409,7 @@ Additional signals that elevate severity: - Type contains `[]` or `List` (collection growth) - Multiple instances of the same generic type growing (e.g., `Dictionary` with different type args) -#### 5.3.3 Automated Leak Detection Flow +#### [PROFILER-LEAKS-AUTOMATION-FLOW] Automated Leak Detection Flow ```mermaid flowchart TD @@ -430,11 +428,11 @@ flowchart TD M --> N[Show Retention Path] ``` -## 5A. Object Graph Visualization +## [PROFILER-GRAPH] Object Graph Visualization -SharpLsp provides an interactive object retention graph that shows what objects exist in memory and what's holding on to them. This is the killer feature for memory leak investigation — you see the actual reference chains keeping objects alive. +SharpLsp provides an interactive graph of objects and the reference chains retaining them. -### 5A.1 Object Graph Data Model +### [PROFILER-GRAPH-DATA] Object Graph Data Model **Method:** `sharplsp/profiler/getObjectGraph` @@ -502,7 +500,7 @@ interface ObjectGraphStats { } ``` -### 5A.2 Object Inspection +### [PROFILER-GRAPH-INSPECTION] Object Inspection **Method:** `sharplsp/profiler/inspectObject` @@ -541,7 +539,7 @@ interface ObjectField { } ``` -### 5A.3 Architecture — How the Object Graph is Built +### [PROFILER-GRAPH-BUILD] Object Graph Construction The object graph is assembled from `dotnet-dump analyze` commands: @@ -566,34 +564,15 @@ Commands used per node: | `dumpheap -mt ` | Count all instances of a specific method table | | `objsize ` | Calculate retained size (object + transitive refs) | -### 5A.4 Interactive Graph Webview - -The object graph renders as an interactive force-directed graph in a VSCode webview panel. +### [PROFILER-GRAPH-WEBVIEW] Interactive Graph Webview -#### Graph Layout +The object graph renders as an interactive force-directed graph in a VS Code webview panel. -```mermaid -graph LR - subgraph GC Roots - R1[Static Field
AppState._cache] - R2[Thread Stack
Main] - end +#### [PROFILER-GRAPH-WEBVIEW-LAYOUT] Graph Layout - subgraph Retention Chain - A[Dictionary<string,Widget>
1.2 MB retained] - B[Widget[]
entries array] - C[Widget
48 bytes] - D[EventHandler
leak suspect ⚠️] - end +GC roots and retention chains MUST be connected by labelled reference edges; roots appear before retained objects in the initial layout. - R1 -->|_cache| A - A -->|entries| B - B -->|[42]| C - C -->|OnClick| D - R2 -->|local| A -``` - -#### Webview Features +#### [PROFILER-GRAPH-WEBVIEW-FEATURES] Webview Features | Feature | Description | |---------|-------------| @@ -610,35 +589,19 @@ graph LR | **Export** | Save graph as SVG or PNG | | **Depth slider** | Control max traversal depth (1–10) | -#### Node Visual Encoding - -```mermaid -graph TD - subgraph Legend - L1[🔴 Leak Suspect
High severity] - L2[🟠 Large Retained Size
> 1MB] - L3[🔵 GC Root
Static/Thread/Pinned] - L4[⚪ Normal Object
No concerns] - L5[⚠️ Warning Border
Growing type from diff] - end -``` - -### 5A.5 Retention Path View +#### [PROFILER-GRAPH-WEBVIEW-ENCODING] Node Visual Encoding -For any selected object, SharpLsp shows the complete chain from GC root to the object. This answers the question: **"Why isn't this being garbage collected?"** +| Node state | Encoding | +|------------|----------| +| High-severity leak suspect | Red | +| Retained size greater than 1MB | Orange | +| GC root | Blue | +| Normal object | Gray | +| Type growing between snapshots | Warning border | -```mermaid -graph TD - Root["🔵 GC Root
Static: AppState._instance"] --> A["AppState
retains 4.2 MB"] - A -->|_subscriptions| B["List<EventHandler>
retains 2.1 MB"] - B -->|[0]| C["EventHandler
retains 1.0 MB"] - C -->|_target| D["🔴 LeakyService
48 bytes
⚠️ 1,247 instances"] - D -->|_buffer| E["byte[]
1.0 MB"] +### [PROFILER-GRAPH-RETENTION] Retention Path View - style Root fill:#4488ff,color:#fff - style D fill:#ff4444,color:#fff - style E fill:#ff8844,color:#fff -``` +For any selected object, SharpLsp shows the complete chain from a GC root to that object. Each node in the retention path shows: - Type name and size @@ -646,26 +609,22 @@ Each node in the retention path shows: - Instance count (if many instances of same type exist — leak signal) - Retained size (total memory kept alive through this node) -### 5A.6 Heap Snapshot Diff Visualization +### [PROFILER-GRAPH-DIFF] Heap Snapshot Diff Visualization -When two snapshots are compared, the diff is shown as an annotated table AND as a visual graph overlay. +When two snapshots are compared, the diff is shown as an annotated table and a visual graph overlay. -#### Diff Table View +#### [PROFILER-GRAPH-DIFF-TABLE] Diff Table View -| Type | Baseline Count | Current Count | Delta | Baseline Size | Current Size | Delta | Severity | -|------|---------------|--------------|-------|--------------|-------------|-------|----------| -| `EventHandler` | 12 | 1,247 | +1,235 | 576 B | 59.9 KB | +59.3 KB | 🔴 High | -| `byte[]` | 340 | 1,580 | +1,240 | 1.2 MB | 5.6 MB | +4.4 MB | 🔴 High | -| `String` | 8,200 | 9,100 | +900 | 320 KB | 355 KB | +35 KB | 🟡 Low | +The table MUST show type, baseline and current counts, count delta, baseline and current sizes, size delta, and severity. -#### Diff Graph Overlay +#### [PROFILER-GRAPH-DIFF-OVERLAY] Diff Graph Overlay In graph view, nodes from the comparison snapshot are annotated with growth indicators: - **Pulsing red border** — count grew >100% - **Growing arrow** — size delta shown on hover - **New nodes** (not in baseline) appear with dashed border -### 5A.7 Performance Requirements +### [PROFILER-GRAPH-PERFORMANCE] Performance Requirements | Metric | Target | |--------|--------| @@ -677,9 +636,9 @@ In graph view, nodes from the comparison snapshot are annotated with growth indi | Graph webview node expansion | <1s | | Retained size calculation | <5s per node | -## 6. Session Management +## [PROFILER-SESSIONS] Session Management -### 6.1 Session Lifecycle +### [PROFILER-SESSIONS-LIFECYCLE] Session Lifecycle ``` Created ──start──▶ Running ──stop──▶ Stopped ──cleanup──▶ Disposed @@ -693,7 +652,7 @@ Created ──start──▶ Running ──stop──▶ Stopped ──clea - Maximum concurrent sessions: 5 (configurable via `sharplsp.toml`) - Orphaned sessions (editor disconnect) cleaned up on LSP shutdown -### 6.2 Configuration +### [PROFILER-SESSIONS-CONFIG] Configuration `sharplsp.toml` settings: @@ -707,9 +666,9 @@ default_counter_interval = 1 output_directory = ".sharplsp/profiles" ``` -## 7. Editor Integration +## [PROFILER-EDITOR] Editor Integration -### 7.1 VSCode Extension +### [PROFILER-EDITOR-VSCODE] VS Code Extension | UI Element | Purpose | |-----------|---------| @@ -723,11 +682,11 @@ output_directory = ".sharplsp/profiles" | Quick pick | Process selection from discovered .NET processes | | File open | Open `.speedscope.json` output in browser/SpeedScope viewer | -### 7.1.1 Profiler Tree View — Intent-Revealing UX +#### [PROFILER-EDITOR-VSCODE-TREE] Profiler Tree View -The PROFILER tree view MUST make every action discoverable **directly from the node the user is looking at**. A user who right-clicks a session must be able to stop it. A user who right-clicks a process must be able to profile it. No toolbar hunting. No blind QuickPicks. +The PROFILER tree view MUST expose session and process actions directly from the corresponding node: sessions can be stopped and processes can be profiled from their context menus. -#### Tree Structure +##### [PROFILER-EDITOR-VSCODE-TREE-STRUCTURE] Tree Structure ``` PROFILER [refresh] [open-trace] [⋯ overflow] @@ -739,7 +698,7 @@ PROFILER [refresh] [open-trace] [⋯ overflow] └── Claude (PID 98153) ``` -#### Context Values +##### [PROFILER-EDITOR-VSCODE-TREE-CONTEXT] Context Values Every tree item MUST set a `contextValue` that the `view/item/context` menu `when` clauses key off: @@ -751,18 +710,18 @@ Every tree item MUST set a `contextValue` that the `view/item/context` menu `whe | Counters session | `profiler-session-counters` | | Process entry | `profiler-process` | -#### Default Click Behavior +##### [PROFILER-EDITOR-VSCODE-TREE-CLICK] Default Click Behavior Clicking a node performs the most common action for that node kind — never a no-op. -| Node | Default Click | Rationale | -|------|--------------|-----------| -| Trace session | Stop trace + open result in SpeedScope | Click = "I'm done, show me the flamegraph." | -| Counters session | Reveal the live counters webview | Click = "Show me the numbers" (stopping is a menu item). | -| Process | Start trace on this PID | Click = "profile this." | -| Header / empty | No-op | Informational. | +| Node | Default Click | +|------|---------------| +| Trace session | Stop trace and open the result in SpeedScope | +| Counters session | Reveal the live counters webview | +| Process | Start trace on this PID | +| Header / empty | No-op | -#### Context Menu (Right-Click) Entries +##### [PROFILER-EDITOR-VSCODE-TREE-MENU] Context Menu Entries **On a trace session:** - Stop & Open (inline icon = `debug-stop`) @@ -779,16 +738,14 @@ Clicking a node performs the most common action for that node kind — never a n - Collect Memory Dump of This Process - Copy PID -#### Tooltips +##### [PROFILER-EDITOR-VSCODE-TREE-TOOLTIPS] Tooltips Every session and process node MUST have a Markdown tooltip that includes: - Node identity (PID, session ID, kind) - Output path if any - A one-line hint describing what clicking does -This eliminates "what is this thing and what do I do with it?" confusion. - -#### Toolbar Organisation +##### [PROFILER-EDITOR-VSCODE-TREE-TOOLBAR] Toolbar Organisation The view title bar keeps only actions that don't belong to a specific node: @@ -798,20 +755,18 @@ The view title bar keeps only actions that don't belong to a specific node: | `navigation@2` | Open Trace File… | `folder-opened` | | `overflow` | Start Trace (picker), Start Counters (picker), Collect Dump (picker), Convert .nettrace, Analyze Heap, Compare Snapshots, Detect Leaks | — | -The overflow menu (`⋯`) holds picker-based workflows that don't need a visible button. All direct-action equivalents live on the tree node context menus. - -### 7.1.2 Trace File Opening +#### [PROFILER-EDITOR-VSCODE-TRACE] Trace File Opening -SharpLsp MUST let the user open a `.nettrace` **file** as a first-class action, not just as a side-effect of stopping a session. Users who find an orphaned `.nettrace` (e.g. because the editor was closed mid-recording) need a path forward. +SharpLsp MUST let the user open a `.nettrace` file independently of a live session. The `sharplsp.profiler.openTrace` command: 1. Shows an open-file dialog filtering for `.nettrace`, `.speedscope.json`, and `.json` files. 2. If the chosen file is `.nettrace`, invokes `sharplsp/profiler/convertTrace` to produce a sibling `.speedscope.json`. 3. Opens the resulting SpeedScope file in the external SpeedScope web viewer. -Stopping a trace session uses the same pipeline, so the UX is consistent: every trace — freshly captured or loaded from disk — ends up in SpeedScope with one interaction. +Stopping a trace session uses the same conversion-and-open pipeline. -### 7.2 Commands +### [PROFILER-EDITOR-COMMANDS] Commands | Command | Title | |---------|-------| @@ -837,7 +792,7 @@ Stopping a trace session uses the same pipeline, so the UX is consistent: every | `sharplsp.profiler.dumpProcess` | SharpLsp: Collect Memory Dump of This Process | | `sharplsp.profiler.copyPid` | SharpLsp: Copy PID | -## 8. Performance Requirements +## [PROFILER-PERFORMANCE] Performance Requirements | Metric | Target | |--------|--------| @@ -848,7 +803,7 @@ Stopping a trace session uses the same pipeline, so the UX is consistent: every | Heap analysis (50k types) | <5s | | GC root traversal | <10s | -## 9. Error Handling +## [PROFILER-ERRORS] Error Handling | Condition | Response | |-----------|----------| @@ -860,19 +815,6 @@ Stopping a trace session uses the same pipeline, so the UX is consistent: every | Tool produces unexpected output | Log raw output at `warn` level, return parse error | | Editor disconnects during session | Clean up all sessions on LSP shutdown | -## 10. Competitive Parity Matrix - -| Feature | VS | Rider | CDK | SharpLsp Target | Priority | -|---------|----|----|-----|-------------|----------| -| CPU trace collection | Yes | Yes | No | Yes | P0 | -| Live performance counters | Yes (PerfView) | Yes | No | Yes | P0 | -| Memory dump collection | Yes | Yes | No | Yes | P0 | -| Heap analysis | Yes | Yes (dotMemory) | No | Yes (basic) | P1 | -| GC root analysis | Yes | Yes (dotMemory) | No | Yes (basic) | P1 | -| Leak detection heuristics | Partial | Yes | No | Yes (counter-based) | P1 | -| Automated leak detection | No | Yes (dotMemory) | No | Yes (snapshot diff) | P1 | -| Heap snapshot diffing | No | Yes (dotMemory) | No | Yes | P1 | -| Object retention graph | Yes | Yes (dotMemory) | No | Yes (interactive) | P1 | -| Object inspection | Yes | Yes (dotMemory) | No | Yes | P1 | -| Flame graph visualization | External | Built-in | No | External (SpeedScope) | P1 | -| Allocation tracking | Yes | Yes (dotTrace) | No | Future | P2 | +## [PROFILER-SCOPE] Deferred Scope + +Allocation tracking is deferred; this specification defines no request or UI contract for it. diff --git a/docs/specs/REFERENCES-SPEC.md b/docs/specs/REFERENCES-SPEC.md index af5f45b9..4aca4402 100644 --- a/docs/specs/REFERENCES-SPEC.md +++ b/docs/specs/REFERENCES-SPEC.md @@ -1,16 +1,14 @@ -# Find All References & Document Highlights Specification +# Find All References & Document Highlights Specification `[REFERENCES]` **Parent:** [SHARPLSP-SPEC.md](SHARPLSP-SPEC.md) -## 1. Overview +## Overview `[REFERENCES-OVERVIEW]` -Find All References locates every usage of a symbol across the entire solution. Document Highlights locates usages within the current document only (used for read/write highlighting on cursor move). SharpLsp implements `textDocument/references` ([LSP 3.17 §3.17.10](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_references)) and `textDocument/documentHighlight` ([LSP 3.17 §3.17.5](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_documentHighlight)) for both C# and F# as equal first-class citizens. +`textDocument/references` ([LSP 3.17](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_references)) locates symbol usages across the solution. `textDocument/documentHighlight` ([LSP 3.17](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_documentHighlight)) locates read/write usages in the current document. Both methods apply equally to C# and F#. -Both methods are **P0** (launch blocker) and target Phase 2 delivery. +## LSP Protocol `[REFERENCES-PROTOCOL]` -## 2. LSP Protocol - -### 2.1 textDocument/references +### textDocument/references `[REFERENCES-PROTOCOL-FIND]` ``` method: textDocument/references @@ -37,7 +35,7 @@ interface Location { - `null` when no symbol can be resolved at the given position. - Results are sorted by file path, then by position within each file. -### 2.2 textDocument/documentHighlight +### textDocument/documentHighlight `[REFERENCES-PROTOCOL-HIGHLIGHT]` ``` method: textDocument/documentHighlight @@ -66,23 +64,25 @@ enum DocumentHighlightKind { - Each highlight is annotated with `Read` or `Write` kind where determinable. - `null` when no symbol can be resolved at the given position. -## 3. Request Routing +## Request Routing `[REFERENCES-ROUTING]` Both requests are **semantic** requests. The Rust host routes them to the appropriate sidecar based on document language. | Step | Component | Action | |---|---|---| | 1 | Rust host | Receives request, identifies language from VFS | -| 2 | Rust host | Checks salsa cache for matching key (see §7) | +| 2 | Rust host | Checks the navigation cache for a matching key (see [REFERENCES-CACHE]) | | 3 | Rust host | On cache miss, dispatches to C# sidecar (Roslyn) or F# sidecar (FCS) via IPC | | 4 | Sidecar | Resolves symbol at position, finds all reference locations | | 5 | Rust host | Caches result, returns LSP response to client | -The Rust host MAY use tree-sitter to pre-validate the position (reject whitespace, comments, string literals) and short-circuit with `null` before dispatching to the sidecar. +The Rust host MAY use tree-sitter to reject whitespace, comments, and string literals with `null` before sidecar dispatch. + +Implementation anchors: Rust routing and DTO conversion live in [`src/semantic.rs`](../../src/semantic.rs); C# dispatch, symbol resolution, and wire types live in [`CSharpSidecar.cs`](../../sidecars/SharpLsp.Sidecar.CSharp/CSharpSidecar.cs), [`DefinitionResolver.cs`](../../sidecars/SharpLsp.Sidecar.CSharp/Workspace/DefinitionResolver.cs), and [`Messages.cs`](../../sidecars/SharpLsp.Sidecar.CSharp/Messages.cs); F# behavior and wire types live in [`FSharpReferences.fs`](../../sidecars/SharpLsp.Sidecar.FSharp/FSharpReferences.fs) and [`FSharpWire.fs`](../../sidecars/SharpLsp.Sidecar.FSharp/FSharpWire.fs). Coarse protocol coverage is in [`tests/e2e_modules/references.rs`](../../tests/e2e_modules/references.rs). -## 4. C# Implementation (Roslyn) +## C# Implementation (Roslyn) `[REFERENCES-CSHARP]` -### 4.1 textDocument/references +### textDocument/references `[REFERENCES-CSHARP-FIND]` 1. Obtain `Document` from the current `Solution` snapshot for the given URI. 2. Get the source text and convert `(line, character)` to an absolute position via [`SourceText.Lines.GetPosition()`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.text.textlinecollection.getposition). @@ -94,9 +94,9 @@ The Rust host MAY use tree-sitter to pre-validate the position (reject whitespac 8. If `context.includeDeclaration` is true, also include the symbol's declaration location(s) from `ReferencedSymbol.Definition.Locations`. 9. Map each location to `(filePath, line, character, endLine, endCharacter)`. -### 4.2 textDocument/documentHighlight +### textDocument/documentHighlight `[REFERENCES-CSHARP-HIGHLIGHT]` -1. Steps 1–5 as in §4.1. +1. Resolve the `Document` and symbol as in [REFERENCES-CSHARP-FIND]. 2. Call [`SymbolFinder.FindReferencesAsync(symbol, solution)`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.findusages.symbolfinder.findreferencesasync) scoped to the current document. 3. Filter results to only locations within the requested document. 4. Classify each reference as `Read` or `Write`: @@ -105,7 +105,7 @@ The Rust host MAY use tree-sitter to pre-validate the position (reject whitespac - Declaration site → `Write` 5. Include the declaration location with `Write` kind. -### 4.3 Symbol Resolution Special Cases +### Symbol Resolution Special Cases `[REFERENCES-CSHARP-RESOLUTION]` | Symbol at Cursor | Behavior | |---|---| @@ -122,9 +122,9 @@ The Rust host MAY use tree-sitter to pre-validate the position (reject whitespac | `using` alias | References to the alias + aliased type | | Implicit references (attribute `[Foo]` → `FooAttribute`) | Include the implicit form | -## 5. F# Implementation (FCS) +## F# Implementation (FCS) `[REFERENCES-FSHARP]` -### 5.1 textDocument/references +### textDocument/references `[REFERENCES-FSHARP-FIND]` 1. Get `FSharpCheckFileResults` for the document via `FSharpChecker.CheckFileInProject()`. 2. Call `GetSymbolUseAtLocation(line, col, lineText, names)` to obtain the `FSharpSymbolUse` at the cursor. @@ -133,15 +133,15 @@ The Rust host MAY use tree-sitter to pre-validate the position (reject whitespac 5. If `context.includeDeclaration` is true, include the symbol's declaration range. 6. Map each `FSharpSymbolUse.Range` to LSP `Location`. -### 5.2 textDocument/documentHighlight +### textDocument/documentHighlight `[REFERENCES-FSHARP-HIGHLIGHT]` -1. Steps 1–3 as in §5.1 (document-scoped only). +1. Resolve document-scoped symbol uses as in [REFERENCES-FSHARP-FIND]. 2. Classify each `FSharpSymbolUse`: - `FSharpSymbolUse.IsFromDefinition` → `Write` - `FSharpSymbolUse.IsFromPattern` → `Write` - All other usages → `Read` -### 5.3 F#-Specific Cases +### F#-Specific Cases `[REFERENCES-FSHARP-CASES]` | Symbol at Cursor | Behavior | |---|---| @@ -152,7 +152,7 @@ The Rust host MAY use tree-sitter to pre-validate the position (reject whitespac | Module function | All call sites across the project | | Type abbreviation | All usages of the abbreviation | -## 6. Cross-Language References (P2) +## Cross-Language References `[REFERENCES-CROSS-LANGUAGE]` When a C# project references an F# project (or vice versa), find-all-references must cross the language boundary. @@ -161,9 +161,9 @@ When a C# project references an F# project (or vice versa), find-all-references | C# symbol used in F# code | C# sidecar finds references in C# projects → Rust host also dispatches to F# sidecar for F# projects | | F# symbol used in C# code | F# sidecar finds references in F# projects → Rust host also dispatches to C# sidecar for C# projects | -Cross-language references are a P2 feature targeting Phase 4. The Rust host merges results from both sidecars and deduplicates by location. +The Rust host merges results from both sidecars and deduplicates by location. -## 7. Caching Strategy +## Caching Strategy `[REFERENCES-CACHE]` Reference results are cached via the [salsa](https://salsa-rs.github.io/salsa/) incremental computation database in the Rust host. @@ -172,13 +172,11 @@ Reference results are cached via the [salsa](https://salsa-rs.github.io/salsa/) | `(document_uri, document_version, position, include_declaration)` for references | Any document change in the project | | `(document_uri, document_version, position)` for document highlights | Document edit (version change) | -Document highlight results are cached more aggressively since they are scoped to a single file. - References results SHOULD be invalidated when any document in the solution changes, since references are solution-wide. The Rust host MAY use a coarse invalidation strategy (invalidate all reference caches on any edit) for simplicity. Stale requests for superseded document versions MUST be cancelled. -## 8. Performance Requirements +## Performance Requirements `[REFERENCES-PERFORMANCE]` | Metric | Target | Measurement | |---|---|---| @@ -189,23 +187,23 @@ Stale requests for superseded document versions MUST be cancelled. | Cached reference lookup | <1ms | salsa cache hit | | Tree-sitter pre-validation | <1ms | Whitespace/comment/literal rejection | -References may be returned incrementally via partial results (`partialResult` token) for large result sets to provide progressive UI feedback. +Large result sets MAY use the LSP `partialResult` token. -## 9. Error Handling +## Error Handling `[REFERENCES-ERRORS]` | Condition | Response | |---|---| | Position is whitespace or comment | Return `null` (no references) | | Sidecar not ready / loading | Return `null` with `window/showMessage` notification | | Symbol resolution fails | Return `null` | -| Sidecar crashes during request | Return `null`, trigger crash recovery (see SHARPLSP-SPEC §5) | +| Sidecar crashes during request | Return `null` and trigger [sidecar recovery](SIDECAR-LIFECYCLE-SPEC.md) | | No references found (only declaration) | Return `[]` (empty array) if `includeDeclaration` is false; `[declaration]` if true | -Reference requests MUST NOT block, hang, or return errors to the client. On any failure, return `null`. +Reference requests MUST NOT hang or return protocol errors to the client; failures return `null`. -## 10. Wire Types (IPC) +## Wire Types (IPC) `[REFERENCES-IPC]` -### 10.1 Request +### Request `[REFERENCES-IPC-REQUEST]` ```csharp [MessagePackObject] @@ -220,7 +218,7 @@ public class ReferencesRequest For document highlights, reuses `PositionRequest` (shared with hover/definition). -### 10.2 Response +### Response `[REFERENCES-IPC-RESPONSE]` Reuses `LocationListResult` from the definition spec for references: @@ -252,23 +250,9 @@ public class DocumentHighlightListResult } ``` -### 10.3 IPC Methods +### IPC Methods `[REFERENCES-IPC-METHODS]` | IPC Method | LSP Method | Response Type | |---|---|---| | `textDocument/references` | `textDocument/references` | `LocationListResult` | | `textDocument/documentHighlight` | `textDocument/documentHighlight` | `DocumentHighlightListResult` | - -## 11. Competitive Parity Matrix - -| Feature | VS | CDK | Rider | SharpLsp Target | Priority | -|---|---|---|---|---|---| -| Find all references (in-source) | Y | Y | Y | Y | P0 | -| Find all references (metadata) | Y | N | Y | Y (P1) | P1 | -| Document highlights (read/write) | Y | Y | Y | Y | P0 | -| Find usages (advanced, grouped) | Y | N | Y | Y | P1 | -| Cross-language references (C# to F#) | N | N | Y* | Y | P2 | -| Reference count code lens | Y | Y | Y | Y | P1 | -| Partial result streaming | Y | N | Y | Y | P1 | - -*\* Rider supports both languages but via proprietary code, not LSP.* diff --git a/docs/specs/RENAME-SPEC.md b/docs/specs/RENAME-SPEC.md index 2124565a..ce3a36a7 100644 --- a/docs/specs/RENAME-SPEC.md +++ b/docs/specs/RENAME-SPEC.md @@ -1,4 +1,4 @@ -# Rename Specification +# [RENAME] Rename Specification **Parent:** [SHARPLSP-SPEC.md](SHARPLSP-SPEC.md) @@ -10,7 +10,7 @@ Rename is not a generic code action. Editors invoke it through the dedicated LSP ## [RENAME-PROTOCOL] LSP Protocol -### [RENAME-PROTOCOL-PREPARE] textDocument/prepareRename +### [RENAME-PREPARE] textDocument/prepareRename ``` method: textDocument/prepareRename @@ -28,7 +28,7 @@ result: Range | { range: Range; placeholder: string } | null - Return the current symbol name as `placeholder`. - Return `null` when the position is whitespace, trivia, a keyword that is not a renameable symbol, metadata-only source, generated source that cannot be edited, or a symbol kind SharpLsp does not yet support. -### [RENAME-PROTOCOL-EXECUTE] textDocument/rename +### [RENAME-APPLY] textDocument/rename ``` method: textDocument/rename @@ -99,6 +99,8 @@ The C# sidecar MUST use Roslyn semantics. The F# sidecar MUST use FCS symbol resolution and rename support rather than text matching. +Code references use `[FS-RENAME-PREPARE]` for the F# prepare path and `[FS-RENAME-APPLY]` for the F# edit path; both implement [RENAME-PREPARE] and [RENAME-APPLY]. + 1. Get checked file results for the current document. 2. Resolve the `FSharpSymbolUse` at the requested position. 3. Validate that the symbol kind is renameable and that the new name is valid F# syntax for that symbol kind. diff --git a/docs/specs/SCRIPTING-FILEBASED-SPEC.md b/docs/specs/SCRIPTING-FILEBASED-SPEC.md index 9b70852e..1551496f 100644 --- a/docs/specs/SCRIPTING-FILEBASED-SPEC.md +++ b/docs/specs/SCRIPTING-FILEBASED-SPEC.md @@ -1,11 +1,10 @@ -# Scripting and File-Based Apps Specification +# Scripting and File-Based Apps Specification `[SCRIPT-FILEBASED]` **Parent:** [SHARPLSP-SPEC.md](SHARPLSP-SPEC.md) -## 1. Overview +## Overview `[SCRIPT-OVERVIEW]` -SharpLsp must provide full semantic language support for .NET source files that are **not owned by a -project file**. There are three distinct such formats, and they are not interchangeable: +SharpLsp provides semantic support for three distinct project-less .NET formats: | Format | Extension | Compilation model | Reference resolution | |---|---|---|---| @@ -13,59 +12,41 @@ project file**. There are three distinct such formats, and they are not intercha | C# script | `.csx` | `SourceCodeKind.Script` | `#r` / `#load` via Roslyn script resolvers | | F# script | `.fsx`, `.fsscript` | FSI script compilation | `#r "nuget:"` / `#load` / `#I` via FCS | -These are **first-class editing scenarios**, not a degraded fallback. A `.cs` file-based app opened -without a solution must get the same completion, hover, definition, rename, and diagnostic quality as -a file inside a `.csproj`. F# scripts are held to the same bar as C# per the project's F#-first -mandate. +A project-less `.cs` app and F# script MUST provide the same completion, hover, definition, rename, and diagnostic quality as project-owned code. -### 1.1 Why the naive approach is wrong `[SCRIPT-ANTIPATTERN]` +### Why directory globbing is wrong `[SCRIPT-ANTIPATTERN]` -The first implementation of this feature (PR #188) resolved a project-less file by globbing **every -`.cs` file in the containing directory** into one synthetic Roslyn project. This is incorrect and -must never be reintroduced. Concretely: +Never construct a project-less workspace by globbing every `.cs` file in its directory: -- A .NET file-based app's compilation closure is **one root file** plus its explicit `#:include` - closure. The .NET SDK documentation is unambiguous: *"By default, the single C# file is included."* -- Globbing a directory compiles unrelated programs together. Two sibling file-based apps each with - top-level statements produce `CS0017` (multiple entry points) and duplicate-type errors that do not - exist in a real build. -- It silently reads every `.cs` file in whatever directory the user happened to open a file from, - including generated output, `obj/`, and unrelated source. -- It ignores every `#:` directive, so `#:package`, `#:sdk`, and `#:property` have no effect — - the editor's view of the code diverges from what `dotnet run file.cs` actually compiles. +- A file-based app contains one root plus its explicit `#:include` closure. +- Globbing combines sibling apps, causing false `CS0017` and duplicate-type diagnostics. +- Globbing reads generated output, `obj/`, and unrelated source outside the declared closure. +- Ignoring `#:` directives makes editor semantics diverge from `dotnet run file.cs`. -The rule this spec enforces: **the compilation closure is derived from the file, never from the -directory.** +The compilation closure is derived from the file, never the directory. ---- +## Taxonomy and detection `[SCRIPT-TAXONOMY]` -## 2. Taxonomy and detection - -### 2.1 Document kind `[SCRIPT-DETECT]` +### Document kind `[SCRIPT-DETECT]` Every opened document resolves to exactly one `DocumentKind` before any workspace is created: | Kind | Trigger | |---|---| -| `ProjectOwned` | An owning `.csproj`/`.fsproj` is found by cone search (§2.2) | +| `ProjectOwned` | An owning `.csproj`/`.fsproj` is found by [SCRIPT-CONE] | | `CSharpFileBasedApp` | `.cs`, no owning project | | `CSharpScript` | `.csx` | | `FSharpScript` | `.fsx`, `.fsscript` | -| `FSharpSignature` | `.fsi`, no owning project — syntax-only, see §5.4 | +| `FSharpSignature` | `.fsi`, no owning project — syntax-only per [FSX-FSI] | | `Unsupported` | Any other extension | -Classification is by extension **plus** cone search. It is never by content sniffing. +Classification uses extension plus cone search, never content sniffing. `Unsupported` documents MUST NOT initialize or latch a sidecar workspace; a later supported document must still initialize it. -`Unsupported` documents must not trigger sidecar workspace initialization. This is a hard requirement: -the host latches "workspace initialized" on the first document that successfully initializes a -workspace, and latching on a `.md` or `.json` file permanently prevents the real workspace from ever -opening. +Implementations: `src/main.rs`, `sidecars/SharpLsp.Sidecar.CSharp/Workspace/SolutionLoader.cs`, and `sidecars/SharpLsp.Sidecar.CSharp/Workspace/WorkspaceManager.SingleFile.cs`. -### 2.2 Project cone precedence `[SCRIPT-CONE]` +### Project cone precedence `[SCRIPT-CONE]` -Before a document is treated as file-based or script, SharpLsp walks from the document's directory -toward the filesystem root looking for an owning project. The first directory containing any of -`*.sln`, `*.slnx`, `*.csproj`, `*.fsproj` wins, and the document is classified `ProjectOwned`. +Before treating a document as project-less, SharpLsp walks from its directory toward the filesystem root. The first directory containing `*.sln`, `*.slnx`, `*.csproj`, or `*.fsproj` wins and classifies the document as `ProjectOwned`. The walk stops at the first of: - a directory containing a project or solution file, @@ -73,14 +54,11 @@ The walk stops at the first of: - a directory containing `.git`, - the filesystem root. -Rationale: the .NET SDK documentation explicitly warns against placing file-based apps inside a -project cone because implicit build files interfere. When a user does it anyway, the project wins — -that matches what `dotnet run` does when a project file is present in the working directory. +The project wins inside a project cone, matching `dotnet run` when a project exists in the working directory. -A `.csx`/`.fsx` file is **never** `ProjectOwned`. Scripts are self-describing even inside a project -cone, because MSBuild does not compile `.csx`/`.fsx` by default. +A `.csx` or `.fsx` file is never `ProjectOwned`; MSBuild does not compile scripts by default. -### 2.3 Compilation closure `[SCRIPT-CLOSURE]` +### Compilation closure `[SCRIPT-CLOSURE]` | Kind | Closure | |---|---| @@ -88,24 +66,17 @@ cone, because MSBuild does not compile `.csx`/`.fsx` by default. | `CSharpScript` | root `.csx` + transitive `#load` expansion | | `FSharpScript` | root `.fsx` + transitive `#load` expansion (computed by FCS) | -Closure expansion is cycle-safe: a file already in the closure is not re-added, and a cycle is -reported as a diagnostic rather than causing unbounded recursion. Closure expansion is bounded at -**64 files** and **8 levels** of nesting; exceeding either bound produces a warning diagnostic and -truncates, so a pathological `#:include **/*.cs` cannot hang the sidecar. +Closure expansion does not re-add files and reports cycles as diagnostics. It is bounded at **64 files** and **8 levels**; exceeding either bound truncates expansion and emits a warning. ---- +## C# file-based apps `[FILEBASED]` -## 3. C# file-based apps `[FILEBASED]` +Targets the [.NET 10 file-based app model](https://learn.microsoft.com/en-us/dotnet/core/sdk/file-based-apps). -Targets the .NET 10 SDK file-based app feature -([docs](https://learn.microsoft.com/en-us/dotnet/core/sdk/file-based-apps)). +Implementations: `sidecars/SharpLsp.Sidecar.CSharp/Workspace/FileLevelDirectives.cs`, `DocumentClosure.cs`, and `WorkspaceManager.SingleFile.cs`. -### 3.1 Directive parsing `[FILEBASED-DIRECTIVES]` +### Directive parsing `[FILEBASED-DIRECTIVES]` -File-level directives are parsed **off the Roslyn CST**, never with regular expressions or string -matching. Roslyn 5.6+ lexes `#:` as `IgnoredDirectiveTriviaSyntax` and `#!` as -`ShebangDirectiveTriviaSyntax`. The parser walks leading trivia of the compilation unit and collects -these nodes. +File-level directives are parsed **off the Roslyn CST**, never with regular expressions or string matching. Roslyn 5.6+ lexes `#:` as `IgnoredDirectiveTriviaSyntax` and `#!` as `ShebangDirectiveTriviaSyntax`. The parser walks leading trivia of the compilation unit and collects these nodes. Supported directives, matching the SDK exactly: @@ -117,111 +88,72 @@ Supported directives, matching the SDK exactly: | `#:property` | `#:property =` | Value may contain MSBuild expressions | | `#:include` | `#:include ` | Literal path, glob, or MSBuild property | -`#:include` maps to item types by extension, per the SDK: `*.cs` → `Compile`, `*.resx` → -`EmbeddedResource`, `*.json` → `None`, `*.razor` → `Content`. Only `Compile` items participate in the -semantic closure; the rest are recorded so the synthesized project stays faithful. +`#:include` maps to item types by extension, per the SDK: `*.cs` → `Compile`, `*.resx` → `EmbeddedResource`, `*.json` → `None`, `*.razor` → `Content`. Only `Compile` items participate in the semantic closure; the rest are recorded so the synthesized project stays faithful. -Directives must appear before the first non-trivia token. A `#:` directive that appears after real -code is reported as a diagnostic at its own location, matching compiler behavior. +Directives must appear before the first non-trivia token. A `#:` directive that appears after real code is reported as a diagnostic at its own location, matching compiler behavior. -### 3.2 Shebang `[FILEBASED-SHEBANG]` +### Shebang `[FILEBASED-SHEBANG]` -A leading `#!` line is valid in a file-based app and must not produce a diagnostic. Because Roslyn -lexes it as `ShebangDirectiveTriviaSyntax`, no text preprocessing is required — the file is passed to -Roslyn verbatim. SharpLsp must never strip, rewrite, or offset the shebang line, because doing so -would desynchronize LSP positions from the on-disk text. +A leading `#!` line is valid in a file-based app and must not produce a diagnostic. Because Roslyn lexes it as `ShebangDirectiveTriviaSyntax`, no text preprocessing is required — the file is passed to Roslyn verbatim. SharpLsp must never strip, rewrite, or offset the shebang line, because doing so would desynchronize LSP positions from the on-disk text. -### 3.3 Reference resolution `[FILEBASED-REFERENCES]` +### Reference resolution `[FILEBASED-REFERENCES]` Reference resolution has two tiers. Tier 1 is correct; tier 2 is a bounded degradation. #### Tier 1 — synthesized project + real restore `[FILEBASED-REFERENCES-MSBUILD]` -1. Synthesize an MSBuild project equivalent to the SDK's virtual project from the parsed directives. - The project is constructed through `Microsoft.Build.Construction.ProjectRootElement` — an actual - XML DOM — and never by string concatenation, per the repo's structured-file rule. -2. Write it to a per-app cache directory keyed by a hash of the root file's full path, mirroring the - SDK's own `/dotnet/runfile/-/` scheme. +1. Synthesize an MSBuild project equivalent to the SDK's virtual project from the parsed directives. The project is constructed through `Microsoft.Build.Construction.ProjectRootElement` — an actual XML DOM — and never by string concatenation, per the repo's structured-file rule. +2. Write it to a per-app cache directory keyed by a hash of the root file's full path, mirroring the SDK's own `/dotnet/runfile/-/` scheme. 3. Run `dotnet restore` on it. 4. Load it through the **existing** `MSBuildWorkspace` path. -This yields exactly the references, implicit usings, analyzers, framework references, and language -version that `dotnet build file.cs` yields, and it reuses the workspace pipeline already in -production rather than duplicating it. +The resulting references, implicit usings, analyzers, framework references, and language version MUST match `dotnet build file.cs`. -Defaults applied when no directive overrides them, matching the SDK: `TargetFramework` from the -resolved SDK band, `ImplicitUsings=enable`, `Nullable=enable`, `OutputKind=ConsoleApplication`, -`PublishAot=true`, `PackAsTool=true`. `PublishAot`/`PackAsTool` do not affect semantics but are -carried so `dotnet project convert` parity holds. +Defaults applied when no directive overrides them, matching the SDK: `TargetFramework` from the resolved SDK band, `ImplicitUsings=enable`, `Nullable=enable`, `OutputKind=ConsoleApplication`, `PublishAot=true`, `PackAsTool=true`. `PublishAot`/`PackAsTool` do not affect semantics but are carried so `dotnet project convert` parity holds. -Implicit build files — `Directory.Build.props`, `Directory.Build.targets`, -`Directory.Packages.props`, `nuget.config`, `global.json` — are honored because a real restore is -performed from the app's own directory. This is a correctness advantage over any hand-rolled -reference list and is the primary reason tier 1 is the default. +Restore runs from the app directory and MUST honor `Directory.Build.props`, `Directory.Build.targets`, `Directory.Packages.props`, `nuget.config`, and `global.json`. #### Tier 2 — in-memory reference assemblies `[FILEBASED-REFERENCES-FALLBACK]` -When the .NET SDK is unavailable, restore fails, or restore has not yet completed, the sidecar builds -an `AdhocWorkspace` using `Basic.Reference.Assemblies` for the target framework band. This gives -immediate BCL-level IntelliSense with zero I/O so the editor is never dead while restore runs. - -Tier 2 is explicitly **incomplete**: `#:package` references are unresolved, so symbols from NuGet -packages will not bind. The sidecar must publish an informational diagnostic naming the reason, and -must upgrade to tier 1 automatically when restore succeeds. +When the .NET SDK is unavailable, restore fails, or restore has not yet completed, the sidecar builds an `AdhocWorkspace` using `Basic.Reference.Assemblies` for the target framework band. This gives immediate BCL-level IntelliSense with zero I/O so the editor is never dead while restore runs. -Tier 2 must never be silently presented as a successful full load. `workspace/status` reports -`filebased-degraded` in this state. +Tier 2 is explicitly **incomplete**: `#:package` references are unresolved, so symbols from NuGet packages will not bind. The sidecar must publish an informational diagnostic naming the reason, and must upgrade to tier 1 automatically when restore succeeds. -### 3.4 Parse options `[FILEBASED-PARSEOPTIONS]` +Tier 2 must never be silently presented as a successful full load. `workspace/status` reports `filebased-degraded` in this state. -`LanguageVersion` is resolved from the target framework band, not hardcoded to `Preview`. `Preview` -enables unstable features that the user's SDK may reject, producing editor-only false negatives. -`LanguageVersion.Latest` is used when the band cannot be determined. +### Parse options `[FILEBASED-PARSEOPTIONS]` -### 3.5 Entry points `[FILEBASED-ENTRYPOINT]` +`LanguageVersion` is resolved from the target framework band, not hardcoded to `Preview`. `Preview` enables unstable features that the user's SDK may reject, producing editor-only false negatives. `LanguageVersion.Latest` is used when the band cannot be determined. -A file-based app root file carries top-level statements. `#:include`d `.cs` files may add types, -methods, and namespaces but **may not** add top-level statements — the SDK forbids it. SharpLsp -reports a violation as a diagnostic on the offending included file rather than allowing a confusing -`CS0017` from the compiler. +### Entry points `[FILEBASED-ENTRYPOINT]` ---- +A file-based app root file carries top-level statements. `#:include`d `.cs` files may add types, methods, and namespaces but **may not** add top-level statements — the SDK forbids it. SharpLsp reports a violation as a diagnostic on the offending included file rather than allowing a confusing `CS0017` from the compiler. -## 4. C# scripts `[CSX]` +## C# scripts `[CSX]` -`.csx` is Roslyn scripting, **not** a file-based app. Conflating the two is a correctness bug: `#r` -and `#load` are script-only, `#:` directives are file-based-only, and the two use different -`SourceCodeKind` values. +`.csx` is Roslyn scripting, **not** a file-based app. Conflating the two is a correctness bug: `#r` and `#load` are script-only, `#:` directives are file-based-only, and the two use different `SourceCodeKind` values. -### 4.1 Parse and compilation options `[CSX-OPTIONS]` +### Parse and compilation options `[CSX-OPTIONS]` -- `CSharpParseOptions` with `kind: SourceCodeKind.Script`. This enables top-level statements, - declarations, and a trailing expression. +- `CSharpParseOptions` with `kind: SourceCodeKind.Script`. This enables top-level statements, declarations, and a trailing expression. - `OutputKind.DynamicallyLinkedLibrary`. -- Script default imports applied as global usings: `System`, `System.IO`, `System.Collections.Generic`, - `System.Console`, `System.Diagnostics`, `System.Dynamic`, `System.Linq`, - `System.Linq.Expressions`, `System.Text`, `System.Threading.Tasks`. +- Script default imports applied as global usings: `System`, `System.IO`, `System.Collections.Generic`, `System.Console`, `System.Diagnostics`, `System.Dynamic`, `System.Linq`, `System.Linq.Expressions`, `System.Text`, `System.Threading.Tasks`. -### 4.2 Directive resolution `[CSX-RESOLVERS]` +### Directive resolution `[CSX-RESOLVERS]` -- `#load` is resolved by a `SourceReferenceResolver` rooted at the script's directory, feeding - §2.3 closure expansion. +- `#load` is resolved by a `SourceReferenceResolver` rooted at the script's directory, feeding [SCRIPT-CLOSURE] expansion. - `#r "assembly.dll"` is resolved by a `MetadataReferenceResolver` rooted at the script's directory. -- `#r "nuget: Pkg, Version"` requires NuGet resolution and is **out of scope for phase 1**. It must - produce a clearly-worded unresolved-reference diagnostic, never a silent wrong answer. +- `#r "nuget: Pkg, Version"` requires NuGet resolution and is **out of scope for phase 1**. It must produce a clearly-worded unresolved-reference diagnostic, never a silent wrong answer. ---- +## F# scripts `[FSX]` -## 5. F# scripts `[FSX]` +F# scripts use FCS directive resolution; SharpLsp MUST NOT reimplement it. -F# scripts are handled by FCS natively and require no directive parsing of our own — a significant -advantage over the C# path that must be preserved rather than reimplemented. +Implementation: `sidecars/SharpLsp.Sidecar.FSharp/FSharpWorkspace.fs`. -### 5.1 Project options `[FSX-OPTIONS]` +### Project options `[FSX-OPTIONS]` -`FSharpChecker.GetProjectOptionsFromScript` is the single entry point. It resolves `#r`, `#r "nuget:"`, -`#I`, and `#load` closures, selects the framework references, and returns `FSharpProjectOptions` -directly consumable by the existing `parseAndCheckOnce` pipeline. +`FSharpChecker.GetProjectOptionsFromScript` is the single entry point. It resolves `#r`, `#r "nuget:"`, `#I`, and `#load` closures, selects the framework references, and returns `FSharpProjectOptions` directly consumable by the existing `parseAndCheckOnce` pipeline. Invocation parameters: - `assumeDotNetFramework = false` @@ -229,106 +161,60 @@ Invocation parameters: - `useFsiAuxLib = true` — makes the `fsi` object bind, so `fsi.CommandLineArgs` resolves. - `previewEnabled` follows the resolved language version. -### 5.2 Preprocessor symbols `[FSX-SYMBOLS]` +### Preprocessor symbols `[FSX-SYMBOLS]` -Scripts opened in the editor define both `INTERACTIVE` and `EDITING`. `COMPILED` is **not** defined. -Getting this wrong makes `#if INTERACTIVE` blocks appear greyed-out-dead in the editor while being -live at runtime. +Scripts opened in the editor define both `INTERACTIVE` and `EDITING`. `COMPILED` is **not** defined. Getting this wrong makes `#if INTERACTIVE` blocks appear greyed-out-dead in the editor while being live at runtime. -### 5.3 NuGet references `[FSX-NUGET]` +### NuGet references `[FSX-NUGET]` -`#r "nuget: ..."` resolution is performed by FCS's dependency manager and requires network and cache -access. It is slow on first use (seconds). Resolution runs off the request path; the script is first -checked without the package references so the editor is responsive, then re-checked once resolution -completes and diagnostics are republished. +`#r "nuget: ..."` resolution is performed by FCS's dependency manager and requires network and cache access. It is slow on first use (seconds). Resolution runs off the request path; the script is first checked without the package references so the editor is responsive, then re-checked once resolution completes and diagnostics are republished. -### 5.4 Signature files `[FSX-FSI]` +### Signature files `[FSX-FSI]` -A `.fsi` signature file with no owning project has no meaningful semantic closure. It is served -syntax-only (document symbols, folding, selection range) by the Rust host, and no F# sidecar -workspace is opened for it. +A `.fsi` signature file with no owning project has no meaningful semantic closure. It is served syntax-only (document symbols, folding, selection range) by the Rust host, and no F# sidecar workspace is opened for it. ---- +## Host routing `[SCRIPT-ROUTE]` -## 6. Host routing `[SCRIPT-ROUTE]` +### Lazy workspace initialization `[SCRIPT-ROUTE-LAZY]` -### 6.1 Lazy workspace initialization `[SCRIPT-ROUTE-LAZY]` - -When the LSP client supplies no workspace root, the host defers `workspace/open` until the first -`textDocument/didOpen` that resolves to a supported `DocumentKind`. +When the LSP client supplies no workspace root, the host defers `workspace/open` until the first `textDocument/didOpen` that resolves to a supported `DocumentKind`. Requirements: -- The "initialized" latch is set **only** when a workspace was actually opened. A `didOpen` for an - `Unsupported` document must leave the latch clear so a later `.cs`/`.fs` open still initializes. -- Only the sidecar matching the document's language is started. Opening a `.cs` file must not spawn - the F# sidecar and vice versa. -- The second language's sidecar is started on demand when a document of that language is first - opened, so a mixed-language folder works without a restart. -- Lazy initialization performs the same steps as eager initialization — workspace open, analyzer - configuration, diagnostics wiring, then health monitoring. It must share one implementation with - the eager path rather than duplicating a subset of it. - -### 6.2 Workspace target `[SCRIPT-ROUTE-TARGET]` +- The "initialized" latch is set **only** when a workspace was actually opened. A `didOpen` for an `Unsupported` document must leave the latch clear so a later `.cs`/`.fs` open still initializes. +- Only the sidecar matching the document's language is started. Opening a `.cs` file must not spawn the F# sidecar and vice versa. +- The second language's sidecar is started on demand when a document of that language is first opened, so a mixed-language folder works without a restart. +- Lazy initialization performs the same steps as eager initialization — workspace open, analyzer configuration, diagnostics wiring, then health monitoring. It must share one implementation with the eager path rather than duplicating a subset of it. -The host sends the **file path**, not the parent directory, for script and file-based documents. The -parent directory is meaningful only for `ProjectOwned` documents. Sending a directory is what forces -the sidecar into directory-globbing and is prohibited. +### Workspace target `[SCRIPT-ROUTE-TARGET]` -### 6.3 Health monitor ordering `[SCRIPT-ROUTE-HEALTH]` +The host sends the **file path**, not the parent directory, for script and file-based documents. The parent directory is meaningful only for `ProjectOwned` documents. Sending a directory is what forces the sidecar into directory-globbing and is prohibited. -Health monitoring starts only after `workspace/open` completes, matching the existing eager path — a -health check that races workspace load can time out on the transport lock and kill a healthy sidecar. +### Health monitor ordering `[SCRIPT-ROUTE-HEALTH]` ---- +Per [SIDECAR-HEALTH-ACTIVITY](SIDECAR-LIFECYCLE-SPEC.md), the per-language supervisor sends no ping until `workspace/open` and generation bootstrap complete. Eager and lazy callers MUST NOT start another health task; workspace open uses its 600-second response budget without a competing transport-locking ping. -## 7. Lifecycle +## Lifecycle `[SCRIPT-LIFECYCLE]` -### 7.1 Directive edits `[SCRIPT-RELOAD]` +### Directive edits `[SCRIPT-RELOAD]` -Editing a `#:package`, `#:project`, `#:sdk`, or `#:include` directive changes the compilation closure -and reference set. On `didChange`, the sidecar re-parses directives from the in-memory text and, if -the directive set changed, schedules a workspace reload debounced by -`sharplsp.toml`'s `server.debounce_ms`. Text-only edits never trigger reload. +Editing a `#:package`, `#:project`, `#:sdk`, or `#:include` directive changes the compilation closure and reference set. On `didChange`, the sidecar re-parses directives from the in-memory text and, if the directive set changed, schedules a workspace reload debounced by `sharplsp.toml`'s `server.debounce_ms`. Text-only edits never trigger reload. -### 7.2 Closure membership changes `[SCRIPT-RELOAD-CLOSURE]` +### Closure membership changes `[SCRIPT-RELOAD-CLOSURE]` -A file entering or leaving the `#:include` / `#:load` closure adds or removes a Roslyn document. -Removal must also clear published diagnostics for that file, otherwise stale squiggles persist in -files no longer part of the app. +A file entering or leaving the `#:include` / `#:load` closure adds or removes a Roslyn document. Removal must also clear published diagnostics for that file, otherwise stale squiggles persist in files no longer part of the app. -### 7.3 Multiple roots `[SCRIPT-MULTIROOT]` +### Multiple roots `[SCRIPT-MULTIROOT]` -Two file-based apps in one directory are two independent compilations. The sidecar keeps a map of -root path → workspace and never merges them. Opening `foo.cs` and `bar.cs` from the same folder -yields two closures, not one project containing both. +Two file-based apps in one directory are two independent compilations. The sidecar keeps a map of root path → workspace and never merges them. Opening `foo.cs` and `bar.cs` from the same folder yields two closures, not one project containing both. ---- +## Error handling and degradation `[SCRIPT-DEGRADE]` -## 8. Error handling and degradation `[SCRIPT-DEGRADE]` +- A file with no supported document kind returns a `Result` failure, never an empty synthetic workspace. +- A directory with no solution or project is valid: `OpenCoreAsync` records a project-less root and returns success, deferring workspace creation to the first document update. Each loose file becomes an independent ad-hoc project per [SCRIPT-ANTIPATTERN]. `IsLoaded` remains false until a document arrives. +- Multiple candidate solutions are ambiguity, not project absence. `SolutionLoader.FindAmbiguousSolutions` MUST return an error naming every candidate and the `csharp.solution_path` resolution setting from [WORKSPACE-SOLUTION-PATH], never enter project-less mode. +- Any I/O during closure expansion is wrapped; a failure to read one included file degrades that file only and is reported as a diagnostic, leaving the rest of the closure loaded. -- A **file** path that resolves to no supported document kind returns a `Result` failure. It must not - be silently converted into an empty synthetic workspace — that turns a real "I could not load your - code" into a wall of phantom diagnostics. -- A **directory** holding no solution or project at all is not a failure. The host opens a workspace - folder eagerly, before any document exists, so `OpenCoreAsync` records the root as project-less and - returns success, deferring workspace creation to the first document update. That document is then - loaded as a file-based app or script, and each subsequent loose file is added as its own ad-hoc - project — two independent files in one folder stay two compilations, per [SCRIPT-ANTIPATTERN]. - `IsLoaded` stays false until a document arrives, so nothing claims a workspace exists before one - does. -- Ambiguous solution discovery (multiple `.sln` under the root) also returns "no target" from - `SolutionLoader`. That case is **ambiguity, not absence**, and must surface as an error naming every - candidate and the `csharp.solution_path` setting that resolves it — never the project-less deferral - above. Treating it as file-based mode would silently mis-analyze an entire repository: no project - reference resolves, and every cross-project type becomes a phantom "not found" diagnostic. - `SolutionLoader.FindAmbiguousSolutions` is what distinguishes the two, and - [WORKSPACE-SOLUTION-PATH] specifies the setting the message points at. -- Any I/O during closure expansion is wrapped; a failure to read one included file degrades that file - only and is reported as a diagnostic, leaving the rest of the closure loaded. - ---- - -## 9. Performance `[SCRIPT-PERF]` +## Performance `[SCRIPT-PERF]` | Operation | Target | |---|---| @@ -338,27 +224,18 @@ yields two closures, not one project containing both. | Tier 1 workspace ready (cold restore) | <10s, non-blocking | | Directive re-parse on keystroke | <1ms | -Cone search is bounded by the stop conditions in §2.2 and must not stat the whole tree. +Cone search is bounded by [SCRIPT-CONE] and must not stat the whole tree. ---- +## Security `[SCRIPT-SECURITY]` -## 10. Security `[SCRIPT-SECURITY]` - -- Opening a file must never cause SharpLsp to read files outside the declared closure. Directory-wide - reads are prohibited (§1.1). -- `#:include` and `#load` paths that escape the root file's directory are permitted (the SDK permits - `../`) but are logged at debug level. -- Tier 1 runs `dotnet restore`, which executes NuGet resolution and may execute package build logic. - This is the same trust boundary as opening any project and is acceptable, but restore must run only - for documents the user actually opened, never speculatively across a directory. +- Opening a file must never cause SharpLsp to read files outside the declared closure. Directory-wide reads are prohibited by [SCRIPT-ANTIPATTERN]. +- `#:include` and `#load` paths that escape the root file's directory are permitted (the SDK permits `../`) but are logged at debug level. +- Tier 1 `dotnet restore` may execute package build logic, so it runs only for opened documents, never speculatively across a directory. - No script is ever executed to obtain type information. All analysis is compile-time. ---- - -## 11. Testing `[SCRIPT-TESTS]` +## Testing `[SCRIPT-TESTS]` -Coarse end-to-end tests only, per repo policy. Every test drives the real sidecar over real IPC with -real files on disk. +Tests drive real sidecars over IPC with real files. Implementations: `sidecars/SharpLsp.Sidecar.CSharp.Tests/WorkspaceManagerSingleFileTests.cs` and `sidecars/SharpLsp.Sidecar.FSharp.Tests/FSharpScriptTests.fs`. Required coverage: - `.cs` file-based app: BCL completion resolves; hover on `Console.WriteLine` binds. diff --git a/docs/specs/SHARPLSP-SPEC.md b/docs/specs/SHARPLSP-SPEC.md index 16c6d2ac..bd8c2587 100644 --- a/docs/specs/SHARPLSP-SPEC.md +++ b/docs/specs/SHARPLSP-SPEC.md @@ -1,48 +1,24 @@ -# SHARPLSP - -**The .NET Language Server Platform** +# [SHARPLSP] SHARPLSP **TECHNICAL SPECIFICATION v0.1** -C# + F# | Editor-Agnostic | Rust-Hosted | Open Source - -*March 2026 | DRAFT* - -## 1. Mission Statement - -SharpLsp is an open-source, editor-agnostic [Language Server Protocol (LSP)](https://microsoft.github.io/language-server-protocol/) implementation for the .NET ecosystem, written in Rust, aiming to match — and ultimately go beyond — what Visual Studio, [JetBrains Rider](https://www.jetbrains.com/rider/), and [C# Dev Kit](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csdevkit) deliver today, with C# and F# as equal first-class citizens. - -SharpLsp exists because .NET developers deserve world-class tooling that is not gated behind proprietary licenses, vendor lock-in, or single-editor coupling. Every .NET developer, in every editor, on every platform, should have access to the best possible development experience. +## [SHARPLSP-MISSION] Mission -### 1.1 Design Principles +SharpLsp is an open-source, editor-agnostic [LSP 3.17](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/) implementation for C# and F#, with a Rust host and compiler-backed .NET sidecars. -- **Editor-agnostic:** Pure [LSP 3.17+](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/) protocol. No editor-specific APIs. Works in [VS Code](https://code.visualstudio.com/), [Neovim](https://neovim.io/), [Helix](https://helix-editor.com/), [Zed](https://zed.dev/), [Emacs](https://www.gnu.org/software/emacs/), [Sublime](https://www.sublimetext.com/), or any LSP-capable editor. +### [SHARPLSP-MISSION-PRINCIPLES] Design Principles -- **C# and F# are equals:** F# is not a second-class citizen bolted on later. Both languages share infrastructure, both hit feature parity targets, both are tested to the same standard. +- **Editor-agnostic:** use LSP 3.17+ without editor-specific APIs. +- **Language parity:** C# and F# share infrastructure, feature targets, and test standards. +- **Open dependencies:** use Roslyn and FCS without proprietary Visual Studio or C# Dev Kit components. +- **Rust hot path:** keep protocol handling, document state, syntax parsing, routing, and caching in Rust. +- **Compiler semantics:** delegate semantic analysis to Roslyn and FCS; do not reimplement type checkers. -- **Zero proprietary dependencies:** The only Microsoft components are the open-source, MIT-licensed [Roslyn compiler](https://github.com/dotnet/roslyn) and [F# Compiler Services](https://fsharp.github.io/fsharp-compiler-docs/). No Visual Studio licensing. No C# Dev Kit EULA. +## [SHARPLSP-ARCHITECTURE] Architecture -- **Rust for the hot path:** Protocol handling, document management, syntax parsing, request routing, and caching all happen in Rust for maximum throughput and minimum latency. +### [SHARPLSP-ARCHITECTURE-TIERS] High-Level Architecture -- **Correctness over cleverness:** Semantic analysis is delegated to the official compilers via managed sidecar processes. We do not reimplement type checkers. - -- **Match the leaders, then go further:** Not approximate parity. Not a lightweight alternative. Full feature-for-feature coverage of what Visual Studio, Rider, and C# Dev Kit do best — and then more. Every refactoring Rider has. Every code fix Visual Studio has. Every diagnostic, every navigation feature. - -### 1.2 Where SharpLsp Fits - -The .NET tooling landscape today is excellent in places, but no single product covers every developer. SharpLsp is positioned to complement three established tools by closing the gaps each leaves behind: - -| Tool | Gap SharpLsp Addresses | -|---|---| -| Visual Studio | Windows-only, closed-source IDE. Most language features are tied to the full IDE install. No LSP surface for external editors. | -| JetBrains Rider | Excellent product, but proprietary and paid ($169–$399/yr). Uses a custom protocol, not LSP. Dual-process JVM/.NET architecture is resource-heavy. | -| C# Dev Kit | VS Code-only. Proprietary license for teams >5. No F# support. Custom non-standard LSP extensions don't carry across other editors. | - -## 2. Architecture - -### 2.1 High-Level Architecture - -SharpLsp uses a three-tier architecture: a Rust host process handles the LSP protocol and syntax-level analysis, communicating with two managed .NET sidecar processes (one for C#/[Roslyn](https://github.com/dotnet/roslyn), one for F#/[FCS](https://fsharp.github.io/fsharp-compiler-docs/)) that perform all semantic analysis. This is not a compromise — it is the optimal design, validated by Visual Studio's own ServiceHub architecture and [FsAutoComplete](https://github.com/fsharp/FsAutoComplete)'s production deployment. +SharpLsp uses a Rust host for the LSP protocol and syntax analysis, plus managed .NET sidecars for C#/[Roslyn](https://github.com/dotnet/roslyn) and F#/[FCS](https://fsharp.github.io/fsharp-compiler-docs/) semantic analysis. **Tier 1 — Rust LSP Host** @@ -68,9 +44,9 @@ SharpLsp uses a three-tier architecture: a Rust host process handles the LSP pro - [FSharpChecker](https://fsharp.github.io/fsharp-compiler-docs/reference/fsharp-compiler-codeanalysis-fsharpchecker.html) with incremental build caching (MRU caches for parse/check results) - [Ionide.ProjInfo](https://github.com/ionide/proj-info) for project cracking (MSBuild evaluation for F# projects) - [FSharpLint](https://github.com/fsprojects/FSharpLint) for linting -- Same RPC interface and transport as C# sidecar for architectural symmetry +- Same RPC interface and transport as the C# sidecar -### 2.2 IPC Transport Protocol +### [SIDECAR-IPC-OVERVIEW] IPC Transport Protocol Communication between the Rust host and .NET sidecars uses a custom binary RPC protocol: @@ -79,13 +55,13 @@ Communication between the Rust host and .NET sidecars uses a custom binary RPC p | Transport | Named pipes (Windows) / Unix domain sockets (Linux, macOS) | | Serialization | [MessagePack](https://msgpack.org/) via [rmp-serde](https://crates.io/crates/rmp-serde) (Rust) and [MessagePack-CSharp](https://github.com/MessagePack-CSharp/MessagePack-CSharp) (.NET) | | Framing | 4-byte little-endian length prefix + MessagePack payload | -| Concurrency | Request IDs for multiplexed async request/response + server-initiated notifications | +| Concurrency | One active host-to-sidecar request per connection; request IDs provide exact correlation, while a single connection driver dispatches interleaved server-initiated notifications | | Cancellation | Dedicated cancel notification matching LSP `$/cancelRequest` semantics | | Performance target | <500µs round-trip overhead (excluding compiler work) | -MessagePack was chosen over JSON-RPC because it is 2.3x faster to serialize and 57% smaller on the wire, and because Roslyn's own out-of-process ServiceHub uses MessagePack in production, proving it works at IDE scale. +The detailed frame ownership, correlation, notification, health, and poisoning rules are normative in [SIDECAR-LIFECYCLE-SPEC.md](SIDECAR-LIFECYCLE-SPEC.md). -### 2.3 Request Routing Strategy +### [SHARPLSP-ARCHITECTURE-ROUTING] Request Routing Strategy The Rust host classifies every incoming LSP request and routes it to the fastest handler: @@ -98,20 +74,35 @@ The Rust host classifies every incoming LSP request and routes it to the fastest Key optimization: on every keystroke, tree-sitter re-parses in <1ms and provides immediate feedback for syntax-level features, while semantic requests are coalesced with a debounce window (default 150ms) before dispatching to sidecars. Stale in-flight semantic requests are cancelled when superseded. -### 2.4 Sidecar Lifecycle Management - -- **Startup:** Sidecars are spawned lazily on first request for their language. Published as self-contained single-file executables (AOT is incompatible with Roslyn, FSharp.Compiler.Service, and other reflection-heavy dependencies). -- **Health monitoring:** Periodic heartbeat pings (every 5s). If a sidecar fails to respond within 2s, it is marked unhealthy. -- **Request timeouts** `[SIDECAR-REQUEST-TIMEOUT]`: Every host→sidecar request carries a response budget — 600s for `workspace/open` (a full MSBuild design-time build on a cold NuGet cache is legitimately slow), 120s for everything else. A request that exceeds its budget is failed to the client, the IPC connection is dropped, and the sidecar process is killed: the late response would otherwise be handed to the next caller and desync the framed protocol, and the health monitor deliberately skips pinging while a request is in flight, so a wedged handler would never be detected. The next request respawns a clean sidecar via the normal crash-recovery path. -- **Crash recovery:** On sidecar death, cache last-known-good results for graceful degradation. Restart with exponential backoff (1s, 2s, 4s, max 30s). Notify editor via LSP `window/showMessage`. -- **Isolation:** C# and F# sidecars are independent processes. A Roslyn OOM does not affect FCS, and vice versa. -- **Shutdown:** On LSP `shutdown` notification, send cancellation to sidecars, wait up to 5s for graceful exit, then SIGKILL. - -### 2.5 Project System - -The project system is the hardest engineering problem in .NET tooling. [MSBuild](https://learn.microsoft.com/en-us/visualstudio/msbuild/msbuild) project files are Turing-complete, and correct evaluation requires handling SDK-style projects, legacy .csproj/.fsproj, multi-targeting, [Directory.Build.props](https://learn.microsoft.com/en-us/visualstudio/msbuild/customize-by-directory), [Directory.Packages.props](https://learn.microsoft.com/en-us/nuget/consume-packages/central-package-management) (Central Package Management), [global.json](https://learn.microsoft.com/en-us/dotnet/core/tools/global-json) SDK pinning, conditional compilation symbols, and NuGet package resolution. - -**SharpLsp's approach:** +### [SIDECAR-LIFECYCLE-OVERVIEW] Sidecar Lifecycle Management + +The normative state machine and platform contract are in [SIDECAR-LIFECYCLE-SPEC.md](SIDECAR-LIFECYCLE-SPEC.md). + +- **Startup:** A per-language supervisor lazily launches one direct, version-matched sidecar process, + using a new current-user-only IPC endpoint for every generation. `READY` identifies the generation, + process, protocol, and effective bound endpoint; semantic readiness follows workspace bootstrap. +- **Health monitoring:** The connection driver pings only while `Ready` and idle (every 5s, with a 2s + response budget). An in-flight request is governed by its own deadline and cannot race a second + transport-locking health caller. +- **Request timeouts** `[SIDECAR-REQUEST-TIMEOUT]`: Every host-to-sidecar request carries a response + budget—600s for `workspace/open` (a full MSBuild design-time build on a cold NuGet cache is + legitimately slow), 120s for everything else. A request that exceeds its budget is failed to the + client, the IPC connection is poisoned, and the contained sidecar process tree is terminated: a + late response can therefore never be handed to the next caller. +- **Crash recovery:** Startup and runtime failures share one exponential backoff sequence (1s, 2s, + 4s, up to 30s). A replacement generation replays workspace, configuration, and current VFS document + state before becoming ready. Last-known-good feature caches may provide explicitly stale graceful + degradation. +- **Isolation and containment:** C# and F# supervisors, backoff, endpoints, and process trees are + independent. Windows Job Objects and Unix process groups/parent-death handling prevent orphaned + sidecars and compiler descendants. +- **Shutdown:** The sidecar flushes a correlated shutdown acknowledgement before cancelling its loop. + The host allows up to 5s for clean exit, then terminates and reaps only that generation's contained + process tree. + +### [SHARPLSP-ARCHITECTURE-PROJECTS] Project System + +Project evaluation MUST handle SDK-style and legacy `.csproj`/`.fsproj` files, multi-targeting, [Directory.Build.props](https://learn.microsoft.com/en-us/visualstudio/msbuild/customize-by-directory), [Directory.Packages.props](https://learn.microsoft.com/en-us/nuget/consume-packages/central-package-management), [global.json](https://learn.microsoft.com/en-us/dotnet/core/tools/global-json), conditional symbols, and NuGet resolution. - **C# projects:** [MSBuildWorkspace](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.msbuild.msbuildworkspace) ([Microsoft.CodeAnalysis.Workspaces.MSBuild](https://www.nuget.org/packages/Microsoft.CodeAnalysis.Workspaces.MSBuild) + [Microsoft.Build.Locator](https://github.com/microsoft/MSBuildLocator)) performs design-time builds to extract source files, references, and compiler options. - **F# projects:** [Ionide.ProjInfo](https://github.com/ionide/proj-info) performs MSBuild evaluation with F#-specific handling (file ordering, which is semantically significant in F#). @@ -121,40 +112,26 @@ The project system is the hardest engineering problem in .NET tooling. [MSBuild] - **Multi-targeting:** Projects targeting multiple TFMs (e.g., `net8.0;net48;netstandard2.0`) present multiple analysis contexts. SharpLsp exposes a custom LSP extension for users to select the active TFM, defaulting to the first. - **Project-less files:** A `.cs` [file-based app](https://learn.microsoft.com/en-us/dotnet/core/sdk/file-based-apps), a `.csx` Roslyn script, and a `.fsx` F# script are all first-class editing targets with no owning project. Their compilation closure is derived from the root file — `#:include` for file-based apps, `#load` for scripts — and never from the containing directory. See [SCRIPTING-FILEBASED-SPEC.md](SCRIPTING-FILEBASED-SPEC.md). -#### Choosing the Solution to Open `[WORKSPACE-SOLUTION-PATH]` +#### [WORKSPACE-SOLUTION-PATH] Choosing the Solution to Open -The host sends one path to each sidecar's `workspace/open`. When that path is a -directory, the C# sidecar discovers a target under it: an unambiguous `.sln`, -`.slnx`, or `.csproj` is opened directly. Discovery **never guesses** between -several nested solutions — a monorepo root holding `app/App.sln` and -`other/Other.sln` is ambiguous, and guessing would silently load the wrong half -of the repository. +The host sends one path to each sidecar's `workspace/open`. When that path is a directory, the C# sidecar discovers a target under it: an unambiguous `.sln`, `.slnx`, or `.csproj` is opened directly. Discovery **never guesses** between several nested solutions — a monorepo root holding `app/App.sln` and `other/Other.sln` is ambiguous, and guessing would silently load the wrong half of the repository. -`csharp.solution_path` in `sharplsp.toml` resolves that ambiguity by naming the -solution to open, absolute or relative to the workspace root: +`csharp.solution_path` in `sharplsp.toml` resolves that ambiguity by naming the solution to open, absolute or relative to the workspace root: ```toml [csharp] solution_path = "app/App.sln" ``` -The host resolves the setting and sends the **solution file** rather than the -root, so the sidecar opens it without running discovery at all. The setting -falls back to workspace-root discovery when unset, and when it names a path that -is not an existing file — a stale or misspelled entry degrades to auto-discovery -instead of wedging the workspace on a path that cannot load. +The host resolves the setting and sends the **solution file** rather than the root, so the sidecar opens it without running discovery at all. The setting falls back to workspace-root discovery when unset, and when it names a path that is not an existing file — a stale or misspelled entry degrades to auto-discovery instead of wedging the workspace on a path that cannot load. -Without this, an ambiguous root loads no solution, and every semantic -request — hover, completion, diagnostics, navigation — returns empty for the -whole workspace. +### [SHARPLSP-ARCHITECTURE-BINARIES] Binary Layout and Installation -### 2.6 Binary Layout & Installation - -**The `sharplsp` binary is bundled inside every per-platform VSIX.** A user who installs the VS Code extension gets a fully working LSP server with zero additional steps. Extensions are NOT thin clients that require a system-installed binary — the binary ships inside the extension. +Every per-platform VSIX MUST bundle `sharplsp`. **Per-platform VSIX layout:** -Each platform gets its own VSIX. The `sharplsp` binary lives at: +The binary lives at: ``` bin//sharplsp (Unix) @@ -181,8 +158,7 @@ bin//sharplsp.exe (Windows) **Sidecar install locations:** -Sidecars are required framework-dependent .NET executables. Every VSIX bundles them -under `bin/all/`; users do not install sidecars separately. +Sidecars are framework-dependent .NET 10 executables bundled under `bin/all/`; a missing runtime or sidecar is an activation failure. | Artifact | VSIX path | Resolver sources | |---|---|---| @@ -204,18 +180,9 @@ $ sharplsp-sidecar-fsharp --version sharplsp-sidecar-fsharp 0.1.0 ``` -Extensions use this to verify the correct version is active before starting. - -**Sidecar distribution:** +Extensions use this output to verify all versions before starting. -Both sidecars are required framework-dependent .NET assemblies. Every VSIX bundles -`sharplsp-sidecar-csharp` and `sharplsp-sidecar-fsharp` under `bin/all/`. They require -.NET 10 on the host machine. Missing .NET 10, a missing sidecar, or a failed sidecar -version probe is an activation failure. - -### 2.7 Editor Extension Binary Strategy - -**The VS Code extension bundles the `sharplsp` binary.** It ships as a per-platform VSIX with the correct pre-built binary for each OS/architecture combination. No system-level install is required for the LSP server. +### [SHARPLSP-ARCHITECTURE-EXTENSIONS] Editor Extension Binary Strategy Binary resolution is handled exclusively by `@nimblesite/shipwright-vscode` (`activateDeploymentToolkit`). Extensions MUST NOT hand-roll binary resolution. @@ -226,17 +193,16 @@ On activation, the VS Code extension follows this sequence: 3. **Version verification:** Shipwright probes each resolved binary with `--version` and compares against the manifest's `expectedVersion`. 4. **Start LSP client:** Pass the resolved `sharplsp` path to `LanguageClient`. Never hardcode a path. -**CRITICAL — Missing required components fail activation:** +**Missing required components fail activation:** -When any required component step above fails — version mismatch, binary not found, missing -.NET 10, or `--version` returns garbage — the extension MUST: +When any required component step above fails — version mismatch, binary not found, missing .NET 10, or `--version` returns garbage — the extension MUST: - Show a clear, user-facing error message explaining what happened and how to fix it (e.g., "SharpLsp: sharplsp v0.1.0 required but v0.0.9 found.") -- Crash activation instead of starting without C# or F# support -- NEVER block the editor's main thread or event loop waiting for a binary that will never arrive -- NEVER leave the extension in a half-initialized zombie state where it eats CPU or holds locks +- Fail activation instead of starting without C# or F# support +- MUST NOT block the editor's main thread or event loop +- MUST release resources after partial initialization -This applies to ALL editor extensions: VS Code, Zed, Neovim, Helix, etc. An extension that locks up the editor because the binary version is wrong is a critical bug of the highest severity. +These requirements apply to every editor extension. **Version contract:** @@ -262,11 +228,9 @@ The Rust binary MUST have a test that proves: 1. `--version` prints the correct format: `sharplsp X.Y.Z` where X.Y.Z matches `Cargo.toml` 2. The process exits with code 0 -This is editor-agnostic by design. One set of binaries serves VS Code, Zed, Neovim, Helix, and any future editor. A user who runs `make install` already has everything every extension needs. An extension that auto-installs binaries provides them for every other extension too. - -## 3. Technology Stack +## [SHARPLSP-TECHNOLOGY] Technology Stack -### 3.1 Rust Host Crates +### [SHARPLSP-TECHNOLOGY-RUST] Rust Host Crates | Crate | Version | Purpose | |---|---|---| @@ -284,7 +248,7 @@ This is editor-agnostic by design. One set of binaries serves VS Code, Zed, Neov | [notify](https://crates.io/crates/notify) | 7.x | Cross-platform filesystem watcher | | [dashmap](https://crates.io/crates/dashmap) | 6.x | Concurrent hash map for shared caches | -### 3.2 C# Sidecar Packages +### [SHARPLSP-TECHNOLOGY-CSHARP] C# Sidecar Packages | Package | Version | Purpose | |---|---|---| @@ -296,7 +260,7 @@ This is editor-agnostic by design. One set of binaries serves VS Code, Zed, Neov | [ICSharpCode.Decompiler](https://github.com/icsharpcode/ILSpy/tree/master/ICSharpCode.Decompiler) | latest | Decompiled metadata source navigation | | [MessagePack-CSharp](https://github.com/MessagePack-CSharp/MessagePack-CSharp) | latest | IPC serialization | -### 3.3 F# Sidecar Packages +### [SHARPLSP-TECHNOLOGY-FSHARP] F# Sidecar Packages | Package | Version | Purpose | |---|---|---| @@ -306,11 +270,11 @@ This is editor-agnostic by design. One set of binaries serves VS Code, Zed, Neov | [FSharp.Analyzers.SDK](https://github.com/ionide/FSharp.Analyzers.SDK) | latest | Third-party F# analyzer support | | [MessagePack-CSharp](https://github.com/MessagePack-CSharp/MessagePack-CSharp) | latest | IPC serialization | -## 4. Feature Specification +## [SHARPLSP-FEATURES] Feature Specification -This section specifies every feature SharpLsp will implement, mapped to the LSP protocol method, implementation source, and the Roslyn/FCS API that powers it. Features are organized by category. Both C# and F# columns indicate full support unless otherwise noted. +Both C# and F# columns require full support unless noted. -### 4.1 Code Intelligence +### [SHARPLSP-FEATURES-INTELLIGENCE] Code Intelligence | Feature | LSP Method | C# API (Roslyn) | F# API (FCS) | Priority | |---|---|---|---|---| @@ -324,13 +288,13 @@ This section specifies every feature SharpLsp will implement, mapped to the LSP | Inlay hints (params) | `textDocument/inlayHint` | Parameter name hints | Parameter name hints | P1 | | Inline values | `textDocument/inlineValue` | Debugger expression eval | Debugger expression eval | P2 | -#### Completion edit semantics `[COMPLETION-EDIT-REPLACE]` +#### [COMPLETION-EDIT-REPLACE] Completion Edit Semantics Every completion item returned by either sidecar carries an explicit LSP `textEdit`, not just an `insertText`. Its range is the identifier span **at the caret** — the typed prefix to the left of the cursor *plus any identifier characters that already follow it on the same line*. Accepting an item therefore **replaces** that identifier instead of being appended to it: completing `WriteLine` at `Console.|WriteLine` yields `Console.WriteLine`, never `Console.WriteLineWriteLine` (GitHub #178). Without a `textEdit` the editor falls back to its own word-boundary heuristic, which appends after a member-access trigger character and duplicates the identifier. The C# sidecar derives the span from [`CompletionService.GetDefaultCompletionListSpan`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.completion.completionservice.getdefaultcompletionlistspan) extended over trailing identifier characters; the F# sidecar derives it from the FCS partial-name island (`QuickParse.GetPartialLongNameEx`) with the same trailing-character extension. The `NewText` is the item's insert text. The Rust host maps the flat sidecar edit onto `CompletionItem.textEdit` in `src/semantic.rs`. -### 4.2 Navigation +### [SHARPLSP-FEATURES-NAVIGATION] Navigation | Feature | LSP Method | C# API (Roslyn) | F# API (FCS) | Priority | |---|---|---|---|---| @@ -348,21 +312,15 @@ The C# sidecar derives the span from [`CompletionService.GetDefaultCompletionLis | Go to decompiled source | Custom: `sharplsp/decompileSource` | [ICSharpCode.Decompiler](https://github.com/icsharpcode/ILSpy) | ICSharpCode.Decompiler | P1 | | Go to source generator output | Custom: `sharplsp/generatorOutput` | GeneratorDriverRunResult | N/A | P2 | -### 4.3 Diagnostics & Analysis +### [SHARPLSP-FEATURES-DIAGNOSTICS] Diagnostics and Analysis SharpLsp uses the LSP 3.17 **pull-diagnostics + workspace-refresh** model (`textDocument/diagnostic`, `workspace/diagnostic`, `workspace/diagnostic/refresh`), mirroring `Microsoft.CodeAnalysis.LanguageServer` (the engine behind C# Dev Kit). The Rust host never proactively pushes errors during workspace load — that is the only architecture that produces correct diagnostics while NuGet restore, source generators, and cross-project `CompilationReference`s are still resolving. A NuGet restore gate runs before `MSBuildWorkspace.OpenSolutionAsync` to eliminate the largest class of phantom CS0246s. See [DIAGNOSTICS-SPEC.md](DIAGNOSTICS-SPEC.md) for the full specification, including the pull + refresh cycle, the NuGet restore gate, project filtering, and the truth guarantees SharpLsp makes (and doesn't make) about diagnostic completeness during workspace load. -SharpLsp also owns custom static analyzers that run through the same workspace -diagnostics channel. The first rules detect unused public C# and F# code -elements at solution scope, but only when `sharplsp.toml` explicitly marks the -workspace as a monorepo. See -[DIAGNOSTICS-STATIC-ANALYZERS-SPEC.md](DIAGNOSTICS-STATIC-ANALYZERS-SPEC.md). +SharpLsp also owns custom static analyzers that run through the same workspace diagnostics channel. The first rules detect unused public C# and F# code elements at solution scope, but only when `sharplsp.toml` explicitly marks the workspace as a monorepo. See [DIAGNOSTICS-STATIC-ANALYZERS-SPEC.md](DIAGNOSTICS-STATIC-ANALYZERS-SPEC.md). -### 4.4 Code Actions & Refactoring - -This is where SharpLsp must match Rider's 2,200+ inspections and 60+ refactorings. Roslyn provides a substantial base of [CodeFixProviders](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.codefixes.codefixprovider) and [CodeRefactoringProviders](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.coderefactorings.coderefactoringprovider) out of the box. SharpLsp will expose all of them and add custom ones. +### [SHARPLSP-FEATURES-REFACTORING] Code Actions and Refactoring | Feature | LSP Method | C# API | F# API | Priority | |---|---|---|---|---| @@ -389,16 +347,14 @@ This is where SharpLsp must match Rider's 2,200+ inspections and 60+ refactoring | Convert auto-prop ↔ full prop | `textDocument/codeAction` | Roslyn property conversion | N/A | P1 | | Convert method ↔ property | `textDocument/codeAction` | Custom implementation | N/A | P2 | -### 4.5 Formatting +### [SHARPLSP-FEATURES-FORMATTING] Formatting SharpLsp does **not** provide document formatting. Use dedicated formatters: - **C#**: [CSharpier](https://csharpier.com/) — the community-standard opinionated C# formatter - **F#**: [Fantomas](https://github.com/fsprojects/fantomas) via the [Ionide](https://ionide.io/) extension — the standard F# formatter -These tools are excellent at what they do and there is no reason to duplicate their work inside an LSP server. - -### 4.6 Semantic Highlighting +### [SHARPLSP-FEATURES-HIGHLIGHTING] Semantic Highlighting | Feature | LSP Method | C# API | F# API | Priority | |---|---|---|---|---| @@ -406,7 +362,7 @@ These tools are excellent at what they do and there is no reason to duplicate th | Delta semantic tokens | `textDocument/semanticTokens/full/delta` | Incremental classification | Incremental classification | P1 | | Range semantic tokens | `textDocument/semanticTokens/range` | Classifier (range-scoped) | GetSemanticClassification (range) | P0 | -### 4.7 Code Lens +### [SHARPLSP-FEATURES-CODE-LENS] Code Lens | Feature | LSP Method | C# API | F# API | Priority | |---|---|---|---|---| @@ -416,13 +372,11 @@ These tools are excellent at what they do and there is no reason to duplicate th | Run/debug test | `textDocument/codeLens` | Custom test runner integration | Custom test runner integration | P2 | | Recent changes (git) | `textDocument/codeLens` | git log integration | git log integration | P3 | -### 4.8 Debugging (DAP Integration) +### [SHARPLSP-FEATURES-DEBUGGING] Debugging -> **Full specification:** [DEBUGGING-SPEC.md](./DEBUGGING-SPEC.md) -> -> SharpLsp delivers a fully open-source .NET debugging experience via [DAP](https://microsoft.github.io/debug-adapter-protocol/specification). Phase 4 uses [netcoredbg](https://github.com/Samsung/netcoredbg) (MIT) with a `DapRouter` layer in the Rust host for capability augmentation (logpoints, async call stack reconstruction, Hot Reload). Phase 5 replaces netcoredbg with a SharpLsp-native C# Debug Sidecar (Tier 4) built on [ClrDebug](https://github.com/lordmilko/ClrDebug) + ICorDebug, achieving full feature parity with Microsoft's proprietary vsdbg. +See [DEBUGGING-SPEC.md](DEBUGGING-SPEC.md) for the DAP router and debug-sidecar contract. -### 4.9 Test Discovery & Execution +### [SHARPLSP-FEATURES-TESTING] Test Discovery and Execution | Feature | Protocol | Implementation | Priority | |---|---|---|---| @@ -435,7 +389,7 @@ These tools are excellent at what they do and there is no reason to duplicate th | Code coverage | Custom: `sharplsp/coverage` | [coverlet](https://github.com/coverlet-coverage/coverlet) integration | P3 | | F# [Expecto](https://github.com/haf/expecto)/[FsCheck](https://github.com/fscheck/FsCheck) support | Custom: `sharplsp/testDiscovery` | Expecto test tree discovery | P1 | -### 4.10 Workspace Features +### [SHARPLSP-FEATURES-WORKSPACE] Workspace Features | Feature | LSP Method | Implementation | Priority | |---|---|---|---| @@ -448,15 +402,13 @@ These tools are excellent at what they do and there is no reason to duplicate th | NuGet uninstall package | Custom: `sharplsp/nuget/uninstall` | `dotnet remove package` + sidecar reload | P2 | | Multi-TFM selection | Custom: `sharplsp/targetFramework` | Active TFM switching per project | P1 | | File watching & reload | `workspace/didChangeWatchedFiles` | [notify](https://crates.io/crates/notify) crate + sidecar reload | P0 | -| Workspace diagnostics (pull) | `workspace/diagnostic` + `workspace/diagnostic/refresh` | Solution-wide error analysis via LSP 3.17 pull model + 2000ms-debounced refresh; primary diagnostic path (see [DIAGNOSTICS-SPEC §1.1](DIAGNOSTICS-SPEC.md#11-the-pull--refresh-cycle)) | P0 | +| Workspace diagnostics (pull) | `workspace/diagnostic` + `workspace/diagnostic/refresh` | Solution-wide error analysis via LSP 3.17 pull model + 2000ms-debounced refresh; primary diagnostic path (see [DIAG-ARCHITECTURE-PULL-REFRESH](DIAGNOSTICS-SPEC.md#diag-architecture-pull-refresh)) | P0 | | Monorepo static analyzers | `workspace/diagnostic` partial results | SharpLsp-owned unused-public-code analyzers for C# and F#; gated by `workspace.repository_kind = "monorepo"` | P0 | -| NuGet restore gate | (internal, before `workspace/open`) | `dotnet restore` if `obj/project.assets.json` is stale; eliminates phantom CS0246 for NuGet types ([DIAGNOSTICS-SPEC §6](DIAGNOSTICS-SPEC.md#6-nuget-restore-gate)) | P0 | +| NuGet restore gate | (internal, before `workspace/open`) | `dotnet restore` if `obj/project.assets.json` is stale; eliminates phantom CS0246 for NuGet types ([DIAG-RESTORE](DIAGNOSTICS-SPEC.md#diag-restore)) | P0 | | Project init complete | Custom: `workspace/projectInitializationComplete` | Notification fired once per workspace open after restore + `MSBuildWorkspace.OpenSolutionAsync`; matches Roslyn LSP contract | P0 | | Configuration | `workspace/didChangeConfiguration` | [.editorconfig](https://editorconfig.org/) + sharplsp.toml | P0 | -### 4.11 F#-Specific Features - -F# has unique language features that require dedicated support beyond what the shared infrastructure provides: +### [SHARPLSP-FEATURES-FSHARP] F#-Specific Features | Feature | LSP Method | Implementation | Priority | |---|---|---|---| @@ -470,7 +422,7 @@ F# has unique language features that require dedicated support beyond what the s | F# Interactive integration | Custom: `sharplsp/fsi` | Send selection to FSI, evaluate | P2 | | File ordering awareness | Custom: `sharplsp/fileOrder` | Semantic file reorder suggestions | P1 | -## 5. Performance Requirements +## [SHARPLSP-PERFORMANCE] Performance Requirements | Metric | Target | Measurement Method | |---|---|---| @@ -487,13 +439,11 @@ F# has unique language features that require dedicated support beyond what the s | Incremental re-parse on keystroke | <1ms | tree-sitter incremental parse time | | Sidecar crash recovery | <3 seconds | Time from crash detection to restored functionality | -## 6. Implementation Plan - -### Phase 1: Protocol Skeleton & Syntax Features (Months 1–3) +## [SHARPLSP-PLAN] Implementation Plan -**Goal:** A working LSP server that handles all syntax-level features for both C# and F#, with a VS Code extension as test harness. +### [SHARPLSP-PLAN-PROTOCOL] Protocol Skeleton and Syntax Features -**Deliverables:** +**Schedule:** Months 1–3. - Rust binary implementing [LSP 3.17](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/) lifecycle (initialize, initialized, shutdown, exit) - Full document synchronization (open, change, close, save) with VFS @@ -504,11 +454,9 @@ F# has unique language features that require dedicated support beyond what the s - CI/CD pipeline with cross-platform builds (Linux, macOS, Windows) - Logging infrastructure via [tracing](https://crates.io/crates/tracing) crate with [OpenTelemetry](https://opentelemetry.io/) export -### Phase 2: Sidecar Integration & Core Semantics (Months 4–8) - -**Goal:** Full semantic analysis for both languages. This is the phase where SharpLsp becomes genuinely useful. +### [SHARPLSP-PLAN-SEMANTICS] Sidecar Integration and Core Semantics -**Deliverables:** +**Schedule:** Months 4–8. - C# sidecar with MSBuildWorkspace, full project loading, design-time build evaluation - F# sidecar with FSharpChecker, Ionide.ProjInfo, project cracking @@ -522,11 +470,9 @@ F# has unique language features that require dedicated support beyond what the s - [salsa](https://salsa-rs.github.io/salsa/) database for incremental caching of semantic results - Request coalescing and cancellation -### Phase 3: Code Actions & Refactoring (Months 9–14) +### [SHARPLSP-PLAN-REFACTORING] Code Actions and Refactoring -**Goal:** Feature parity with C# Dev Kit for code actions. Approach Rider's refactoring depth. - -**Deliverables:** +**Schedule:** Months 9–14. - All Roslyn built-in CodeFixProviders exposed via LSP code actions - All Roslyn built-in CodeRefactoringProviders exposed via LSP code actions @@ -538,11 +484,9 @@ F# has unique language features that require dedicated support beyond what the s - Code lens (reference count, implementation count) - Decompiled source navigation via [ICSharpCode.Decompiler](https://github.com/icsharpcode/ILSpy) -### Phase 4: Advanced Features & Ecosystem (Months 15–20) - -**Goal:** Feature parity with Rider. Go beyond what any single tool offers today. +### [SHARPLSP-PLAN-ECOSYSTEM] Advanced Features and Ecosystem -**Deliverables:** +**Schedule:** Months 15–20. - Solution-wide error analysis (SWEA equivalent) - Test discovery and execution ([xUnit](https://xunit.net/), [NUnit](https://nunit.org/), [MSTest](https://learn.microsoft.com/en-us/dotnet/core/testing/unit-testing-mstest-intro), [Expecto](https://github.com/haf/expecto), [FsCheck](https://github.com/fscheck/FsCheck)) @@ -557,11 +501,9 @@ F# has unique language features that require dedicated support beyond what the s - Performance optimization pass (memory budgets, cache eviction, lazy loading) - Custom Rider-class inspections beyond Roslyn's built-in set -### Phase 5: Beyond Parity (Months 21+) +### [SHARPLSP-PLAN-LEADERSHIP] Beyond Parity -**Goal:** Features no existing tool has. This is where SharpLsp moves from matching the field to leading it. - -**Stretch deliverables:** +**Schedule:** Month 21 onward. - AI-assisted code actions (LLM-powered refactoring suggestions via [MCP](https://modelcontextprotocol.io/) or custom protocol) - Cross-language navigation (C# ↔ F# within the same solution, via binary references initially, source-level eventually) @@ -571,7 +513,7 @@ F# has unique language features that require dedicated support beyond what the s - Database-aware analysis (SQL-in-string validation, [EF Core](https://learn.microsoft.com/en-us/ef/core/) migration awareness) - Collaborative editing support (operational transform / CRDT) -## 7. Risks & Mitigations +## [SHARPLSP-RISKS] Risks and Mitigations | Risk | Impact | Likelihood | Mitigation | |---|---|---|---| @@ -580,10 +522,8 @@ F# has unique language features that require dedicated support beyond what the s | Memory pressure in large solutions | High | Medium | Implement per-project sidecar pooling. Add memory budget enforcement with cache eviction. Consider separate sidecar instances per project in extreme cases. | | F# tree-sitter grammar incomplete | Medium | Medium | Fall back to FCS for any syntax feature where tree-sitter produces incorrect results. Contribute upstream to improve the grammar. | | Roslyn version coupling | Medium | Certain | Pin Roslyn version per SharpLsp release. Test against multiple Roslyn versions in CI. Abstract sidecar RPC to isolate version dependencies. | -| Microsoft ships improvements to Roslyn LSP server | Low | High | SharpLsp's value is unified C#+F#, editor-agnostic, open governance, and performance. These remain regardless of Microsoft's progress. | -| Adoption challenge | Medium | Medium | Ship early with partial features. Demonstrate clear value in editors Microsoft ignores (Neovim, Helix, Emacs). Build community around open governance. | -## 8. Licensing +## [SHARPLSP-LICENSING] Licensing SharpLsp is MIT-licensed. All dependencies are compatible: @@ -601,13 +541,11 @@ SharpLsp is MIT-licensed. All dependencies are compatible: **Critical:** SharpLsp must never incorporate code from C# Dev Kit's proprietary components (Solution Explorer, IntelliCode, test explorer). These are closed-source under Visual Studio licensing. All equivalent features must be reimplemented from publicly documented APIs and protocols. -## 9. Complete Feature TODO List - -Every feature SharpLsp must implement to match — and ultimately go beyond — Visual Studio, Rider, and C# Dev Kit. Features are grouped by category, prioritized (P0 = launch blocker, P1 = fast follow, P2 = competitive parity, P3 = beyond parity), and marked with their implementation status. +## [SHARPLSP-TODO] Complete Feature List -**Legend:** VS = Visual Studio, CDK = C# Dev Kit, R = Rider. ✓ = the tool has this feature. +Priorities: P0 = launch blocker, P1 = fast follow, P2 = parity, P3 = later. -### 9.1 Code Intelligence +### [SHARPLSP-TODO-INTELLIGENCE] Code Intelligence | Feature | VS | CDK | R | Priority | Phase | |---|---|---|---|---|---| @@ -625,7 +563,7 @@ Every feature SharpLsp must implement to match — and ultimately go beyond — | Regex syntax highlighting in strings | ✓ | ✗ | ✓ | P2 | 4 | | Date/time format string validation | ✗ | ✗ | ✓ | P3 | 5 | -### 9.2 Navigation +### [SHARPLSP-TODO-NAVIGATION] Navigation | Feature | VS | CDK | R | Priority | Phase | |---|---|---|---|---|---| @@ -649,11 +587,11 @@ Every feature SharpLsp must implement to match — and ultimately go beyond — | Breadcrumb / scope bar | ✓ | ✓ | ✓ | P1 | 3 | | Structural navigation (next/prev member) | ✓ | ✗ | ✓ | P2 | 4 | -### 9.3 Diagnostics & Analysis +### [SHARPLSP-TODO-DIAGNOSTICS] Diagnostics and Analysis -See [DIAGNOSTICS-SPEC.md](DIAGNOSTICS-SPEC.md) § Competitive Analysis for the full feature comparison table. Key change from this document: **solution-wide analysis is now P0 (Phase 2), default enabled** — not P1/Phase 4. SharpLsp-owned monorepo static analyzers are specified separately in [DIAGNOSTICS-STATIC-ANALYZERS-SPEC.md](DIAGNOSTICS-STATIC-ANALYZERS-SPEC.md). +[DIAGNOSTICS-SPEC.md](DIAGNOSTICS-SPEC.md) defines default-enabled P0 solution-wide analysis. SharpLsp-owned monorepo analyzers are specified in [DIAGNOSTICS-STATIC-ANALYZERS-SPEC.md](DIAGNOSTICS-STATIC-ANALYZERS-SPEC.md). -### 9.4 Code Actions & Refactoring +### [SHARPLSP-TODO-REFACTORING] Code Actions and Refactoring | Feature | VS | CDK | R | Priority | Phase | |---|---|---|---|---|---| @@ -693,11 +631,11 @@ See [DIAGNOSTICS-SPEC.md](DIAGNOSTICS-SPEC.md) § Competitive Analysis for the f | F#: Convert pipe ↔ nested function calls | ✗ | ✗ | ✗ | P1 | 4 | | F#: Convert to/from computation expression | ✗ | ✗ | ✗ | P2 | 4 | -### 9.5 Formatting & Style +### [SHARPLSP-TODO-FORMATTING] Formatting and Style SharpLsp does **not** provide formatting. Use [CSharpier](https://csharpier.com/) for C# and [Fantomas](https://github.com/fsprojects/fantomas) (via [Ionide](https://ionide.io/)) for F#. -### 9.6 Semantic Highlighting & Visual Features +### [SHARPLSP-TODO-HIGHLIGHTING] Semantic Highlighting and Visual Features | Feature | VS | CDK | R | Priority | Phase | |---|---|---|---|---|---| @@ -708,7 +646,7 @@ SharpLsp does **not** provide formatting. Use [CSharpier](https://csharpier.com/ | Linked editing ranges | ✓ | ✓ | ✓ | P1 | 1 | | Color information (CSS in Razor) | ✓ | ✗ | ✓ | P3 | 5 | -### 9.7 Debugging & Testing +### [SHARPLSP-TODO-DEBUGGING] Debugging and Testing > Full debugging feature parity details: [DEBUGGING-SPEC.md](./DEBUGGING-SPEC.md) @@ -735,7 +673,7 @@ SharpLsp does **not** provide formatting. Use [CSharpier](https://csharpier.com/ | Continuous testing | ✓ | ✗ | ✓ | P3 | 5 | | Code coverage overlay | ✓ | ✗ | ✓ | P3 | 5 | -### 9.8 Workspace & Project Management +### [SHARPLSP-TODO-WORKSPACE] Workspace and Project Management | Feature | VS | CDK | R | Priority | Phase | |---|---|---|---|---|---| @@ -752,7 +690,7 @@ SharpLsp does **not** provide formatting. Use [CSharpier](https://csharpier.com/ | Configuration via sharplsp.toml | ✗ | ✗ | ✗ | P0 | 1 | | Bundled required sidecars in VSIX | ✓ | ✗ | ✓ | P0 | 1 | -### 9.9 F#-Specific Features +### [SHARPLSP-TODO-FSHARP] F#-Specific Features | Feature | VS | CDK | R | Priority | Phase | |---|---|---|---|---|---| @@ -769,9 +707,7 @@ SharpLsp does **not** provide formatting. Use [CSharpier](https://csharpier.com/ | FSharpLint integration | ✗ | ✗ | ✗ | P1 | 4 | | FSharp.Analyzers.SDK support | ✗ | ✗ | ✗ | P1 | 4 | -### 9.10 Features That Set SharpLsp Apart - -These are features no single tool offers today. This is where SharpLsp moves beyond parity and aims to set the bar: +### [SHARPLSP-TODO-DIFFERENTIATORS] Differentiating Features | Feature | VS | CDK | R | Priority | Phase | |---|---|---|---|---|---| @@ -788,7 +724,7 @@ These are features no single tool offers today. This is where SharpLsp moves bey *\* Rider supports both C# and F# but via proprietary code, not LSP, and not available to any other editor.* -## 10. Success Metrics +## [SHARPLSP-SUCCESS] Success Metrics | Milestone | Criteria | Target Date | |---|---|---| @@ -798,35 +734,8 @@ These are features no single tool offers today. This is where SharpLsp moves bey | Community adoption | 1,000+ GitHub stars, 100+ daily active users | Month 24 | | Feature leadership | Features no other tool has (cross-language nav, AI actions, architecture analysis) | Month 24+ | -## 11. Distribution - -SharpLsp is distributed as per-platform VSIXs with the `sharplsp` binary and both -required sidecars bundled inside each one. Installing the VS Code extension is all -a user needs. - -- **`sharplsp`** — bundled inside each per-platform VSIX (`bin//sharplsp[.exe]`). Also available via Homebrew (macOS/Linux) and Scoop (Windows) for users who want it on PATH. -- **`sharplsp-sidecar-csharp`** — bundled inside every VSIX at `bin/all/sharplsp-sidecar-csharp`. -- **`sharplsp-sidecar-fsharp`** — bundled inside every VSIX at `bin/all/sharplsp-sidecar-fsharp`. - -Binary resolution is handled by `@nimblesite/shipwright-vscode`. The bundled -binary is the default resolution source. The `sharplsp.lspPath` setting -overrides it for advanced users. - -**.NET 10 runtime acquisition.** The C# and F# sidecars are framework-dependent -.NET 10 assemblies. SharpLsp does NOT bundle a runtime. Instead, the VS Code -extension declares `ms-dotnettools.vscode-dotnet-runtime` (Microsoft's .NET -Install Tool) as an `extensionDependencies` entry, then calls `dotnet.acquire` -on activation to obtain a per-user .NET 10 runtime. Acquisition shows a -non-interactive progress notification + status-bar indicator; the user is -informed but never asked to do anything. See -[DISTRIBUTION-SPEC.md `[DIST-RUNTIME-ACQUIRE]`](DISTRIBUTION-SPEC.md#dist-runtime-acquire). - -See [DISTRIBUTION-SPEC.md](DISTRIBUTION-SPEC.md) for the full distribution -specification including version invariants, release workflow, and the -editor extension contract. - ---- +## [SHARPLSP-DISTRIBUTION] Distribution -**END OF SPECIFICATION** +Per-platform VSIX paths and binary resolution are specified by [SHARPLSP-ARCHITECTURE-BINARIES] and [SHARPLSP-ARCHITECTURE-EXTENSIONS]. [DISTRIBUTION-SPEC.md](DISTRIBUTION-SPEC.md) is normative for version invariants, packaging, release workflow, and editor activation. -*SharpLsp: Because .NET developers deserve better.* +Under `[DIST-RUNTIME-ACQUIRE]`, the VS Code extension declares `ms-dotnettools.vscode-dotnet-runtime` as an `extensionDependencies` entry and calls `dotnet.acquire` for a per-user .NET 10 runtime. Acquisition MUST show non-interactive progress and a status-bar indicator. diff --git a/docs/specs/SIDECAR-LIFECYCLE-SPEC.md b/docs/specs/SIDECAR-LIFECYCLE-SPEC.md new file mode 100644 index 00000000..373e8b58 --- /dev/null +++ b/docs/specs/SIDECAR-LIFECYCLE-SPEC.md @@ -0,0 +1,540 @@ +# Sidecar Lifecycle and IPC Reliability Specification `[SIDECAR]` + +**Status:** Normative — required behavior; implementation completeness is tracked in the plan +**Applies to:** Rust LSP host, shared .NET sidecar host, C# sidecar, F# sidecar +**Implementation plan:** [SIDECAR-LIFECYCLE-PLAN.md](../plans/SIDECAR-LIFECYCLE-PLAN.md) +The words **MUST**, **MUST NOT**, **SHOULD**, and **MAY** are normative. Implementations and tests cite the most specific applicable stable ID. + +## Objective `[SIDECAR-LIFECYCLE-OBJECTIVE]` + +SharpLsp runs C# and F# semantic engines in independent .NET processes. The lifecycle MUST resolve a spawnable artifact, create an isolated IPC endpoint, establish a correlated session, restore desired workspace state, distinguish slow work from failure, and terminate the complete process tree. + +The lifecycle subsystem MUST make that sequence one state machine. A request, heartbeat, process +exit, startup timeout, editor shutdown, and parent-death event MUST all be serialized through that +same owner. No caller may independently spawn, reconnect, back off, or kill a sidecar. + +### Scope `[SIDECAR-LIFECYCLE-SCOPE]` + +This specification defines: + +- C# and F# sidecar executable resolution and launch fallback; +- per-spawn IPC endpoint allocation and the pre-IPC `READY` handshake; +- supervisor states, generation fencing, request admission, and restart backoff; +- frame ownership, response correlation, notification dispatch, cancellation, and timeouts; +- health checks that distinguish idle, busy, stalled, and dead sidecars; +- workspace/configuration/document rehydration after a new generation starts; +- graceful shutdown, hard termination, parent-death detection, and descendant cleanup; +- cross-platform security, observability, performance budgets, and end-to-end acceptance tests. + +The contract is identical for the Roslyn and FCS sidecars unless a requirement explicitly names a +platform. C# and F# retain separate supervisor instances, endpoint generations, backoff counters, and +process-containment scopes. + +### Non-goals `[SIDECAR-LIFECYCLE-NONGOALS]` + +This work does not change Roslyn/FCS feature behavior, MessagePack DTO payloads owned by individual +features, LSP client restart policy, or editor binary acquisition. It does not introduce a remote IPC +transport or treat sidecar IPC as a cross-user trust boundary. It also does not make semantic handlers +concurrent: the v1 connection driver deliberately admits one host-to-sidecar request at a time while +still receiving interleaved sidecar notifications. + +## Ownership and State `[SIDECAR-ARCHITECTURE]` + +### Component Ownership `[SIDECAR-ARCHITECTURE-OWNERSHIP]` + +| Component | Sole responsibilities | MUST NOT own | +|---|---|---| +| `SidecarManager` facade | Stable API used by LSP features; converts supervisor results into typed errors | Child handles, endpoint cleanup, transport reads, backoff sleeps | +| Supervisor task | State transitions, generation number, launch candidates, process containment, bootstrap, backoff, shutdown | Feature-specific MessagePack payload logic | +| Connection driver task | Sole ownership of one `FramedTransport`; frame reads/writes; active request ID; notification dispatch | Process spawning, retry policy, workspace selection | +| Rust session state | Desired workspace target, analyzer configuration, and authoritative open-document snapshots | Roslyn/FCS semantic state | +| Shared .NET `SidecarHost` | Listener, handshake, sequential dispatch, response flush, parent watchdog, local process containment | Host retry/backoff policy | +| C#/F# engines | Language-specific handlers and semantic state | IPC lifecycle or process ownership | + +There MUST be exactly one supervisor task and at most one connection driver per language. Public +methods communicate with them through bounded channels and await `Result` values. They MUST NOT hold +a mutex across process spawn, IPC, sleep, or user-code awaits. + +### Supervisor State Model `[SIDECAR-STATE-MODEL]` + +```mermaid +flowchart LR + Stopped --> Resolving --> Spawning --> AwaitingReady --> Connecting --> Bootstrapping --> Ready + Ready --> Stopping --> Stopped + Resolving --> Backoff + Spawning --> Backoff + AwaitingReady --> Backoff + Connecting --> Backoff + Bootstrapping --> Backoff + Ready --> Backoff + Backoff --> Resolving +``` + +| State | Required owned resources | Request behavior | +|---|---|---| +| `Stopped` | No child, transport, listener endpoint, or retry timer | First eligible operation starts resolution | +| `Resolving` | Candidate list for the next generation | Concurrent callers join the same readiness waiter | +| `Spawning` | Generation, candidate, endpoint lease, containment scope, child | Concurrent callers continue waiting; no second spawn | +| `AwaitingReady` | Running child and capped stdout/stderr collectors | Only handshake/process/timeout events are accepted | +| `Connecting` | Validated handshake and effective endpoint | Connection retry is bounded; no feature request is written | +| `Bootstrapping` | Connection driver plus desired session snapshot | Internal bootstrap requests only | +| `Ready` | Child, containment, connection driver, completed bootstrap | Feature requests are queued in bounded arrival order | +| `Backoff` | Failure record and monotonic `retry_not_before` | Calls fail promptly with retry metadata; they do not spawn | +| `Stopping` | Resources being drained or terminated | New calls fail as shutting down; queued calls are cancelled | + +`Ready` means the current generation is semantically usable, not merely that an operating-system +listener exists. The `READY` stdout record advances `AwaitingReady` to `Connecting`; only successful +bootstrap advances `Bootstrapping` to `Ready`. + +### Transition Rules `[SIDECAR-STATE-TRANSITIONS]` + +1. Only the supervisor mutates state. +2. Every transition records `from`, `to`, language, generation, attempt, reason, and elapsed time. +3. Startup, connect, bootstrap, protocol, request-timeout, process-exit, and shutdown failures all + return to the supervisor; they never perform a private restart. +4. A process or connection event carries its generation. An event for any older generation is logged + at debug level and ignored. +5. A child is reaped before its generation is discarded. A new generation MUST NOT reuse the old + child handle, connection driver, endpoint, or endpoint nonce. +6. `ensure_ready` is coalesced: N concurrent callers produce one spawn/bootstrap sequence and N + completion results. +7. The C# supervisor cannot transition or reset the F# supervisor, and vice versa. + +### Generation Fencing `[SIDECAR-STATE-GENERATION]` + +The supervisor assigns a monotonically increasing `u64` generation before each spawn attempt. +Generation zero is reserved for “not started”. The generation is included in launch arguments, the +`READY` record, connection-driver events, process-exit events, logs, and bootstrap completion. + +Generation fencing MUST prevent all stale asynchronous work from publishing state. In particular, a +late process-exit event, response, timeout, health tick, or bootstrap result from generation N MUST +NOT kill, disconnect, mark ready, or reset backoff for generation N+1. + +## Resolution and Startup `[SIDECAR-STARTUP]` + +### Launch Candidates `[SIDECAR-STARTUP-RESOLUTION]` + +Resolution produces typed `LaunchCandidate` values and runs again for each generation. It MUST return +the absolute executable path actually passed to `CreateProcess`/`exec`, not a bare command name. + +| Priority | Source | Accepted form | Failure policy | +|---|---|---|---| +| 1 | `SHARPLSP_CSHARP_SIDECAR_PATH` / `SHARPLSP_FSHARP_SIDECAR_PATH` | Absolute native apphost, or `.dll` explicitly paired with absolute `dotnet` | Explicit override is authoritative; invalid or unspawnable is a visible hard failure | +| 2 | Shipwright-resolved bundled/installed artifact | Absolute native apphost or framework-dependent `.dll` | Continue only when the candidate is absent or mechanically unspawnable | +| 3 | `PATH` | Absolute native executable discovered by platform rules | Continue to the next source on invalid format or spawn failure | +| 4 | Development output | Prebuilt apphost, or `dotnet ` | Final fallback; missing build output is a resolution failure | + +On Windows, a direct candidate MUST be a real `.exe`. `.cmd`, `.bat`, PowerShell scripts, and +extensionless command shims MUST NOT be selected or invoked through a shell. A `.dll` is valid only as +an argument to a resolved `dotnet.exe`. On Unix, a direct candidate MUST be a regular file with an +executable mode. All platforms reject directories and inaccessible files. + +`dotnet run` MUST NOT be a launch candidate: it inserts an intermediary process, makes direct-child +termination unreliable, and can rebuild during an editor request. Development builds are produced +before launch and executed as an apphost or with `dotnet `. + +Candidate-local mechanical failures may advance to the next non-explicit candidate within one +startup attempt. Listener, handshake, protocol-version, or application-initialization failures are +generation failures and MUST NOT be hidden by silently trying a different binary. Backoff begins only +after the allowed candidate chain is exhausted. + +### Spawn Contract `[SIDECAR-STARTUP-SPAWN]` + +The host launches a sidecar with explicit arguments equivalent to: + +```text + --endpoint --parent-pid --generation --protocol 1 +``` + +The sidecar MUST validate all arguments before binding. The production host MUST always supply the +parent PID. The child inherits only the intended environment (including `DOTNET_ROOT`), has stdin +closed, has stdout and stderr piped, and is created without a visible console window on Windows. + +Before the sidecar emits `READY`, it MUST install its parent-death watcher and platform containment, +initialize structured file logging, create the listener, and know the listener's effective bound +endpoint. Engine initialization that is allowed to degrade per [DIST-SDK-DISCOVERY] may complete +before `READY`; it MUST NOT bypass the lifecycle setup. + +### Endpoint Allocation and Ownership `[SIDECAR-STARTUP-ENDPOINT]` + +Every spawn attempt receives a new unpredictable endpoint. The endpoint key includes language, host +PID, generation, and at least 64 bits from an operating-system CSPRNG. A workspace hash MAY be included +for diagnostics but MUST NOT be the uniqueness mechanism. + +Recommended shapes are: + +```text +Windows: \\.\pipe\sharplsp---- +Unix: /slsp----.sock +``` + +Requirements: + +- Two hosts opening the same workspace MUST never intentionally share an endpoint. +- A restart MUST allocate a different endpoint from the failed generation so an orphan cannot block + or impersonate the replacement. +- The Windows listener uses `PipeOptions.CurrentUserOnly` and one server instance. +- The Unix socket is created in an owner-only directory where possible and has mode `0600`. +- Neither host nor sidecar may delete an arbitrary pre-existing socket before bind. A random collision + is treated as bind failure and retried with a fresh generation/nonce. +- The listener tracks whether it created a Unix socket and removes only that owned path on disposal. + Stale unique paths from hard crashes may be age-cleaned only inside the validated SharpLsp runtime + directory; they are never unlinked merely because a new host wants the same name. +- The requested path stays below the common 107-byte Unix limit where possible. If the listener must + relocate it, that relocation is authoritative and is reported by the handshake. + +The host logs an endpoint fingerprint, not the full workspace-derived value, at normal levels. + +### Versioned Readiness Handshake `[SIDECAR-STARTUP-HANDSHAKE]` + +After the listener is bound, the sidecar writes and flushes exactly one UTF-8 line to stdout: + +```text +READY:{"protocol":1,"generation":42,"pid":1234,"endpoint":""} +``` + +The JSON object has these required fields: + +| Field | Type | Rule | +|---|---|---| +| `protocol` | unsigned integer | Must equal the host's requested protocol version | +| `generation` | unsigned integer | Must equal the launch generation | +| `pid` | unsigned integer | Actual sidecar process PID, used for diagnostics and containment verification | +| `endpoint` | string | Exact bound endpoint; it may differ from the requested Unix path | + +The host rejects malformed JSON, missing/unknown protocol versions, generation mismatch, zero PID, +an endpoint with the wrong platform shape, or an endpoint not attributable to the requested lease. +The startup budget is 30 seconds. Waiting is a race among a valid handshake, child exit, stdout EOF, +and the timeout; every losing child is terminated and reaped. + +The host then retries connection only for transient listener-visibility errors (`not found` or Windows +`ERROR_PIPE_BUSY`) with bounded exponential delays from 25ms to 250ms for at most 2 seconds. Other +connect errors fail immediately. A successful connection consumes the endpoint lease. + +During a one-release migration the host MAY accept legacy `READY:` only from a binary that +has already passed the exact version check. New sidecars MUST emit the versioned record. + +### Startup Failure Contract `[SIDECAR-STARTUP-FAILURE]` + +Any failure before `READY` MUST: + +1. write the full exception and structured context to the sidecar rolling file; +2. write and flush at most one sanitized `FATAL:` line to stderr containing a stable failure category, + concise reason, and log directory; +3. return a non-zero process exit code; and +4. dispose any listener and owned Unix path. + +The host continuously drains capped stdout/stderr so a verbose child cannot deadlock. It retains at +most the final 16KiB per stream, forwards sanctioned lines through structured logging, reaps the +child, and reports the exit status, failure category, launch source, and sidecar log path. Raw stack +traces, ANSI control sequences, workspace source text, and unbounded output MUST NOT enter the editor +output panel. + +Expected startup failures return `Result`; no startup path may `panic`, `unwrap`, or silently return +success. The `FATAL:` line is an explicit exception to the normal no-sidecar-stderr rule in +[DIST-CLEAN-OUTPUT]. + +## Process Lifetime and Containment `[SIDECAR-PROCESS]` + +### Parent-death Watcher `[SIDECAR-PROCESS-PARENT]` + +The sidecar installs the watcher before listener creation and `READY`: + +- On Windows it opens a waitable handle to `--parent-pid` and exits when that exact process object is + signalled. +- On Unix, because the production launch is direct, it verifies the supplied PID is its parent and + watches for reparenting/parent disappearance. +- Detection latency MUST be at most one second. +- If the parent is already gone or cannot be validated, startup fails before `READY`. +- Normal supervisor shutdown wins over the watcher and follows [SIDECAR-SHUTDOWN-PROTOCOL]. + +Hard parent death triggers descendant termination, listener disposal, and sidecar exit without +waiting for an IPC request. Waiting indefinitely in `AcceptStreamAsync` is forbidden. + +### Descendant Containment `[SIDECAR-PROCESS-TREE]` + +| Platform | Required containment | +|---|---| +| Windows | Before engine child processes can start, the sidecar creates a Job Object with `JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE`, assigns itself to it, and retains the safe job handle for process lifetime. Sidecar exit therefore terminates Roslyn BuildHost, MSBuild, and other descendants. Failure to establish the job is a pre-READY fatal error. | +| Linux/macOS | The host launches the sidecar as leader of a dedicated process group. Planned hard termination signals the group, and the parent-death path terminates that group before exit. The host process is never in the sidecar group. | + +Production launch MUST be direct, so the Rust `Child` PID and handshake PID identify the same sidecar +process. No termination path may enumerate by executable name, kill VS Code, or target a PID/process +group that does not belong to the current generation. + +### Exit Detection and Reaping `[SIDECAR-PROCESS-EXIT]` + +A generation-scoped process watcher reports exit status to the supervisor in every active state. +Expected zero exit after acknowledged shutdown transitions to `Stopped`. Any other exit before or +while `Ready` is classified as failure and enters backoff. The child is always awaited/reaped, even +after timeout or hard kill. Dropping a `Child` handle is a safety net, not the primary cleanup path. + +## IPC Session `[SIDECAR-IPC]` + +### Frame and Envelope Contract `[SIDECAR-IPC-FRAMING]` + +IPC remains a 4-byte little-endian unsigned payload length followed by a MessagePack envelope. Both +sides reject frames above 64MiB before allocation. EOF between the length prefix and payload is a +terminal truncated-frame error, not a clean end-of-stream. + +The envelope fields remain: + +| Field | Request | Response | Notification | +|---|---|---|---| +| `id` | Required non-zero `u32` | Required and equal to request | Null | +| `method` | Required non-empty string | Null | Required non-empty string | +| `payload` | MessagePack bytes | MessagePack bytes | MessagePack bytes | +| `error` | Null | Null or one error string | Null | + +An envelope that matches none or more than one of these shapes is a protocol fault. + +### Single Transport Owner `[SIDECAR-IPC-DRIVER]` + +Only the connection driver reads from or writes to `FramedTransport`. Callers submit bounded commands +containing method, payload, response deadline, and a completion channel. The driver: + +1. writes at most one host request at a time; +2. continuously reads frames while that request is active; +3. dispatches sidecar notifications without mistaking them for the active response; +4. completes the active request only for its exact response ID; and +5. reports EOF, I/O, framing, decode, correlation, and deadline faults to the supervisor. + +The command queue has a finite capacity. Saturation returns a typed busy error; it MUST NOT allocate +an unbounded backlog. Workspace mutations and semantic reads retain arrival order. + +### Response Correlation `[SIDECAR-IPC-CORRELATION]` + +Request IDs are monotonically allocated within a generation and are never zero. A response ID must +equal the active request ID. A missing ID, duplicate response, response for an unknown/old ID, or +mismatch is a protocol fault: the driver fails the active request, stops admitting writes, drops the +transport, and asks the supervisor to terminate/back off that generation. The suspect frame MUST NOT +be handed to a later caller. + +The same validation applies to `ping`, bootstrap, and `shutdown` responses. + +### Cancellation and Response Budgets `[SIDECAR-IPC-TIMEOUT]` + +This section expands the existing [SIDECAR-REQUEST-TIMEOUT] rule: + +- `workspace/open` has a 600-second response budget. +- Every other ordinary request has a 120-second response budget unless a more specific feature spec + defines a shorter budget. +- Idle health `ping` has a 2-second response budget. +- The deadline begins when the frame is written, not while the command waits behind another request. +- Cancellation before the first byte is written removes the command without affecting the session. +- Cancellation after write sends the protocol cancellation notification when supported, then drains + and discards the matching response. The next request is not written until that response is drained. +- If a written request cannot be drained before its response budget, the connection is poisoned and + the generation is terminated. Late frames can therefore never desynchronize the next caller. + +No request is automatically replayed after an ambiguous post-write failure. Read-only feature owners +may explicitly retry against the next ready generation; mutations rely on the Rust VFS/session replay +contract rather than guessing whether the sidecar applied the old request. + +### Health and Activity `[SIDECAR-HEALTH-ACTIVITY]` + +Health is part of the connection driver, not a second caller racing for the transport lock. + +- No ping is sent during `Resolving` through `Bootstrapping`, `Backoff`, or `Stopping`. +- While an ordinary request is active and within its response budget, the sidecar is **busy**, not + unhealthy. The request's own deadline detects a stall. +- When `Ready` and idle, the driver sends a ping after 5 seconds without successful frame activity. +- A matching pong resets idle activity. A ping timeout, mismatched ID, process exit, or transport fault + is unhealthy and terminates the generation. +- Merely observing a held lock or queued request is never proof of liveness. +- At most one health timer exists per supervisor, including across eager/lazy workspace paths. + +### Managed Message Loop Failures `[SIDECAR-IPC-MESSAGE-LOOP]` + +In the .NET host, EOF between messages ends the session normally. `IOException`, +`ObjectDisposedException`, truncated frames, and write failures are terminal. Malformed MessagePack or +invalid envelope shape may produce one correlated protocol error when safe; repeated decode/dispatch +failures are bounded and then terminate non-zero. The loop MUST NOT catch a persistent transport +exception and immediately retry the same broken stream. + +The failure counter resets only after a complete valid message/response cycle. Terminal exit disposes +the listener/transport and allows process containment to clean descendants. + +## Failure, Backoff, and Recovery `[SIDECAR-RECOVERY]` + +### Failure Taxonomy `[SIDECAR-RECOVERY-FAILURES]` + +The supervisor records one of: `resolution`, `spawn`, `pre-ready-exit`, `ready-timeout`, +`listener-bind`, `handshake`, `connect`, `bootstrap`, `protocol`, `request-timeout`, `health`, +`process-exit`, or `shutdown-timeout`. The category is stable for logs and user-facing summaries; +platform exception details remain diagnostic context. + +Resolution, spawn, pre-READY, connect, bootstrap, runtime crash, health, and protocol failures all +advance the same per-language backoff sequence. There is no health-loop-only crash path. + +### Backoff Algorithm `[SIDECAR-RECOVERY-BACKOFF]` + +The base sequence is 1s, 2s, 4s, 8s, 16s, then 30s maximum. Each delay receives bounded ±20% jitter +and is represented by a monotonic `retry_not_before` timestamp. Calls during the window fail promptly +with failure category and remaining retry duration; they do not sleep and do not launch another +process. + +Backoff resets to 1s only after 60 seconds continuously in `Ready` or an explicit full LSP session +restart. A single ping or response does not reset a flapping process. A user-initiated retry command +MAY bypass the current timer once but MUST NOT create concurrent attempts. + +### Bootstrap and Rehydration `[SIDECAR-RECOVERY-REHYDRATE]` + +The Rust host is the source of truth for desired session state. It retains, per language: + +- selected workspace/solution or project-less root-file target; +- current analyzer/configuration payload; +- latest open-document URI, language, version, and full text from the VFS; and +- subscriptions required for sidecar notifications. + +Every new connection is bootstrapped in this order: + +1. `workspace/open` for the current target, if one exists; +2. `analyzers/configure` and other deterministic session configuration; +3. replay of latest open documents owned by that language in stable URI order; and +4. registration/activation of notification consumers. + +Only then does the generation become `Ready`. A bootstrap failure enters normal backoff. Eager +workspace startup, lazy project-less startup, second-language startup, explicit solution selection, +and crash recovery MUST call this one bootstrap implementation. A generation change invalidates +sidecar-derived caches and triggers feature-specific refresh/retry behavior, including diagnostic +generation rules in [DIAG-PUSH-GATE]. + +### Degraded Behavior `[SIDECAR-RECOVERY-DEGRADED]` + +During startup/backoff, Rust syntax-only features remain available. Semantic requests receive a typed +`SidecarUnavailable` result containing language, failure category, and retry-after duration. Where a +feature has a last-known-good cache, it MAY serve that cache with explicit stale provenance; it MUST +NOT invent new semantic results. + +The editor receives at most one rate-limited, plain-language notification per failure episode, with a +`Show Log` action. Repeated LSP requests during the same backoff window do not produce repeated toasts +or one process per request. Recovery to `Ready` emits one structured recovery event and refreshes +affected editor state. + +## Shutdown `[SIDECAR-SHUTDOWN]` + +### Sidecar Acknowledgement Ordering `[SIDECAR-SHUTDOWN-ACK]` + +The `shutdown` handler MUST serialize an `ok` response without cancelling the token needed to write +it. The message loop writes and flushes the response using a bounded write token; only after the flush +succeeds does it cancel dispatch, close the listener/transport, and exit zero. Cancellation from the +handler before the acknowledgement write is forbidden. + +### Host Shutdown Protocol `[SIDECAR-SHUTDOWN-PROTOCOL]` + +For each language, the supervisor: + +1. transitions to `Stopping`, rejects new commands, and cancels commands not yet written; +2. sends one correlated `shutdown` request if a connection exists; +3. waits up to 1 second for the matching acknowledgement; +4. after acknowledgement, closes IPC and waits for clean process exit within the remaining 5-second + graceful-shutdown budget; +5. on missing acknowledgement, timeout, or non-exit, terminates the current generation's contained + process tree; and +6. reaps the direct child, disposes the containment handle, and removes only owned endpoints. + +Shutdown is idempotent. Calling it in `Stopped` succeeds. Host teardown waits for both language +supervisors concurrently, and one stuck language cannot prevent hard cleanup of the other. + +## Observability and Security `[SIDECAR-OPERATIONS]` + +### Structured Lifecycle Logs `[SIDECAR-OBSERVABILITY]` + +Lifecycle logs include `language`, `generation`, `state_from`, `state_to`, `attempt`, `launch_source`, +`pid`, `endpoint_fingerprint`, `request_id`, `method`, `failure_category`, `elapsed_ms`, and +`retry_after_ms` where applicable. Routine requests and pings are debug-level; transitions, recovery, +and graceful shutdown are information-level; failures are warning/error-level once per event. + +The host and sidecar use their existing structured logging systems. Source text, MessagePack payloads, +environment secrets, raw workspace paths at normal log levels, and unbounded exception repetition are +forbidden. The error surfaced for pre-READY exit always names the sidecar log directory. + +### Local IPC Security `[SIDECAR-SECURITY]` + +- Endpoint nonces come from an OS CSPRNG and are not derived solely from public workspace data. +- Unix runtime directories and sockets are owner-only; Windows pipes are current-user-only. +- No launch candidate is passed through a shell, so workspace or path text cannot become shell syntax. +- Handshake endpoints are validated against the current lease before connection. +- Frame limits apply before allocation on both sides. +- Endpoint cleanup is confined to validated, owned paths. +- IPC remains unauthenticated same-user local transport; it MUST NOT bind TCP or a remotely reachable + endpoint as a silent fallback. + +## Budgets and Resource Bounds `[SIDECAR-PERFORMANCE]` + +| Operation/resource | Required bound | +|---|---| +| Pre-READY startup | 30s maximum | +| READY-to-connect retry | 2s maximum | +| Idle ping cadence / response | 5s / 2s | +| Ordinary request / `workspace/open` | 120s / 600s | +| Graceful shutdown before hard termination | 5s | +| Parent-death detection | 1s maximum | +| Frame payload | 64MiB maximum | +| Captured startup stdout/stderr tail | 16KiB each | +| Backoff | 1s exponential to 30s, ±20% jitter | +| Supervisor and connection command queues | Finite, with typed saturation failure | + +No error path may spin without await/backoff, leak a child/descendant, create an unbounded task per +request, or accumulate an unbounded output/command buffer. + +## Compatibility and Integration `[SIDECAR-COMPATIBILITY]` + +The versioned handshake is the only intended startup-protocol change. MessagePack framing and existing +feature DTOs remain compatible. Host and sidecars ship together and are exact-version verified by the +distribution layer; the optional legacy READY parser is temporary migration support, not a permanent +second protocol. + +`SHARPLSP-SPEC.md`, [DIST-CLEAN-OUTPUT], [DIST-CI-WIN-TRANSPORT], [SCRIPT-ROUTE-HEALTH], and +[SIDECAR-REQUEST-TIMEOUT] remain compatible summaries; this document is the normative detailed +lifecycle contract when a summary is ambiguous. + +## End-to-end Acceptance `[SIDECAR-TESTING]` + +Tests MUST be coarse end-to-end tests using real processes, real platform IPC, real files, and either +the published C#/F# sidecars or a separately spawned lifecycle fixture built on the production shared +`SidecarHost`. In-memory transports, mocked process APIs, sleeps as the only assertion, and +test-only branches in production code are prohibited. + +Required scenarios: + +1. Two real SharpLsp hosts open the same workspace and both complete C# and F# semantic requests; + their endpoints and PIDs differ and neither steals the other's socket. +2. A real sidecar listener bind failure emits one `FATAL` diagnostic, exits non-zero, and produces a + host error with exit status and log path. +3. Repeated pre-READY failure produces one spawn attempt per backoff window, not one per semantic + request; recovery succeeds when the real artifact becomes available. +4. A long Unix endpoint connects using the effective path advertised in the versioned READY record. +5. Windows PATH resolution skips `.cmd`, `.bat`, and extensionless shims and launches the next valid + absolute candidate. +6. A response with the wrong ID poisons the generation; it is never returned to the current or next + caller. A sidecar notification arriving before a valid response is dispatched and the response + still reaches the correct caller. +7. A request within its budget is not killed by health monitoring. An idle unresponsive sidecar and a + request beyond its deadline are terminated and restarted. +8. Persistent transport/decode failure exits the .NET message loop within a bounded time and does not + flood logs or consume a CPU core indefinitely. +9. `shutdown` returns the matching acknowledgement before the sidecar exits; normal shutdown does not + require the hard-kill path. +10. On Windows, killing the host and hard-killing a wedged sidecar remove the sidecar and a real child + helper/BuildHost, after which the named pipe can be rebound. On Unix, the equivalent process group + has no surviving members or socket. +11. Killing and restarting a sidecar while documents are open replays workspace, configuration, and + latest VFS text; the next semantic result reflects the latest edit for both C# and F#. +12. The full Windows VSIX lifecycle chunk and Linux/macOS host-sidecar suites exercise the release + artifacts, not only the shared transport library. + +## Issue Traceability `[SIDECAR-TRACEABILITY]` + +| Issue | Root failure | Normative requirements | Closure evidence | +|---|---|---|---| +| #150 | Listener failure exits cleanly and is invisible | [SIDECAR-STARTUP-FAILURE], [SIDECAR-OBSERVABILITY] | Non-zero real-process test; one fatal line; host exit-status/log-path assertion | +| #151 | Workspace-derived endpoints collide/steal | [SIDECAR-STARTUP-ENDPOINT], [SIDECAR-STARTUP-HANDSHAKE] | Concurrent-host tests on Windows and Unix | +| #152 | Pre-READY failures bypass crash backoff | [SIDECAR-STATE-TRANSITIONS], [SIDECAR-RECOVERY-BACKOFF] | Spawn-count/backoff/recovery process test | +| #153 | Persistent transport exception hot-loops | [SIDECAR-IPC-MESSAGE-LOOP], [SIDECAR-PROCESS-EXIT] | Broken-stream/decode-storm process exits within bound | +| #154 | READY reports requested rather than bound path | [SIDECAR-STARTUP-HANDSHAKE] | Overlong Unix endpoint connects through advertised effective path | +| #163 | Windows direct-child kill leaves descendants/orphans | [SIDECAR-PROCESS-PARENT], [SIDECAR-PROCESS-TREE] | Host-death and hard-kill descendant tests on Windows | +| #164 | Response IDs unchecked; health check races lock | [SIDECAR-IPC-CORRELATION], [SIDECAR-HEALTH-ACTIVITY] | Wrong-ID poison test and long-request/idle-stall health tests | +| #167 | PATH accepts shims `CreateProcess` cannot run | [SIDECAR-STARTUP-RESOLUTION] | Windows real-PATH fallback test | +| #172 | Shutdown cancels token before ack write | [SIDECAR-SHUTDOWN-ACK], [SIDECAR-SHUTDOWN-PROTOCOL] | Matching ack observed before zero process exit | diff --git a/docs/specs/SOLUTION-EXPLORER-SPEC.md b/docs/specs/SOLUTION-EXPLORER-SPEC.md index ccfbc8eb..c1fae665 100644 --- a/docs/specs/SOLUTION-EXPLORER-SPEC.md +++ b/docs/specs/SOLUTION-EXPLORER-SPEC.md @@ -1,16 +1,10 @@ -# Solution Explorer Specification +# Solution Explorer Specification `[SE-SOLUTION-EXPLORER]` -**Status:** Active -**Owner:** SharpLsp LSP -**Last Updated:** 2026-04-26 - -## Overview +## Overview `[SE-OVERVIEW]` The Solution Explorer is a VS Code tree view that displays the full code hierarchy of a .NET solution: solutions, projects, namespaces, types, and members. It accepts legacy `.sln` and XML `.slnx` solution files. It is powered by a custom LSP request (`sharplsp/workspaceSymbols`) backed by the sidecar `solution/read` model, tree-sitter parsing in the Rust host for C#, and the FCS sidecar's `documentSymbol` for F# ([SE-FSHARP-SYMBOLS]). -See [LSP-ARCHITECTURE-SPEC.md](specs/LSP-ARCHITECTURE-SPEC.md) for shared LSP architecture. - -## Architecture +## Architecture `[SE-ARCHITECTURE]` ``` VS Code Tree View @@ -31,32 +25,18 @@ Per-file symbols are sourced by language, never by a single parser: | Language | Source | Rationale | |----------|--------|-----------| -| C# (`.cs`) | tree-sitter parsing in the Rust host | A C# grammar is integrated in the host. | -| F# (`.fs`) | FCS sidecar `textDocument/documentSymbol` ([FS-DOCSYMBOL]) | The host has **no** F# tree-sitter grammar, so a tree-sitter-only path silently drops every `.fs` file (issue #119). F# is a first-class language — its files and symbols MUST appear under an `.fsproj` exactly as `.cs` files appear under a `.csproj`. | +| C# (`.cs`) | tree-sitter parsing in the Rust host | The host owns the C# grammar. | +| F# (`.fs`) | FCS sidecar `textDocument/documentSymbol` ([FS-DOCSYMBOL]) | The host has no F# grammar; every `.fs` file and its symbols MUST appear under an `.fsproj` exactly as C# does under `.csproj`. | -The F# path reuses the **same** sidecar `documentSymbol` request that powers the editor outline, mapping the nested FCS symbols (module, namespace, type, DU case, member) into the shared `FileSymbol`/`SymbolNode` tree model using each symbol's full range. The F# sidecar must be threaded into `workspace_symbols::handle`; when it is unavailable the project's `.fs` files contribute no symbols rather than failing the whole request. +The F# path reuses the sidecar `documentSymbol` request that powers the editor outline. It maps nested FCS modules, namespaces, types, DU cases, and members into the shared `FileSymbol`/`SymbolNode` tree using full ranges. `workspace_symbols::handle` receives the F# sidecar; when unavailable, `.fs` files contribute no symbols without failing the request. ### Live-Buffer Path Identity [SE-LIVE-BUFFER] -`sharplsp/workspaceSymbols` MUST parse the latest open-buffer text, including -unsaved and rapid successive edits. Disk content is used only when no open VFS -document denotes the source file. - -The editor URI and the project model can use different native paths for the same -file. In particular, Windows runners can send an 8.3 path such as -`C:\Users\RUNNER~1\...`, while the sidecar reports the expanded -`C:\Users\runneradmin\...` path. The VFS therefore resolves and caches the -editor path when the document opens, then compares both the original URI path -and that canonical path during native-path lookup. Canonicalizing only the -project-model path is insufficient because it leaves the editor's aliased path -unchanged and incorrectly falls back to stale disk text. +`sharplsp/workspaceSymbols` MUST parse the latest open VFS text, including unsaved successive edits; disk content is used only when no open document denotes the source file. -Path comparison also ignores Windows verbatim prefixes and casing differences. -The coarse VS Code explorer tests prove that the tree reflects an unsaved rename -and the final value in a burst of renames; the VFS alias regression test covers -the reverse-alias lookup independently of hosted-runner path spelling. +Editor URIs and project models can name one file differently: for example, Windows can supply `C:\Users\RUNNER~1\...` while the sidecar supplies `C:\Users\runneradmin\...`. On open, the VFS resolves and caches the editor path, then native-path lookup compares both the original URI path and its canonical path. Comparison ignores Windows verbatim prefixes and casing. The VS Code explorer tests cover unsaved and burst edits; VFS regression tests cover reverse-alias lookup. Implementations: `src/vfs.rs`, `src/workspace_symbols.rs`, and `editors/vscode/src/test/suite/solution-explorer.test.ts`. -### Request: `sharplsp/workspaceSymbols` +### Request: `sharplsp/workspaceSymbols` `[SE-WORKSPACE-SYMBOLS-REQUEST]` **Params:** ```json @@ -107,7 +87,7 @@ the reverse-alias lookup independently of hosted-runner path spelling. } ``` -### Symbol Kinds +### Symbol Kinds `[SE-SYMBOL-KINDS]` | Kind | Tree-sitter Node | Icon | Theme Color | |------|-----------------|------|-------------| @@ -125,14 +105,14 @@ the reverse-alias lookup independently of hosted-runner path spelling. | Function | `delegate_declaration` | `symbol-method` | `symbolIcon.functionForeground` | | Constant | — | `symbol-constant` | `symbolIcon.constantForeground` | -### Special Node Icons +### Special Node Icons `[SE-SYMBOL-ICONS]` | Node | Icon | Color | |------|------|-------| | Solution (.sln/.slnx) | `package` | `terminal.ansiGreen` | | Project (.csproj/.fsproj) | `project` | `terminal.ansiCyan` | -### Access Modifier Extraction +### Access Modifier Extraction `[SE-SYMBOL-ACCESS]` The `access` field is extracted from tree-sitter `modifier` child nodes. Recognized values: @@ -145,7 +125,7 @@ The `access` field is extracted from tree-sitter `modifier` child nodes. Recogni When no access modifier is present, `access` is `null`. -## Tree Hierarchy +## Tree Hierarchy `[SE-TREE]` ``` Solution (SharpLsp.Sidecars.sln) @@ -161,15 +141,15 @@ Solution (SharpLsp.Sidecars.sln) └── Method (HandleAsync) ``` -### File-Scoped Namespace Handling +### File-Scoped Namespace Handling `[SE-TREE-FILE-NAMESPACE]` `tree-sitter-c-sharp` 0.23 emits `file_scoped_namespace_declaration` without nesting subsequent type declarations as children. The Rust host detects this pattern and reparents root-level types into the single file-scoped namespace. -### Namespace Merging +### Namespace Merging `[SE-TREE-NAMESPACE-MERGE]` Symbols from multiple files sharing the same namespace within a project are merged into a single namespace node. -## Sort Order +## Sort Order `[SE-SORT]` Three sort modes are available, cycled via a toolbar button: @@ -179,7 +159,7 @@ Three sort modes are available, cycled via a toolbar button: | Alphabetical | A-Z by symbol name at every level | `$(case-sensitive)` | | Accessibility | Grouped by access modifier, then alphabetical | `$(shield)` | -### Accessibility Sort Priority +### Accessibility Sort Priority `[SE-SORT-ACCESS]` | Priority | Access Level | |----------|-------------| @@ -193,17 +173,17 @@ Three sort modes are available, cycled via a toolbar button: Within each access group, symbols are sorted alphabetically. -### Sort Scope +### Sort Scope `[SE-SORT-SCOPE]` - Sorting applies recursively to namespace children, type children, and nested members - Project order within a solution is preserved (follows `.sln` or `.slnx` declaration order) - Sorting is client-side only — the LSP response is cached and re-sorted without a new request -### Context Key +### Context Key `[SE-SORT-CONTEXT]` The current sort order is exposed via VS Code context key `sharplsp.sortOrder` (values: `natural`, `alphabetical`, `accessibility`). This controls which toolbar icon is visible. -## Commands +## Commands `[SE-COMMANDS]` | Command | Title | Icon | When | |---------|-------|------|------| @@ -215,27 +195,21 @@ The current sort order is exposed via VS Code context key `sharplsp.sortOrder` ( All three sort commands cycle to the next sort mode. -## Retry Logic - -The workspace symbols request retries up to 3 times with a 2-second delay when: -- The LSP client is not yet running -- A transient error occurs (disposed connection, etc.) - -## Hover / Quick Info +## Retry Logic `[SE-REQUEST-RETRY]` -Symbol nodes in the Solution Explorer support hover tooltips showing the same rich Markdown documentation as the editor hover. This reuses the shared hover pipeline — the same sidecar hover handler and Markdown rendering code powers both surfaces. +The workspace symbols request retries up to three times with a two-second delay when the LSP client is unavailable or the connection fails transiently. -See [HOVER-SPEC.md](HOVER-SPEC.md) for the full hover specification, including symbol resolution, XML doc rendering, and caching strategy. +## Hover / Quick Info `[SE-HOVER]` -When the user hovers over a symbol node in the tree view, the extension sends a `textDocument/hover` request for that symbol's declaration position. The response is displayed as a VS Code tree item tooltip using `MarkdownString`. +On symbol hover, the extension sends `textDocument/hover` at the declaration position and renders the response as a tree-item `MarkdownString`, reusing the editor pipeline specified by [HOVER-SPEC.md](HOVER-SPEC.md). -## Context Menus +## Context Menus `[SE-CONTEXT-MENUS]` -Symbol nodes in the Solution Explorer expose context menu actions via `view/item/context` contribution points. Context menus are scoped by `contextValue` so that only relevant actions appear for each node type. +`view/item/context` contributions are scoped by each node's `contextValue`. -### Sort Members +### Sort Members `[SE-CONTEXT-SORT-MEMBERS]` -Right-clicking a type node (Class, Struct, Interface, Enum, Record) shows a **Sort Members** action that reorders the members of that type in the source file. +**Sort Members** reorders source members for Class, Struct, Interface, Enum, and Record nodes. | Property | Value | |----------|-------| @@ -244,11 +218,9 @@ Right-clicking a type node (Class, Struct, Interface, Enum, Record) shows a **So | When | `view == sharplsp.solutionExplorer && viewItem =~ /^symbol\.(class\|struct\|interface\|enum\|record)$/` | | Group | `1_modification` | -#### Sort Hierarchy +#### Sort Hierarchy `[SE-CONTEXT-SORT-HIERARCHY]` -The default sort hierarchy is **Accessibility → Category → Alphabetical**: - -1. **Accessibility** — members are grouped by access modifier using the same priority table as [Accessibility Sort Priority](#accessibility-sort-priority) +1. **Accessibility** — members are grouped by access modifier using [SE-SORT-ACCESS] 2. **Category** — within each accessibility group, members are grouped by kind: | Priority | Category | @@ -271,7 +243,7 @@ The default sort hierarchy is **Accessibility → Category → Alphabetical**: 3. **Alphabetical** — within each category group, members are sorted A-Z by name -#### Settings +#### Settings `[SE-CONTEXT-SORT-SETTINGS]` The sort hierarchy is configurable via the `sharplsp.memberSortOrder` setting: @@ -314,9 +286,9 @@ The sort hierarchy is configurable via the `sharplsp.memberSortOrder` setting: | `sharplsp.memberSortOrder.accessibilityOrder` | `string[]` | See above | Access modifier priority (first = highest) | | `sharplsp.memberSortOrder.categoryOrder` | `string[]` | See above | Member kind priority (first = highest) | -#### Implementation +#### Implementation `[SE-CONTEXT-SORT-IMPLEMENTATION]` -Sort Members is a **source-editing action** — it modifies the source file, not just the tree view. The flow: +Sort Members edits the source file: 1. User right-clicks a type node → selects "Sort Members" 2. Extension reads the type's `range` from the symbol data @@ -327,9 +299,9 @@ Sort Members is a **source-editing action** — it modifies the source file, not The tree view auto-refreshes after the edit (existing `onDidChangeTextDocument` listener). -### Copy Qualified Name +### Copy Qualified Name `[SE-CONTEXT-COPY-QUALIFIED]` -Right-clicking any symbol node shows a **Copy Qualified Name** action that copies the fully-qualified name (`Namespace.Type.Member`) to the clipboard. +**Copy Qualified Name** copies `Namespace.Type.Member` for any symbol node. | Property | Value | |----------|-------| @@ -340,9 +312,9 @@ Right-clicking any symbol node shows a **Copy Qualified Name** action that copie The qualified name is built by walking the tree from the node to the root, collecting namespace and type names. -### Copy Name +### Copy Name `[SE-CONTEXT-COPY-NAME]` -Right-clicking any symbol, project, or solution node shows a **Copy Name** action that copies the unqualified name to the clipboard. +**Copy Name** copies the unqualified name of a symbol, project, or solution. | Property | Value | |----------|-------| @@ -351,9 +323,9 @@ Right-clicking any symbol, project, or solution node shows a **Copy Name** actio | When | `view == sharplsp.solutionExplorer && viewItem =~ /^(symbol\.\|solution\|project)/ ` | | Group | `9_cutcopypaste` | -### Reveal in File Explorer +### Reveal in File Explorer `[SE-CONTEXT-REVEAL]` -Right-clicking a symbol node shows a **Reveal in File Explorer** action that reveals the file containing the symbol in the VS Code file explorer. +**Reveal in File Explorer** reveals a symbol's source file in VS Code's file explorer. | Property | Value | |----------|-------| @@ -362,9 +334,9 @@ Right-clicking a symbol node shows a **Reveal in File Explorer** action that rev | When | `view == sharplsp.solutionExplorer && viewItem =~ /^symbol\./ ` | | Group | `3_open` | -### Collapse All Children +### Collapse All Children `[SE-CONTEXT-COLLAPSE]` -Right-clicking any collapsible node shows a **Collapse All Children** action that collapses all descendant nodes. +**Collapse All Children** collapses every descendant of a collapsible node. | Property | Value | |----------|-------| @@ -373,13 +345,11 @@ Right-clicking any collapsible node shows a **Collapse All Children** action tha | When | `view == sharplsp.solutionExplorer` | | Group | `inline` | -## Build, Run, and Debug Actions - -The Solution Explorer provides direct access to common .NET CLI operations through context menus. +## Build, Run, and Debug Actions `[SE-ACTIONS]` -### Build and Rebuild +### Build and Rebuild `[SE-ACTIONS-BUILD]` -Right-clicking a solution or project node shows **Build** and **Rebuild** actions. +Solution and project nodes expose **Build** and **Rebuild**. | Property | Value | |----------|-------| @@ -401,9 +371,9 @@ Right-clicking a solution or project node shows **Build** and **Rebuild** action - Output appears in VS Code terminal - Progress notification shown during build -### Run and Debug +### Run and Debug `[SE-ACTIONS-RUN-DEBUG]` -Right-clicking a project node shows **Run** and **Debug** actions. +Project nodes expose **Run** and **Debug**. | Property | Value | |----------|-------| @@ -428,9 +398,7 @@ Right-clicking a project node shows **Run** and **Debug** actions. - Uses the `sharplsp` debug configuration type - Attaches debugger to the running process -### Configure Extra Arguments - -Users can configure extra arguments for dotnet commands via context menu or settings. +### Configure Extra Arguments `[SE-ACTIONS-ARGS]` | Property | Value | |----------|-------| @@ -458,11 +426,11 @@ Users can configure extra arguments for dotnet commands via context menu or sett 2. Global setting `sharplsp.*.extraArgs` 3. No extra args (lowest priority) -## Solution Management +## Solution Management `[SE-SOLUTION]` -### Add Project to Solution +### Add Project to Solution `[SE-SOLUTION-ADD]` -Right-clicking a `.csproj` or `.fsproj` file in the VS Code file explorer shows **Add to Solution** when a solution is loaded. +When a solution is loaded, `.csproj` and `.fsproj` files expose **Add to Solution**. | Property | Value | |----------|-------| @@ -476,9 +444,9 @@ Right-clicking a `.csproj` or `.fsproj` file in the VS Code file explorer shows - Refreshes Solution Explorer after adding - Shows error if no solution is loaded -### Remove Project from Solution +### Remove Project from Solution `[SE-SOLUTION-REMOVE]` -Right-clicking a project node in the Solution Explorer shows **Remove from Solution**. +Project nodes expose **Remove from Solution**. | Property | Value | |----------|-------| @@ -492,9 +460,9 @@ Right-clicking a project node in the Solution Explorer shows **Remove from Solut - Runs `dotnet sln remove ` - Refreshes Solution Explorer after removing -### Context Value Mapping +### Context Value Mapping `[SE-CONTEXT-VALUES]` -To support scoped context menus, symbol nodes set `contextValue` based on their kind: +Nodes set `contextValue` by kind: | Symbol Kind | contextValue | |-------------|-------------| @@ -518,24 +486,15 @@ To support scoped context menus, symbol nodes set `contextValue` based on their | Project Reference | `projectReference` | | Dependency Folder | `dependencyFolder` | -## Navigation +## Navigation `[SE-NAVIGATION]` Clicking a symbol node opens the file and navigates to the symbol's declaration position. ## Active Editor Synchronization `[SE-ACTIVE-EDITOR-SYNC]` -The Solution Explorer MUST stay synchronized with the active text editor. When a -C# or F# document becomes active — opened, focused, or navigated to (Go to -Definition, Quick Open, tab switch) — the tree MUST reveal that document's node: -expand its ancestors, scroll it into view, and **select (highlight)** it. Example: -focusing `FSharpRename.fs` in the editor expands the tree to it and highlights it. -Switching the active editor re-syncs the selection to the new document. This -mirrors VS Code's built-in File Explorer `explorer.autoReveal` behaviour. +When a C# or F# document becomes active through open, focus, navigation, Quick Open, or tab switch, the tree MUST expand its ancestors, reveal its node, and select it without stealing focus. This editor-to-tree behavior is the inverse of [SE-CONTEXT-REVEAL]. -This is the inverse of [Reveal in File Explorer](#reveal-in-file-explorer) -(tree → editor); here the direction is **editor → tree**. - -### Requirements +### Requirements `[SE-ACTIVE-EDITOR-SYNC-REQUIREMENTS]` | # | Requirement | |---|-------------| @@ -546,9 +505,7 @@ This is the inverse of [Reveal in File Explorer](#reveal-in-file-explorer) | 5 | A setting (mirroring `explorer.autoReveal`, default **on**) MUST gate the behaviour so users can disable it. | | 6 | Revealing MUST NOT steal editor focus (`focus: false`) and MUST be a no-op when the active document has no corresponding node (e.g. files outside the loaded solution). | -Tracked in [issue #118](https://github.com/Nimblesite/SharpLsp/issues/118). - -## Key Files +## Key Files `[SE-FILES]` | File | Purpose | |------|---------| @@ -557,3 +514,4 @@ Tracked in [issue #118](https://github.com/Nimblesite/SharpLsp/issues/118). | `editors/vscode/src/constants.ts` | Command and view ID constants | | `editors/vscode/package.json` | VS Code contribution points | | `src/workspace_symbols.rs` | Rust handler: sidecar solution model routing, tree-sitter symbol extraction | +| `editors/vscode/src/test/suite/solution-explorer.test.ts` | Coarse tree, command, reactivity, and live-buffer coverage | diff --git a/docs/specs/VSCODE-REACTIVITY-SPEC.md b/docs/specs/VSCODE-REACTIVITY-SPEC.md index b99cc3e4..44d1c9b1 100644 --- a/docs/specs/VSCODE-REACTIVITY-SPEC.md +++ b/docs/specs/VSCODE-REACTIVITY-SPEC.md @@ -1,4 +1,4 @@ -# VSCode Extension Reactivity Spec +# VSCode Extension Reactivity Spec `[VSCODE-REACTIVITY]` **Status:** active **Owner:** VSCode extension (`editors/vscode/src/`) @@ -6,17 +6,15 @@ --- -## 1. Goal +## Goal `[VSCODE-REACTIVITY-GOAL]` -Every UI surface in the SharpLsp VSCode extension — webview panels, tree views, status bars, code lenses — must be a **pure projection of reactive state**. When the underlying data changes (whether by user action, LSP notification, file-system event, or another tool editing files on disk), **every surface reading that data must update automatically**, with no explicit refresh call from the user or from Claude. +Every webview, tree view, status bar, and code lens MUST be a projection of reactive state. A user action, LSP notification, file-system event, or external disk edit MUST update every dependent surface automatically; correctness MUST NOT depend on Refresh, reopening a panel, or changing focus. -A UI surface that requires the user to click Refresh, reopen a panel, or toggle focus to see current data is **broken** and must be fixed. +## Signal Primitives `[VSCODE-REACTIVITY-SIGNALS]` -## 2. Signal Primitives +The extension uses the in-repo `Signal` primitive in [`signals.ts`](../../editors/vscode/src/signals.ts); no external signal library is introduced. -The extension uses a single in-repo reactive primitive: the `Signal` class in [editors/vscode/src/signals.ts](../../editors/vscode/src/signals.ts). No external dependency (Preact Signals, alien-signals, SolidJS) is introduced — the native primitive is sufficient and keeps the bundle small. - -### Signal +### Signal `[VSCODE-REACTIVITY-SIGNALS-VALUE]` ```ts class Signal { @@ -27,7 +25,7 @@ class Signal { } ``` -### effect(fn) +### effect(fn) `[VSCODE-REACTIVITY-SIGNALS-EFFECT]` ```ts function effect(fn: () => void): () => void @@ -37,7 +35,7 @@ Runs `fn` once, tracks every `Signal.value` read during the call, and re-runs `f Use `effect()` for UI rendering code that reads multiple signals. Use `subscribe()` for imperative side-effects driven by a single signal. -## 3. Source-of-Truth Signals +## Source-of-Truth Signals `[VSCODE-REACTIVITY-STATE]` The extension maintains these **global signals** (module-level exports). Every UI surface that needs the data reads it from these, never from a local cache. @@ -49,13 +47,13 @@ The extension maintains these **global signals** (module-level exports). Every U | `sortOrder` | [state.ts](../../editors/vscode/src/state.ts) | Solution Explorer sort cycle | | `projectDependencies` | [project-deps-store.ts](../../editors/vscode/src/project-deps-store.ts) | `Map` — PackageReferences & ProjectReferences per csproj/fsproj | -New source-of-truth state must be added to one of these modules (or a new peer module). It **must not** be shadowed by a local field in a UI component — UI components read signals directly. +New source-of-truth state MUST live in one of these modules or a peer store and MUST NOT be shadowed in a UI field. Derived flags and version strings, including package installation state, MUST be computed from live signals during rendering rather than stored in selection snapshots. -## 4. File-System Watchers Drive Derived State +## File-System Watchers Drive Derived State `[VSCODE-REACTIVITY-WATCHERS]` State derived from files on disk is refreshed by a `vscode.workspace.createFileSystemWatcher` whose change events write to the corresponding signal. There is **no polling**, and the user never has to trigger a refresh manually. -### Project-dependencies watcher +### Project-dependencies Watcher `[VSCODE-REACTIVITY-WATCHERS-PROJECTS]` Registered once during `activate()` by [project-deps-store.ts](../../editors/vscode/src/project-deps-store.ts) on the glob: @@ -68,11 +66,15 @@ Registered once during `activate()` by [project-deps-store.ts](../../editors/vsc - `onDidDelete` → remove the entry - Directory.Packages.props changes → rescan every tracked project -### Contract: after any external csproj/fsproj write, every surface that reads `projectDependencies` re-renders within ~200 ms (debounce + VSCode FSW latency). +### Update Latency `[VSCODE-REACTIVITY-WATCHERS-LATENCY]` + +After an external `.csproj` or `.fsproj` write, every surface that reads `projectDependencies` MUST re-render within approximately 200 ms, including the 150 ms debounce and VSCode file-watcher latency. + +## UI Surfaces and Their Subscriptions `[VSCODE-REACTIVITY-SURFACES]` -## 5. UI Surfaces and Their Subscriptions +### Solution Explorer Tree `[VSCODE-REACTIVITY-SURFACES-TREE]` -### 5.1 Solution Explorer tree — [tree.ts](../../editors/vscode/src/tree.ts) +Implementation: [`tree.ts`](../../editors/vscode/src/tree.ts). `SolutionExplorerProvider` subscribes to: - `symbolsState` → full rebuild @@ -81,23 +83,25 @@ Registered once during `activate()` by [project-deps-store.ts](../../editors/vsc The tree's Dependencies → Packages node reads the parsed package list from `projectDependencies.value.get(projectPath)`. **It does NOT call `parseProjectDependencies` directly.** The file watcher is the only code path that calls the parser. -### 5.2 NuGet Browser panel — [nuget-browser.ts](../../editors/vscode/src/nuget-browser.ts) +### NuGet Browser Panel `[VSCODE-REACTIVITY-SURFACES-NUGET]` + +Implementation: [`nuget-browser.ts`](../../editors/vscode/src/nuget-browser.ts). `NuGetBrowserPanel` subscribes to: - `projectDependencies` → reload installed packages via LSP (picks up external csproj edits) The Install/Remove button label is driven by the csproj content as surfaced through `projectDependencies` plus the LSP's `sharplsp/nuget/installed` response. Editing the csproj on disk must flip the button without any user action. -## 6. DRY: one renderer, one icon +## Shared Rendering `[VSCODE-REACTIVITY-RENDERING]` -Identical visual elements must be rendered by a **single function**. Specifically: +Identical visual elements MUST use one renderer: - Every package row (Browse tab, Installed tab, details panel header) uses the same icon box structure, with the same `packageIconImg(pkg)` helper rendering the iconUrl `` overlay. Duplicated inline HTML for the same visual element is forbidden. - When a surface needs the same data shape as another (e.g. the Installed tab rendering the same row as Browse), the data is hydrated into the common shape (`NuGetSearchResult`) and passed to the single renderer. -## 7. Required Tests (non-negotiable) +## Required Tests `[VSCODE-REACTIVITY-TESTING]` -Every reactive surface must have an e2e test that: +Every reactive surface MUST have an end-to-end test that: 1. Opens the surface with a known initial state. 2. Mutates the underlying source (file on disk, LSP state, etc.) _without calling any refresh API_. 3. Polls the surface and asserts the new state appears within a timeout. @@ -111,12 +115,3 @@ Current coverage: | NuGet details panel icon | `details panel renders package icon image when iconUrl present` | [nuget-browser.test.ts](../../editors/vscode/src/test/suite/nuget-browser.test.ts) | | NuGet installed tab icons (DRY) | `installed tab renders icons (no DRY violation)` | [nuget-browser.test.ts](../../editors/vscode/src/test/suite/nuget-browser.test.ts) | | Solution Explorer packages node | `Dependencies → Packages tree reacts to external csproj edit` | [solution-explorer.test.ts](../../editors/vscode/src/test/suite/solution-explorer.test.ts) | - -## 8. Anti-patterns (illegal) - -- **Caching data that has a reactive source** in a local field. If `projectDependencies` has the data, read it directly every render. -- **Calling a parser or disk read from a UI component.** Only the watcher/store module does that. -- **Exposing a manual Refresh button** as the primary way to sync state. Refresh buttons may exist as a user escape hatch; they must not be the load-bearing update mechanism. -- **Duplicated inline HTML for the same visual element.** Extract a helper. -- **Diverging representations** of the same data (e.g. a bespoke installed-row renderer alongside the main package-row renderer). -- **Snapshotting derived state into a stored object.** Example: storing `selectedPackage` with an `isInstalled` boolean baked in at selection time. The snapshot becomes stale the moment the underlying data changes. **Always derive boolean flags, version strings, and other derived fields from the live source-of-truth signal at render time.** The renderer is the only correct place to compute "is this package currently installed" — never the selection handler. diff --git a/website/eleventy.config.js b/website/eleventy.config.js index a4a4ae25..d31975d4 100644 --- a/website/eleventy.config.js +++ b/website/eleventy.config.js @@ -28,29 +28,28 @@ function removeOutputSymlinks(directory) { } } -// Patch plugin layouts with local overrides before Eleventy registers virtual templates -for (const file of ["base.njk", "blog.njk", "docs.njk", "prose.njk", "author.njk"]) { +// Techdoc 0.2 registers virtual templates without an override hook. Keep the +// site-owned sources durable and copy only the templates the plugin registers. +for (const file of ["base.njk", "blog.njk", "docs.njk"]) { const local = join(localLayouts, file); if (existsSync(local)) { writeFileSync(join(pluginLayouts, file), readFileSync(local, "utf-8")); } } -// Patch plugin page templates (tags, categories, blog index) with local overrides -for (const file of ["tags-pages.njk", "categories-pages.njk"]) { - const local = join(localOverrides, file); +// Patch the virtual blog pages from stable, site-owned override files. Never +// delete or move these sources: repeated builds must produce identical output. +for (const [target, source] of [ + ["index.njk", "blog-index.njk"], + ["tags-pages.njk", "tags-pages.njk"], + ["categories-pages.njk", "categories-pages.njk"], +]) { + const local = join(localOverrides, source); if (existsSync(local)) { - writeFileSync(join(pluginPages, "blog", file), readFileSync(local, "utf-8")); + writeFileSync(join(pluginPages, "blog", target), readFileSync(local, "utf-8")); } } -// Patch plugin blog index with local override so the virtual template uses our version -const localBlogIndex = join(__dirname, "src/blog/index.njk"); -if (existsSync(localBlogIndex)) { - writeFileSync(join(pluginPages, "blog/index.njk"), readFileSync(localBlogIndex, "utf-8")); - unlinkSync(localBlogIndex); -} - export default function (eleventyConfig) { eleventyConfig.on("eleventy.before", () => { removeOutputSymlinks(outputDir); @@ -60,7 +59,7 @@ export default function (eleventyConfig) { site: { name: "SharpLsp", url: "https://sharplsp.dev", - description: "Open-source .NET language server for C# and F#. One server, every editor.", + description: "A complete open-source C# and F# development experience for every editor.", stylesheet: "/assets/css/styles.css", }, features: { diff --git a/website/package-lock.json b/website/package-lock.json index 1927280c..9e79aaf2 100644 --- a/website/package-lock.json +++ b/website/package-lock.json @@ -10,7 +10,7 @@ "dependencies": { "@11ty/eleventy": "^3.1.6", "@playwright/test": "^1.61.1", - "eleventy-plugin-techdoc": "^0.2.0", + "eleventy-plugin-techdoc": "0.2.0", "markdown-it": "^14.3.0" }, "devDependencies": { @@ -759,9 +759,9 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", - "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", diff --git a/website/package.json b/website/package.json index 3e589cd5..ac541863 100644 --- a/website/package.json +++ b/website/package.json @@ -12,7 +12,7 @@ "dependencies": { "@11ty/eleventy": "^3.1.6", "@playwright/test": "^1.61.1", - "eleventy-plugin-techdoc": "^0.2.0", + "eleventy-plugin-techdoc": "0.2.0", "markdown-it": "^14.3.0" }, "devDependencies": { diff --git a/website/src/_data/i18n.json b/website/src/_data/i18n.json index b9e791ba..79da7be7 100644 --- a/website/src/_data/i18n.json +++ b/website/src/_data/i18n.json @@ -3,20 +3,80 @@ "blog": { "back": "Back to Blog", "title": "Blog", - "subtitle": "Latest posts and updates", + "eyebrow": "SharpLsp journal", + "subtitle": "Engineering notes, release deep-dives, and ideas for a better .NET development experience.", + "all": "All", "tags": "Tags", "categories": "Categories", + "search": "Search articles", + "filterLabel": "Blog filters", + "articles": "Articles", "empty": "No blog posts yet. Check back soon!" }, "home": { - "badgeAvailable": "Available on GitHub", - "badgeOpenSource": "Open Source · MIT License", - "ecosystemTooling": ".NET ecosystem tooling", - "notAffiliated": "Independent open source. Not affiliated with Microsoft.", - "releasesTitle": "Latest Releases", - "releasesSubtitle": "Generated from the published GitHub Releases for SharpLsp.", - "releasesViewAll": "View all releases", - "releasePrerelease": "Prerelease" + "eyebrow": "Open-source .NET tooling · C# + F#", + "title": "A complete .NET development experience.", + "titleAccent": "In your editor.", + "lede": "SharpLsp is the open-source alternative to Visual Studio, Rider, and C# Dev Kit — language intelligence, solution tooling, refactoring, NuGet, debugging, and profiling for C# and F# without subscriptions or vendor lock-in.", + "engines": "C# via Roslyn. F# via FSharp.Compiler.Service.", + "install": "Install SharpLsp", + "github": "View on GitHub", + "trustLabel": "Project facts", + "trust": ["MIT licensed", "Windows, macOS & Linux", "No account required"], + "screenshotAlt": "SharpLsp running a full .NET solution in VS Code with C# navigation, solution explorer, and a graphical NuGet package manager", + "screenshotCaption": "A real .NET workspace powered by SharpLsp — solution explorer, code intelligence, NuGet, debugging, and profiling included.", + "proofLabel": "SharpLsp foundations", + "proof": [ + { "title": "C#", "detail": "Roslyn intelligence" }, + { "title": "F#", "detail": "First-class FCS support" }, + { "title": "Cross-platform", "detail": "Windows · macOS · Linux" }, + { "title": "Open source", "detail": "MIT, forever" } + ], + "workflowEyebrow": "The whole workflow", + "workflowTitle": "One server. Everything you need to ship .NET.", + "workflowIntro": "SharpLsp goes beyond autocomplete. It brings the connected, project-aware experience developers expect from a full IDE to the editor they already use.", + "workflows": [ + { + "title": "Write & navigate", + "description": "Fast, solution-wide language intelligence that understands real C# and F# projects.", + "features": ["Completions, hover, and signature help", "Definitions, references, and symbols", "Solution explorer and project context"] + }, + { + "title": "Refactor & maintain", + "description": "Make large codebases safer to change with compiler-grade analysis and precise edits.", + "features": ["Diagnostics and static analyzers", "Rename, code actions, and refactoring", "Roslyn and Fantomas formatting"] + }, + { + "title": "Build, debug & profile", + "description": "Stay in one workspace from dependency management to the final performance pass.", + "features": ["NuGet search, install, and updates", "Build, test, and debugging workflows", "CPU and allocation profiling"] + } + ], + "languageEyebrow": "First-class by design", + "languageTitle": "C# and F#. No second-class language.", + "languageIntro": "Each language gets its native compiler services and the same commitment to a complete development workflow.", + "languages": [ + { "name": "C#", "engine": "Powered by Roslyn", "description": "The same compiler platform behind Visual Studio, delivered through open protocols." }, + { "name": "F#", "engine": "Powered by FSharp.Compiler.Service", "description": "F# support is designed alongside C# support, not bolted on after it." } + ], + "alternativeEyebrow": "Own your toolchain", + "alternativeTitle": "The open-source alternative to Visual Studio, Rider, and C# Dev Kit.", + "alternativeIntro": "Keep the depth of a .NET IDE without giving up your editor, your platform, or control of your development environment.", + "reasons": [ + { "title": "Editor-independent", "description": "Use VS Code, Zed, or any standards-based editor instead of rebuilding your workflow around one IDE." }, + { "title": "No proprietary core", "description": "Open implementations, open protocols, and source you can inspect from end to end." }, + { "title": "No subscription", "description": "MIT licensed for personal, commercial, and organizational use." }, + { "title": "Cross-platform", "description": "The same C# and F# development experience on Windows, macOS, and Linux." } + ], + "releasesTitle": "What’s shipping", + "releaseEyebrow": "Latest releases", + "releasesSubtitle": "The latest improvements, fixes, and new .NET tooling in SharpLsp.", + "releasesViewAll": "View every release", + "releaseNotes": "Read release notes", + "releasePrerelease": "Prerelease", + "ctaEyebrow": "Your editor. The full stack.", + "ctaTitle": "Build C# and F# without the IDE lock-in.", + "ctaCopy": "Install SharpLsp, open your solution, and keep the editor you already know." }, "releases": { "title": "Releases", @@ -65,20 +125,68 @@ "blog": { "back": "返回博客", "title": "博客", - "subtitle": "最新文章与动态", + "eyebrow": "SharpLsp 技术日志", + "subtitle": "工程笔记、版本解析,以及打造更好 .NET 开发体验的思考。", + "all": "全部", "tags": "标签", "categories": "分类", + "search": "搜索文章", + "filterLabel": "博客筛选", + "articles": "文章", "empty": "暂无博客文章,请稍后再来!" }, "home": { - "badgeAvailable": "现已在 GitHub 提供", - "badgeOpenSource": "开源 · MIT 许可证", - "ecosystemTooling": ".NET 生态工具", - "notAffiliated": "独立开源项目。与 Microsoft 无关。", - "releasesTitle": "最新版本", - "releasesSubtitle": "此列表由 SharpLsp 的 GitHub Releases 自动生成。", + "eyebrow": "开源 .NET 工具 · C# + F#", + "title": "完整的 .NET 开发体验。", + "titleAccent": "就在你的编辑器中。", + "lede": "SharpLsp 是 Visual Studio、Rider 和 C# Dev Kit 的开源替代方案,为 C# 与 F# 提供语言智能、解决方案工具、重构、NuGet、调试和性能分析,无需订阅,也没有厂商锁定。", + "engines": "C# 由 Roslyn 驱动,F# 由 FSharp.Compiler.Service 驱动。", + "install": "安装 SharpLsp", + "github": "在 GitHub 上查看", + "trustLabel": "项目特点", + "trust": ["MIT 许可证", "支持 Windows、macOS 与 Linux", "无需账户"], + "screenshotAlt": "SharpLsp 在 VS Code 中运行完整 .NET 解决方案,显示 C# 导航、解决方案资源管理器和图形化 NuGet 包管理器", + "screenshotCaption": "由 SharpLsp 驱动的真实 .NET 工作区 — 包含解决方案资源管理器、代码智能、NuGet、调试和性能分析。", + "proofLabel": "SharpLsp 基础能力", + "proof": [ + { "title": "C#", "detail": "Roslyn 语言智能" }, + { "title": "F#", "detail": "一等 FCS 支持" }, + { "title": "跨平台", "detail": "Windows · macOS · Linux" }, + { "title": "开源", "detail": "永久 MIT 许可" } + ], + "workflowEyebrow": "完整工作流", + "workflowTitle": "一个服务器,覆盖交付 .NET 所需的一切。", + "workflowIntro": "SharpLsp 不止提供自动补全。它把开发者期望从完整 IDE 获得的、具备项目上下文的连贯体验带到你已经在用的编辑器。", + "workflows": [ + { "title": "编写与导航", "description": "理解真实 C# 与 F# 项目的快速、全解决方案语言智能。", "features": ["补全、悬停与签名帮助", "定义、引用与符号", "解决方案资源管理器与项目上下文"] }, + { "title": "重构与维护", "description": "借助编译器级分析和精确编辑,让大型代码库更安全地演进。", "features": ["诊断与静态分析器", "重命名、代码操作与重构", "Roslyn 与 Fantomas 格式化"] }, + { "title": "构建、调试与分析", "description": "从依赖管理到最终性能优化,始终留在同一个工作区。", "features": ["NuGet 搜索、安装与更新", "构建、测试与调试工作流", "CPU 与内存分配分析"] } + ], + "languageEyebrow": "生来就是一等公民", + "languageTitle": "C# 与 F#,没有二等语言。", + "languageIntro": "每种语言都使用原生编译器服务,并获得同等完整开发工作流的承诺。", + "languages": [ + { "name": "C#", "engine": "由 Roslyn 驱动", "description": "Visual Studio 背后的同一编译器平台,通过开放协议交付。" }, + { "name": "F#", "engine": "由 FSharp.Compiler.Service 驱动", "description": "F# 与 C# 同步设计,而不是事后附加。" } + ], + "alternativeEyebrow": "掌控你的工具链", + "alternativeTitle": "Visual Studio、Rider 和 C# Dev Kit 的开源替代方案。", + "alternativeIntro": "保留 .NET IDE 的深度,同时不放弃你的编辑器、平台或对开发环境的控制。", + "reasons": [ + { "title": "不依赖编辑器", "description": "使用 VS Code、Zed 或任何基于标准的编辑器,无需围绕单一 IDE 重建工作流。" }, + { "title": "无专有核心", "description": "开放实现、开放协议,以及可以端到端检查的源代码。" }, + { "title": "无需订阅", "description": "MIT 许可适用于个人、商业和组织用途。" }, + { "title": "跨平台", "description": "在 Windows、macOS 和 Linux 上获得一致的 C# 与 F# 开发体验。" } + ], + "releasesTitle": "最新进展", + "releaseEyebrow": "最新版本", + "releasesSubtitle": "SharpLsp 最新的改进、修复与 .NET 工具能力。", "releasesViewAll": "查看所有版本", - "releasePrerelease": "预发布" + "releaseNotes": "阅读发行说明", + "releasePrerelease": "预发布", + "ctaEyebrow": "你的编辑器,完整工具栈。", + "ctaTitle": "开发 C# 与 F#,无需被 IDE 锁定。", + "ctaCopy": "安装 SharpLsp,打开解决方案,继续使用你熟悉的编辑器。" }, "releases": { "title": "版本发布", @@ -127,20 +235,68 @@ "blog": { "back": "ブログへ戻る", "title": "ブログ", - "subtitle": "最新記事とアップデート", + "eyebrow": "SharpLsp ジャーナル", + "subtitle": "エンジニアリングノート、リリース解説、より良い .NET 開発体験のためのアイデア。", + "all": "すべて", "tags": "タグ", "categories": "カテゴリー", + "search": "記事を検索", + "filterLabel": "ブログの絞り込み", + "articles": "記事", "empty": "ブログ記事はまだありません。しばらくしてからご確認ください。" }, "home": { - "badgeAvailable": "GitHub で公開中", - "badgeOpenSource": "オープンソース · MIT ライセンス", - "ecosystemTooling": ".NET エコシステムツール", - "notAffiliated": "独立したオープンソースです。Microsoft とは無関係です。", - "releasesTitle": "最新リリース", - "releasesSubtitle": "SharpLsp の GitHub Releases から自動生成されます。", + "eyebrow": "オープンソース .NET ツール · C# + F#", + "title": "SharpLsp で完全な .NET 開発体験を。", + "titleAccent": "いつものエディターで。", + "lede": "SharpLsp は Visual Studio、Rider、C# Dev Kit に代わるオープンソースの選択肢です。C# と F# の言語機能、ソリューション管理、リファクタリング、NuGet、デバッグ、プロファイリングを、サブスクリプションやベンダーロックインなしで提供します。", + "engines": "C# は Roslyn、F# は FSharp.Compiler.Service を使用。", + "install": "SharpLsp をインストール", + "github": "GitHub で見る", + "trustLabel": "プロジェクト情報", + "trust": ["MIT ライセンス", "Windows・macOS・Linux", "アカウント不要"], + "screenshotAlt": "VS Code で完全な .NET ソリューションを実行する SharpLsp。C# ナビゲーション、ソリューションエクスプローラー、NuGet パッケージマネージャーを表示", + "screenshotCaption": "SharpLsp が支える実際の .NET ワークスペース — ソリューション管理、コードインテリジェンス、NuGet、デバッグ、プロファイリングを搭載。", + "proofLabel": "SharpLsp の基盤", + "proof": [ + { "title": "C#", "detail": "Roslyn の言語機能" }, + { "title": "F#", "detail": "第一級の FCS サポート" }, + { "title": "クロスプラットフォーム", "detail": "Windows · macOS · Linux" }, + { "title": "オープンソース", "detail": "永続的な MIT ライセンス" } + ], + "workflowEyebrow": "ワークフロー全体", + "workflowTitle": "1 つのサーバーで、.NET の開発から出荷まで。", + "workflowIntro": "SharpLsp は自動補完だけではありません。フル IDE に期待される、プロジェクトを理解した一貫した体験を、使い慣れたエディターへ届けます。", + "workflows": [ + { "title": "記述とナビゲーション", "description": "実際の C# / F# プロジェクトを理解する、高速でソリューション全体に及ぶ言語機能。", "features": ["補完、ホバー、シグネチャヘルプ", "定義、参照、シンボル", "ソリューションエクスプローラーとプロジェクト情報"] }, + { "title": "リファクタリングと保守", "description": "コンパイラー品質の解析と正確な編集で、大規模なコードベースを安全に変更。", "features": ["診断と静的解析", "名前変更、コードアクション、リファクタリング", "Roslyn と Fantomas による整形"] }, + { "title": "ビルド、デバッグ、解析", "description": "依存関係の管理から最後のパフォーマンス調整まで、1 つのワークスペースで完結。", "features": ["NuGet の検索、インストール、更新", "ビルド、テスト、デバッグ", "CPU とアロケーションのプロファイリング"] } + ], + "languageEyebrow": "最初から第一級", + "languageTitle": "C# と F#。二番手の言語はありません。", + "languageIntro": "それぞれの言語がネイティブのコンパイラーサービスを使い、完全な開発ワークフローへの同じ取り組みを受けます。", + "languages": [ + { "name": "C#", "engine": "Roslyn を採用", "description": "Visual Studio と同じコンパイラープラットフォームを、オープンなプロトコルで提供。" }, + { "name": "F#", "engine": "FSharp.Compiler.Service を採用", "description": "F# は後付けではなく、C# と並行して設計されます。" } + ], + "alternativeEyebrow": "ツールチェーンを自分の手に", + "alternativeTitle": "Visual Studio、Rider、C# Dev Kit に代わるオープンソース。", + "alternativeIntro": "エディターやプラットフォーム、開発環境の主導権を手放さずに、.NET IDE の深さを保てます。", + "reasons": [ + { "title": "エディターに依存しない", "description": "単一の IDE に合わせてワークフローを作り直さず、VS Code、Zed、標準対応エディターを利用できます。" }, + { "title": "プロプライエタリな中核なし", "description": "オープンな実装とプロトコル、最初から最後まで確認できるソースコード。" }, + { "title": "サブスクリプション不要", "description": "個人、商用、組織で利用できる MIT ライセンス。" }, + { "title": "クロスプラットフォーム", "description": "Windows、macOS、Linux で同じ C# / F# 開発体験。" } + ], + "releasesTitle": "開発中の最新機能", + "releaseEyebrow": "最新リリース", + "releasesSubtitle": "SharpLsp に加わった最新の改善、修正、.NET ツール機能。", "releasesViewAll": "すべてのリリースを見る", - "releasePrerelease": "プレリリース" + "releaseNotes": "リリースノートを読む", + "releasePrerelease": "プレリリース", + "ctaEyebrow": "いつものエディターに、すべてを。", + "ctaTitle": "IDE に縛られず C# と F# を開発。", + "ctaCopy": "SharpLsp をインストールしてソリューションを開き、使い慣れたエディターをそのまま使えます。" }, "releases": { "title": "リリース", diff --git a/website/src/_data/release.js b/website/src/_data/release.js index 1203b4b3..44e0e994 100644 --- a/website/src/_data/release.js +++ b/website/src/_data/release.js @@ -1,9 +1,12 @@ +import markdownIt from "markdown-it"; + const REPO = "Nimblesite/SharpLsp"; // Full set powers the on-site /releases/ page; the homepage shows `recent` only. const MAX_RELEASES = 30; -const RECENT_COUNT = 4; +const RECENT_COUNT = 2; const API_URL = `https://api.github.com/repos/${REPO}/releases?per_page=${MAX_RELEASES}`; const RELEASES_URL = `https://github.com/${REPO}/releases`; +const releaseMarkdown = markdownIt({ html: false, linkify: true }); function fallback(reason) { if (reason) console.warn(`[_data/release] using fallback — ${reason}`); @@ -33,6 +36,42 @@ function formatDate(value) { }).format(new Date(value)); } +function inlineText(token) { + return (token.children || []) + .filter((child) => ["text", "code_inline"].includes(child.type)) + .map((child) => child.content) + .join(" ") + .trim(); +} + +function linkedText(token) { + const start = (token.children || []).findIndex((child) => child.type === "link_open"); + const linked = start < 0 ? [] : token.children.slice(start + 1); + const end = linked.findIndex((child) => child.type === "link_close"); + return inlineText({ children: end < 0 ? linked : linked.slice(0, end) }); +} + +function truncateSummary(summary) { + return summary.length > 180 ? `${summary.slice(0, 177).trimEnd()}…` : summary; +} + +function isWebUrl(value) { + try { + return ["http:", "https:"].includes(new URL(value).protocol); + } catch { + return false; + } +} + +function summarizeRelease(body) { + const tokens = releaseMarkdown.parse(body, {}); + const token = tokens.find((item, index) => + item.type === "inline" && tokens[index - 1]?.type !== "heading_open" && inlineText(item), + ); + const summary = token ? linkedText(token) || inlineText(token) : null; + return summary && !isWebUrl(summary) ? truncateSummary(summary) : null; +} + function mapRelease(data) { const tag = data.tag_name; const publishedAt = data.published_at || null; @@ -44,6 +83,7 @@ function mapRelease(data) { publishedAt, publishedDate: formatDate(publishedAt), prerelease: Boolean(data.prerelease), + summary: summarizeRelease(data.body || ""), // Raw GitHub release notes (Markdown). Rendered at build time by the // `releaseNotes` filter with raw HTML disabled, so untrusted PR-title // content in generated notes cannot inject markup. diff --git a/website/src/_data/site.json b/website/src/_data/site.json index d47eed4e..932c68f6 100644 --- a/website/src/_data/site.json +++ b/website/src/_data/site.json @@ -1,20 +1,20 @@ { "title": "SharpLsp", "name": "SharpLsp", - "description": "Open-source .NET language server for C# and F#. Full Roslyn IntelliSense, diagnostics, refactoring, and NuGet management in VS Code, Zed, Neovim, and every LSP-compatible editor.", + "description": "A complete open-source C# and F# development experience for VS Code, Zed, and every LSP editor — an alternative to Visual Studio, Rider, and C# Dev Kit.", "url": "https://sharplsp.dev", "author": "SharpLsp Team", "stylesheet": "/assets/css/styles.css", - "favicon": "/assets/favicon.svg", - "themeColor": "#19d078", - "keywords": "C# language server, F# language server, .NET LSP, open source C# extension, OmniSharp alternative, C# Dev Kit alternative, Roslyn language server, FSharp.Compiler.Service, VS Code C#, Neovim .NET, Helix C#, Zed .NET", + "favicon": "/assets/images/sharplsp-logo.svg", + "themeColor": "#0f7f49", + "keywords": "C# VS Code, C# IDE, .NET IDE, Visual Studio alternative, Rider alternative, C# Dev Kit alternative, C# language server, F# language server, .NET LSP, open source C# extension, Roslyn language server, FSharp.Compiler.Service", "ogImage": "/assets/images/og-image.png", "ogImageWidth": "1200", "ogImageHeight": "630", "searchUrl": "https://sharplsp.dev/blog", "organization": { "name": "SharpLsp", - "logo": "/assets/favicon.svg", + "logo": "/assets/images/sharplsp-logo.svg", "sameAs": [ "https://github.com/Nimblesite/SharpLsp" ] diff --git a/website/src/_includes/layouts/base.njk b/website/src/_includes/layouts/base.njk index b3c87525..32000a0b 100644 --- a/website/src/_includes/layouts/base.njk +++ b/website/src/_includes/layouts/base.njk @@ -28,11 +28,7 @@ - - - - - + @@ -143,6 +139,8 @@ + + {% block head %}{% endblock %} @@ -158,41 +156,27 @@
- - diff --git a/website/src/_includes/layouts/blog.njk b/website/src/_includes/layouts/blog.njk index e2102b3d..e7540236 100644 --- a/website/src/_includes/layouts/blog.njk +++ b/website/src/_includes/layouts/blog.njk @@ -1,4 +1,19 @@ --- -layout: layouts/prose.njk +layout: layouts/base.njk --- -{{ content | safe }} +
+
+
+ {{ category | default("Engineering") | capitalize }} + +
+

{{ title }}

+ {% include "partials/article-author.njk" %} + {% if image %} +
+ {{ imageAlt | default(title) }} +
+ {% endif %} +
+ {{ content | safe }} +
diff --git a/website/src/_includes/layouts/docs.njk b/website/src/_includes/layouts/docs.njk index 501e505d..ef79c719 100644 --- a/website/src/_includes/layouts/docs.njk +++ b/website/src/_includes/layouts/docs.njk @@ -1,23 +1,28 @@ --- -layout: layouts/prose.njk +layout: layouts/base.njk --- -{% include "partials/docs-sidebar.njk" %} -
- {{ content | safe }} + +
+ diff --git a/website/src/_includes/layouts/prose.njk b/website/src/_includes/layouts/prose.njk index 5522d80b..24d76879 100644 --- a/website/src/_includes/layouts/prose.njk +++ b/website/src/_includes/layouts/prose.njk @@ -1,29 +1,6 @@ --- layout: layouts/base.njk --- - -{% if page.url.startsWith('/docs/') or page.url.startsWith('/zh/docs/') or page.url.startsWith('/ja/docs/') %} -
- {{ content | safe }} -
-{% else %}
- {% if date %} -
-
- {{ category | default("Engineering") | capitalize }} - -
-

{{ title }}

- {% include "partials/article-author.njk" %} - {% if image %} -
- {{ imageAlt | default(title) }} -
- {% endif %} -
- {% endif %} - {{ content | safe }}
-{% endif %} diff --git a/website/src/_includes/overrides/blog-index.njk b/website/src/_includes/overrides/blog-index.njk new file mode 100644 index 00000000..d6cc72e4 --- /dev/null +++ b/website/src/_includes/overrides/blog-index.njk @@ -0,0 +1,43 @@ +--- +layout: layouts/base.njk +title: Blog +permalink: /blog/ +--- +{% set langPrefix = "/" + lang if lang and lang != defaultLanguage else "" %} +{% set blogText = i18n[lang or defaultLanguage].blog %} +
+
+

{{ blogText.eyebrow | default("SharpLsp journal") }}

+

{{ blogText.title | default("Blog") }}

+

{{ blogText.subtitle | default(site.description) }}

+
+ + + + {% set postCollection = collections.posts %} + {% if lang and lang != defaultLanguage %} + {% set postCollection = collections[lang + "posts"] | default(collections.posts) %} + {% endif %} + + {% if postCollection | length > 0 %} +
+ {% for post in postCollection %} + {% set currentPost = post %} + {% set featuredPost = loop.first %} + {% include "partials/post-card.njk" %} + {% endfor %} +
+ {% else %} +

{{ blogText.empty | default("No blog posts yet. Check back soon!") }}

+ {% endif %} +
diff --git a/website/src/_includes/partials/home.njk b/website/src/_includes/partials/home.njk new file mode 100644 index 00000000..b030d8a2 --- /dev/null +++ b/website/src/_includes/partials/home.njk @@ -0,0 +1,97 @@ +{% set homeText = i18n[lang or defaultLanguage].home %} +{% set langPrefix = "/" + lang if lang and lang != defaultLanguage else "" %} +{% set docsUrl = langPrefix + "/docs/" %} + +
+
+

{{ homeText.eyebrow }}

+

{{ homeText.title }} {{ homeText.titleAccent }}

+

{{ homeText.lede }}

+

{{ homeText.engines }}

+ +
    + {% for item in homeText.trust %}
  • {{ item }}
  • {% endfor %} +
+
+ +
+ {{ homeText.screenshotAlt }} +
{{ homeText.screenshotCaption }}
+
+
+ +
+ {% for item in homeText.proof %} +

{{ item.title }}{{ item.detail }}

+ {% endfor %} +
+ +
+
+

{{ homeText.workflowEyebrow }}

+

{{ homeText.workflowTitle }}

+

{{ homeText.workflowIntro }}

+
+
    + {% for workflow in homeText.workflows %} +
  1. + 0{{ loop.index }} +

    {{ workflow.title }}

    +

    {{ workflow.description }}

    +
      {% for feature in workflow.features %}
    • {{ feature }}
    • {% endfor %}
    +
  2. + {% endfor %} +
+
+ +
+
+

{{ homeText.languageEyebrow }}

+

{{ homeText.languageTitle }}

+

{{ homeText.languageIntro }}

+
+
+ {% for language in homeText.languages %} +
+ {{ language.name }} +

{{ language.engine }}

{{ language.description }}

+
+ {% endfor %} +
+
+ +
+
+

{{ homeText.alternativeEyebrow }}

+

{{ homeText.alternativeTitle }}

+

{{ homeText.alternativeIntro }}

+
+
+ {% for reason in homeText.reasons %} +
0{{ loop.index }}

{{ reason.title }}

{{ reason.description }}

+ {% endfor %} +
+
+ +{% include "partials/releases-section.njk" %} + +
+
+

{{ homeText.ctaEyebrow }}

+

{{ homeText.ctaTitle }}

+

{{ homeText.ctaCopy }}

+
+ +
diff --git a/website/src/_includes/partials/nav.njk b/website/src/_includes/partials/nav.njk index 0d87e17b..9739c864 100644 --- a/website/src/_includes/partials/nav.njk +++ b/website/src/_includes/partials/nav.njk @@ -1,55 +1,54 @@ -