Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

64 changes: 44 additions & 20 deletions docs/plans/RAZOR-PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ The phases below are in dependency order. Each item is checked only when a test

- [x] Fixture solution `src/sharplsp/tests/fixtures/razor/` with `BlazorApp`, `Widgets` (RCL), `PagesApp` (Pages + MVC) and `Domain` (F#), isolated by its own `Directory.Build.props` ([RAZOR-TESTING])
- [x] Host: add `LangId::Razor` for `.razor` and `.cshtml`, owned by the C# sidecar ([RAZOR-ARCHITECTURE-ROUTING])
- [x] Host: route `documentSymbol`, `foldingRange` and `linkedEditingRange` for Razor to the sidecar, and answer `selectionRange` empty
- [x] Host: route `documentSymbol`, `foldingRange` and `linkedEditingRange` for Razor to the sidecar, and answer `selectionRange` empty (the Deferred selection-range item has since routed it to the sidecar too)
- [x] Host: add `<` and `@` completion triggers, and short-circuit them for C# and F# documents
- [x] Host: publish Razor diagnostics under the source `sharplsp-razor`
- [x] Sidecar: bundle the Razor compiler from the build SDK, and list it in `THIRD-PARTY-NOTICES.md` ([RAZOR-ARCHITECTURE-ENGINE])
Expand Down Expand Up @@ -76,7 +76,7 @@ The phases below are in dependency order. Each item is checked only when a test
- Renaming `Badge.Text` from its code-behind edits `@Text` and Board's `Text=`.
- Renaming `Tally.Label` turns `@bind-Label` into `@bind-Title`.
- prepareRename refuses on a component's start and end tags.
- The F# half of the rename is Phase 6
- The F# half of the rename is Phase 5

### Phase 3: Blazor Markup

Expand Down Expand Up @@ -110,28 +110,52 @@ The phases below are in dependency order. Each item is checked only when a test

### Phase 4: Razor Pages and MVC

- [ ] `@model`, `@inject`, `@section` and `@functions` through the projection ([RAZOR-MVC])
- [ ] Tag helper elements and attributes: hover and completion
- [ ] `_ViewImports.cshtml` imports resolved by the engine

### Phase 5: Editors
- [x] `@model`, `@inject`, `@section` and `@functions` through the projection ([RAZOR-MVC-DIRECTIVES])
- `RazorMvcTests.Model_inject_and_functions_answer_through_the_projection`: `@model EditModel` and `Model.Title` hover their docs, `@inject` hovers `ILogger<…>`, and `@Shout` goes to its `@functions` declaration.
- `Model_expressions_and_section_code_get_csharp_features`: `asp-for="Title"` hovers and goes to `EditModel.Title`, finds its 4 references in `Edit.cshtml`, and renames them all with no generated-file edit; `@Model.Price` inside `@section` hovers. `Section_blocks_fold_and_outline` folds and outlines `@section`.
- Full stack: `test_full_stack_razor_pages_and_mvc_views` (`razor_mvc.rs`).
- [x] Tag helper elements and attributes: hover and completion ([RAZOR-MVC-TAGHELPERS], [RAZOR-MVC-SCOPE])
- Hover: `Tag_helper_elements_and_attributes_answer_hover_from_their_descriptors` (`<input>` answers `InputTagHelper`, `asp-for` its `ModelExpression`).
- Completion: `Tag_helper_attributes_complete_in_an_element_start_tag` (`asp-page`, `asp-controller`, `asp-route-`) and `Tag_helper_elements_complete_after_an_open_angle_bracket` (`partial`, `environment`, `a` once, the `th:` prefix, `@removeTagHelper`). That test found components completing after `<` in `.cshtml` files, where no component binds by tag; they no longer do.
- Scope: `Tag_helper_directives_scope_completion_and_binding`. VS Code: `a Razor Page completes tag helper elements once, before the HTML service` (an element that both HTML and a tag helper name is listed once).
- [x] `_ViewImports.cshtml` imports resolved by the engine ([RAZOR-MVC-IMPORTS]) — `ViewImports_imports_reach_the_views_below_them`: `@model Item` resolves through `Views/_ViewImports.cshtml`'s `@using`, the view reports no error, and its `@addTagHelper` binds `<a>`

### Phase 5: F#

- [x] Hover, completion and definition from Razor into F# types of a referenced F# project ([RAZOR-FSHARP-METADATA], [RAZOR-FSHARP-DEFINITION])
- Hover shows the F# XML docs: the design-time build references Domain's `refint` assembly, which has no XML, so the sidecar swaps in the built DLL with its documentation. `RazorFSharpTests.Hover_and_signature_help_read_FSharp_symbols_from_their_assembly` and `Completion_offers_FSharp_members_in_markup_and_code`.
- Definition lands in `Scores.fs`, not decompiled source: the C# sidecar names each F# symbol by assembly and XML doc ID, and the F# sidecar finds its declaration (`workspace/declarationForeign`, [DEFINITION-CROSSLANG-IDENTITY]). `Definition_names_each_FSharp_symbol_for_the_FSharp_sidecar`; F#: `FSharpForeignSymbolsTests`. The C#→F# definition e2e now lands in `Shapes.fs` too.
- References merge both sides by the same identity (`workspace/referencesForeign`, [REFERENCES-CROSS-LANGUAGE]): `Foreign_references_and_rename_reach_Razor_uses_of_an_FSharp_symbol`.
- F# references and highlights placed a dotted use (`player.Score`) over its whole path. They now sit on the name, the same span the foreign path reports (`F# references and foreign uses place each use on its name alike`).
- [x] F#-origin rename that includes Razor usages, mapped back from the projections ([RAZOR-FSHARP-RENAME])
- From F#: `Foreign_references_and_rename_reach_Razor_uses_of_an_FSharp_symbol` renames `Leaders.razor`'s `@player.Score`.
- From Razor or C#: the rename resolves to the F# symbol and renames it everywhere (`Rename_from_Razor_renames_an_FSharp_symbol_in_CSharp_and_Razor`).
- Full stack: `test_full_stack_fsharp_in_razor_session` (hover, definition into `Scores.fs`, references with and without the declaration, rename from Razor and from F#). VS Code: `F# symbols in Razor navigate to, find and rename in their F# source`.
- That full-stack test only restored the fixture, so C# had no `Domain.dll` to read and every F# symbol stayed unresolved ([DEFINITION-CROSSLANG]: the F# project must be built). Its workspace now builds the F# library (`create_razor_workspace_with_fsharp_built`).

### Phase 6: Editors

- [x] VS Code: sync `razor` and `aspnetcorerazor` documents, and activate on `onLanguage:razor` ([RAZOR-EDITORS]) — `razor-lsp.test.ts`: a Razor file opens as Razor, and its unsaved edits reach the server
- [x] VS Code: forward HTML completion and hover over `sharplsp/razor/htmlProjection` ([RAZOR-FEATURES-HTML]) — `razor-lsp.test.ts`: element names and HTML hovers in markup, C# completion and hover in code
- [x] A C# position offers C# items alone: no word suggestions from the HTML projection, including after a typed dot and inside an unfinished `@(` (#405, review R9, Codex1, fb1fff3). The client sends the caret, and the projection answers null where the compiler-backed completion target is C#, while tag and attribute names stay markup. The host, sidecar and real-editor regressions failed for the bug, then passed unchanged
- [x] VS Code: add a `razor` end-to-end chunk on both platforms — `test-chunks.json`, in both matrices
- [x] Zed: Razor language registration (Zed has no built-in Razor grammar) — `the_server_attaches_to_the_csharp_fsharp_and_razor_languages`

### Phase 6: F#

- [ ] Hover, completion and definition from Razor into F# types of a referenced F# project ([RAZOR-FSHARP])
- [ ] F#-origin rename that includes Razor usages, mapped back from the projections

### Deferred ([RAZOR-FEATURES-DEFERRED])

- [ ] Razor formatting
- [ ] Selection ranges
- [ ] Code actions: extract a block to code-behind, extract a component, generate an event handler, add `@using`
- [ ] Inlay hints, code lens, and call and type hierarchy from Razor positions
- [ ] `.razor.css` and `.razor.js` collocation awareness
- [ ] Blazor WebAssembly debugging and Razor Hot Reload
### Deferred

- [x] Razor formatting — resolved as out of scope: SharpLsp formats no language ([SHARPLSP-FEATURES-FORMATTING]), so the spec now says so for Razor ([RAZOR-SCOPE-FORMATTING]) and nothing is advertised
- [x] Selection ranges ([RAZOR-FEATURES-SELECTION]) — `RazorSelectionRangesTests` (6 cases: C# chains through syntax nodes, tag names through enclosing elements, attribute names and values, implicit expressions, legacy files, unlisted files); `test_full_stack_razor_selection_ranges`; the routing session's `selectionRange` now asserts the exact chain. An attribute's whole span comes from the compiler's spans (a new `MarkupAttribute.End` fact), not a scan of the text
- [x] Code actions: extract a block to code-behind, extract a component, generate an event handler, add `@using` ([RAZOR-CODE-ACTIONS]) — `RazorCodeActionsTests` (5 cases). A new `@using` goes after a Razor Page's `@page` line, never above it (`A_new_import_follows_the_page_directive_a_Razor_Page_opens_with`). New files arrive as LSP `CreateFile` operations (`map_workspace_edit_creates_new_files_before_editing_them`)
- [x] Inlay hints, code lens, and call and type hierarchy from Razor positions ([RAZOR-FEATURES-INLAY-HINTS], [RAZOR-FEATURES-CODE-LENS], [RAZOR-FEATURES-HIERARCHY]) — `RazorHintsAndLensesTests` (2), `RazorHierarchyTests` (3), `test_full_stack_razor_hierarchy_and_hints`
- Outgoing calls from a Razor projection threw: the resolver read the declaration through `Solution.GetDocument`, which is null for a source-generated document. It now reads each declaration through its own tree
- Markup callers are the component, named by its file with kind `Class`, never the generated `BuildRenderTree`
- The C# sidecar named hierarchy kinds in lowercase, while the host maps PascalCase (the F# sidecar's casing). So every C# class, property or interface item reached the client as a method or class. The C# kinds are now PascalCase, and the e2e asserts the LSP kinds
- [x] `.razor.css` and `.razor.js` collocation awareness ([RAZOR-COLLOCATION]) — `RazorCollocationTests` (6), `RazorStylesheetTests` (10), `HtmlMarkupContextTests` (attribute at a caret), `test_full_stack_razor_collocated_styles_and_scripts`, VS Code `a component completes and defines its stylesheet classes, and nests its collocated files`
- Classes come from a CSS Syntax Level 3 tokenizer over rule preludes; a class completes as LSP `Value` and defines to every selector that declares it. A module literal in an `"import"` call defines to its `.razor.js`, from Razor, a code-behind and plain C#
- The attribute at a caret is read by the HTML tokenizer, continued past the caret. Review found three bugs in a text-scanning first version: an attribute after a bare one (`<span hidden class="…">`) was read as the bare one, a caret on a name had no value, and a caret after a value still named its attribute. Each failed in `HtmlMarkupContextTests` first
- The host mapped the `Value` kind to `Text`; it now maps to LSP `Value`
- [x] Blazor WebAssembly debugging and Razor Hot Reload ([DEBUG-FEATURES-LAUNCH-BLAZOR-WASM], [RAZOR-HOT-RELOAD])
- WebAssembly: the `sharplsp-blazorwasm` debugger runs `dotnet run` and hands the DevServer's debug proxy to js-debug — `blazor-wasm-debug.test.ts`, `blazor-wasm-debug-resolution.test.ts`
- Hot Reload: a Razor save swaps the additional document in every framework copy, the generator reruns and Edit and Continue emits the generated C#'s deltas — `RazorHotReloadTests` (5: markup and `@code` body edits, a mixed `.cs` and Razor batch, a signature change that demands a restart, an unlisted file named in its refusal). Each failed with the Razor branch disabled
- A re-save of committed text answered `notCompilable`, sending the client to save again forever. Roslyn's Edit and Continue log showed the emit was Status None, nothing to apply, with no update pending. It now answers `applied` with no deltas, for `.cs` saves too (`Two_phase_updates_commit_discard_and_span_documents`)
- VS Code: both save paths take Razor documents — `debug-hot-reload-e2e.test.ts` (a Razor save reaches the sidecar and is refused by name), `fsi-build-output-e2e.test.ts`
43 changes: 42 additions & 1 deletion docs/specs/DEBUGGING-SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ SharpLsp targets **DAP specification version 1.71.0**.
| Launch with environment variables | `launch` (env) | P1 | |
| Launch with custom working directory | `launch` (cwd) | P1 | |
| Restart the same configuration | `restart` | P1 | Emulated by respawning netcoredbg. The outgoing adapter is retired with `disconnect` (`restart: true`) before any signal, so a launched debuggee is terminated and an attached one only detached; a restart never leaves the previous run behind |
| Launch browser for Blazor WASM | `launch` (browser) | P3 | Requires browser devtools bridge |
| Launch browser for Blazor WASM | `launch` (browser) | P2 | Own debug type `sharplsp-blazorwasm`; hands the app to VS Code's built-in js-debug — [DEBUG-FEATURES-LAUNCH-BLAZOR-WASM] |
| Hot Reload enabled launch | `launch` (hotReload: true) | P2 | See [DEBUG-FEATURES-HOT-RELOAD] |
| Child process auto-attach | `launch` event | P2 | Phase 5: `ICorDebugManagedCallback::CreateProcess` |

Expand Down Expand Up @@ -366,6 +366,47 @@ The profile file belongs to the **resolved project**, not the workspace root. A
5. A candidate path that exists but is not a launch-settings document MUST NOT abort the scan; the resolver continues to the next candidate.
6. Only an interactive launch may prompt. `resolveDebugConfiguration` — the user pressing F5 — MUST be free to ask; `provideDebugConfigurations`, which VS Code may call unprompted to populate a list, MUST NOT prompt and instead emits one configuration per eligible profile, each carrying its own `args` and `env`.

### Blazor WebAssembly launch `[DEBUG-FEATURES-LAUNCH-BLAZOR-WASM]`

A Blazor WebAssembly app is not an assembly netcoredbg can attach to: it runs in the
browser. SharpLsp therefore contributes a second debug type, `sharplsp-blazorwasm`, which
starts the app with `dotnet run` and hands it to VS Code's **built-in JavaScript debugger**
(js-debug, debug types `pwa-chrome` / `pwa-msedge`) through the DevServer's .NET debug proxy.
The pieces it relies on are public: `Microsoft.AspNetCore.Components.WebAssembly.DevServer`
(MIT, referenced by the `blazorwasm` template) and `app.UseWebAssemblyDebugging()`, both of
which serve `/_framework/debug/ws-proxy`.

**Launch configuration**

| Property | Type | Meaning |
|---|---|---|
| `project` | string | The `.csproj`/`.fsproj` to run. Omitted: the project is resolved per [DEBUG-FEATURES-LAUNCH-TARGET] |
| `launchProfile` | string | A `Properties/launchSettings.json` profile, by name |
| `browser` | `"chrome"` \| `"edge"` | Which js-debug type drives the session. Default `chrome` |
| `url` | string | The page to open. Omitted: the address the DevServer announces |
| `webRoot` | string | Where served URLs map onto source. Omitted: `<project>/wwwroot` |
| `timeout` | number | How long js-debug waits for the page. Default 30000 |

`required` MUST be empty: every field has a resolution rule.

**Rules**

1. The type MUST be a single value across `contributes.debuggers`, the constants module and this specification, and it MUST declare **no `languages`** — the F5 auto-pick for `csharp` and `fsharp` belongs to `sharplsp-coreclr` alone ([DEBUG-FEATURES-LAUNCH-DYNAMIC] rule 4). The type is reached from `launch.json`, from `configurationSnippets` and from the dynamic configuration list.
2. Both activation events — `onDebugResolve:sharplsp-blazorwasm` and `onDebugDynamicConfigurations:sharplsp-blazorwasm` — and both configuration-provider trigger kinds MUST be declared, and `initialConfigurations` and `configurationSnippets` MUST be present ([DEBUG-FEATURES-LAUNCH-DYNAMIC] rules 1–5).
3. Every attribute the resolver writes MUST be declared in `configurationAttributes`, and no attribute VS Code core injects MAY be re-declared ([DEBUG-FEATURES-LAUNCH-OUTPUT] rules 2 and 5).
4. `provideDebugConfigurations` MUST offer one configuration per Blazor WebAssembly project reachable from the folder, silently — the project's `Sdk` attribute read with an XML parser, never by matching text, because a commented-out `Sdk`, an `<Sdk>` item and a property whose value names the SDK are different facts that read the same.
5. The project MUST be the configuration's stated `project`, or the one [DEBUG-FEATURES-LAUNCH-TARGET] resolves; a stated project that does not exist MUST be refused by name.
6. The launch profile MUST be the one named, or the project's **sole** `Project` profile ([DEBUG-FEATURES-LAUNCH-PROFILES]); a name the project does not declare MUST be refused, naming the project, never silently ignored.
7. `launchSettings.json` parsing MUST carry the profile's `inspectUri` alongside its arguments, environment and URLs, from the one reader that already owns that document.
8. The app MUST be started as `dotnet run --project <project> [--launch-profile <profile>]` — an argv array handed to a process, never a shell command line.
9. The launch MUST wait for the `Now listening on: <url>` line the ASP.NET Core host logs and MUST take the FIRST http(s) address it announces. Only a line carrying that prefix announces an address; text that merely contains a URL does not. Output arriving in arbitrary chunks MUST be reassembled per line, and the whole launch MUST be bounded by a startup timeout whose message names the project.
10. `inspectUri` MUST be expanded from the listening address — `{wsProtocol}` to `ws` for http and `wss` for https, `{url.hostname}`, `{url.port}` — with a profile that states none defaulting to the `blazorwasm` template's pattern. `{browserInspectUri}` MUST be passed through untouched: js-debug owns it, and without an `inspectUri` no .NET breakpoint in a `.razor` file or a C# class can ever bind.
11. The browser session MUST be started with `vscode.debug.startDebugging`, as a `pwa-chrome`/`pwa-msedge` `launch` configuration carrying `url`, `inspectUri`, `webRoot` and `timeout`, and the provider MUST then answer `undefined` so VS Code starts no session for `sharplsp-blazorwasm` itself.
12. When the browser session ends the `dotnet run` process MUST be terminated, and only the tree that process started — the DevServer runs as its child, so signalling the parent alone leaves the port bound by a process nothing owns. A launch whose process died before the handoff completed MUST be refused rather than registered.
13. A second launch of the SAME project MUST stop the launch already running, so two DevServers never compete for one port, and an exit MUST un-register only the process that owns the entry.
14. Every `dotnet run` line MUST go to the SharpLsp channel logger, with structured context and no file contents or PII, through the same `dotnet` supervisor process control, locale pinning and whole-tree stop every other `dotnet` child uses.
15. The suite MUST prove it against a real app: a real `dotnet run`, the address the real DevServer announced, a real `GET <url>/_framework/debug` answering 200, and the process gone once the browser session ends. Only the browser session itself may be replaced, and only through the shipped seam.

### Debuggee output routing `[DEBUG-FEATURES-LAUNCH-OUTPUT]`

| `console` value | Destination |
Expand Down
Loading
Loading