Skip to content

Razor: Pages and MVC, F# in Razor, editing features, collocated files, WebAssembly debugging and Hot Reload - #422

Merged
MelbourneDeveloper merged 5 commits into
mainfrom
feature/397-razor-phase4
Oct 21, 2026
Merged

MelbourneDeveloper merged 5 commits into
mainfrom
feature/397-razor-phase4

Conversation

@MelbourneDeveloper

Copy link
Copy Markdown
Collaborator

TLDR

Completes Razor support (#397): Razor Pages and MVC views, F# symbols in Razor, the editing features (selection, code actions, hints, lenses, hierarchy), collocated .razor.css/.razor.js files, Blazor WebAssembly debugging and Razor Hot Reload.

Closes #397

Details

Razor Pages and MVC ([RAZOR-MVC])

  • Tag helper scope from @addTagHelper/@removeTagHelper/@tagHelperPrefix and _ViewImports.cshtml (RazorTagHelperScope.cs); tag helper elements and attributes hover, define and complete, each once beside the HTML service's items (razor-html.ts de-duplicates by label).
  • Attribute facts come from the compiler's IR, with each attribute's end (RazorIr.Attributes.cs, MarkupAttribute.End).

F# in Razor solutions ([RAZOR-FSHARP], [DEFINITION-CROSSLANG-IDENTITY], [REFERENCES-CROSS-LANGUAGE])

  • The C# sidecar names an F# project's symbol by assembly and XML doc signature (FSharpProjectSymbols.cs) and reads the built DLL with its XML docs, so hover shows F# /// comments.
  • New host module cross_language.rs: a decompiled F# location is replaced by its F# declaration (workspace/declarationForeign), and references merge both sides' uses (workspace/referencesForeign). An answer the F# sidecar cannot place yet is not cached.
  • F# side: FSharpForeignSymbols.fs; F# references now sit on the name of a dotted use (player.Score).

Editing features from Razor positions

  • Selection ranges ([RAZOR-FEATURES-SELECTION]): RazorSelectionRanges.cs, host razor::handle_selection_ranges (was []).
  • Code actions ([RAZOR-CODE-ACTIONS]): add @using, generate an event handler, extract a block to code-behind, extract elements to a new component (RazorQuickFixes.cs, RazorExtractActions.cs). New files arrive as LSP CreateFile operations (code_actions.rs).
  • Inlay hints, code lens, call and type hierarchy ([RAZOR-FEATURES-INLAY-HINTS], [RAZOR-FEATURES-CODE-LENS], [RAZOR-FEATURES-HIERARCHY]). Markup callers are the component, named by its file. The C# sidecar's hierarchy kinds are now PascalCase, as the host maps them (they reached clients as the wrong SymbolKind).

Collocated files ([RAZOR-COLLOCATION])

  • .razor.css classes complete as LSP Value items inside a class value and define to every selector (CssTokenizer.cs, a CSS Syntax Level 3 tokenizer, and RazorStylesheet.cs). The attribute at the caret is read by the HTML tokenizer (HtmlMarkupContext.Attributes.cs).
  • A module literal in InvokeAsync<IJSObjectReference>("import", "…") defines to its .razor.js, from Razor, a code-behind or plain C#.
  • VS Code configurationDefaults nest a component's .cs/.css/.js under it. Paths go through NativePaths.StylesheetOf/FromModulePath.

Blazor WebAssembly debugging ([DEBUG-FEATURES-LAUNCH-BLAZOR-WASM])

  • New sharplsp-blazorwasm debugger (blazor-wasm-debug.ts, blazor-wasm-launch.ts): runs dotnet run, expands the profile's inspectUri onto the announced address, hands the app to js-debug, and stops the app with the browser session. It declares no languages, so the F5 auto-pick stays sharplsp-coreclr.

Razor Hot Reload ([RAZOR-HOT-RELOAD])

  • A Razor save swaps the additional document in every framework copy, and the signature guard judges the generated projections (HotReloadSession.cs). The guard now ignores all trivia, because doc-comment edits were refused as rude edits.
  • A re-save of committed text answered notCompilable ("save again" forever). Roslyn reports Status None with nothing pending, so it now answers applied with no deltas, for .cs saves too.
  • VS Code's two save paths take the Razor language ids.

Specs and plan: RAZOR-SPEC gains the sections above and loses its "Not Yet Supported" list. DEFINITION/REFERENCES/RENAME/DEBUGGING specs cover the cross-language identity and the WebAssembly debugger. Every RAZOR-PLAN item is checked, with test evidence.

No new dependencies.

How Do The Automated Tests Prove It Works?

Host end-to-end (real sidecars, real fixture solution)

  • test_full_stack_razor_pages_and_mvc_views: @model/@inject hover, @functions definition, <input> and asp-for from tag helpers, partial/environment and asp-page (kind 10) completions, and Index.cshtml resolving @model through _ViewImports.cshtml with no errors.
  • test_full_stack_fsharp_in_razor_session: F# record hover with its docs, definition into Scores.fs, references with and without the declaration, and rename from Razor and from F#. Its workspace now builds the F# library, because C# reads only a built DLL.
  • test_full_stack_razor_selection_ranges, test_full_stack_razor_hierarchy_and_hints: exact nested chains, and hierarchy items and LSP kinds.
  • test_full_stack_razor_collocated_styles_and_scripts: five .title selectors, each ranged on the name; module definition at Card.razor.js 0:0 from @code and plain C#; title completes once as kind 12, replacing only ti.
  • test_cross_language_definition_csharp_to_fsharp now also asserts a C# local's references stay in C# once the F# sidecar is loaded.
  • Full host suite locally: 580/580.

Sidecar (C# 803/803, F# 457/457, Common 125/125)

  • RazorMvcTests, RazorFSharpTests, RazorSelectionRangesTests, RazorCodeActionsTests (including whitespace-trimmed, dedented extraction, and refusal when an outside local is captured), RazorHintsAndLensesTests, RazorHierarchyTests, CallHierarchyKindTests.
  • RazorCollocationTests, RazorStylesheetTests, and HtmlMarkupContextTests for the attribute at a caret. The last caught three bugs, each failing first: a bare attribute before class, a caret on a name, a caret after a value.
  • RazorHotReloadTests (5): markup and @code body edits emit real deltas; a mixed .cs+Razor batch applies as one; an @code signature change is restartRequired naming the method; an unlisted file is refused by name. Each fails with the Razor branch disabled. HotReloadSessionRegistryTests asserts the quiet .cs re-save leaves nothing to commit.
  • RazorSidecarTests drives textDocument/selectionRange and workspace/referencesForeign over the real socket. FSharpForeignSymbolsTests and FSharpForeignSymbolsWireTests cover declarations, uses and the wire handlers.

VS Code

  • razor-lsp.test.ts: F# navigation, references and rename from Razor; a Razor Page completes tag helpers once; stylesheet class definition and completion, and the nesting defaults VS Code applies.
  • blazor-wasm-debug.test.ts (a real dotnet run of the WebAssembly fixture) and blazor-wasm-debug-resolution.test.ts (profiles, inspectUri, run arguments, the app stopped with the session, a browser that never starts, an app that dies during the handoff).
  • debug-hot-reload-e2e.test.ts: a saved .razor reaches the sidecar and is refused by name, and the session survives. fsi-build-output-e2e.test.ts covers the dotnet watch path.
  • Locally: razor 9, debug 58, hot reload 16 passing.

Coverage of changed lines (local): C# 2005/2079 before the review's added tests, with the project at 96.5%; F# 100/101; Common 19/19; host 441/447; new WebAssembly files 887/911 (97.4%). What stays uncovered is the real js-debug handoff, the 180 s startup timer and some unreachable catch arms.

…e completion

Legacy tag helper elements and attributes become markup facts, with names read
where the compiler gives no span; @addTagHelper, @removeTagHelper and
@tagHelperPrefix scope MVC attribute completion. Builds; tests not yet run.
…, WebAssembly debugging and Hot Reload

Completes the Razor plan:
- Razor Pages and MVC views: tag helper hover, definition, completion and scope; @model, @Inject and @functions through the projection.
- F# in Razor solutions: hover with F# docs, definition into the F# source, and references and rename across F#, C# and Razor, found by assembly and XML doc identity.
- Editing features from Razor positions: selection ranges, code actions (add @using, generate a handler, extract to code-behind or a component), inlay hints, code lens, and call and type hierarchy. C# hierarchy kinds are PascalCase, as the host maps them.
- Collocated files: .razor.css classes complete as LSP values and define to their selectors, an "import" call defines to its .razor.js, and VS Code nests the files. The attribute at a caret is read by the HTML tokenizer.
- Blazor WebAssembly launches through the sharplsp-blazorwasm debugger and js-debug.
- Razor Hot Reload: a Razor save swaps the additional document in every framework copy. A re-save of committed text answers applied with nothing to apply, for .cs saves too.
- Review fixes: the host maps the Value completion kind, the F# Razor e2e builds its F# library, and fixture copies and hierarchy runners are shared.
@MelbourneDeveloper
MelbourneDeveloper enabled auto-merge (squash) October 21, 2026 05:13
…t new advisories

RUSTSEC-2026-0382 (rustls-webpki 0.103.15 -> 0.103.16), RUSTSEC-2026-0381
(webpki-roots 1.0.9 -> 1.0.10), and GHSA-vfgh-jcmw-g37q / GHSA-gfr2-rv44-r23c
(markdown-it 14.3.2 -> 14.3.3 under Eleventy) failed the dependency audit.
On Windows the rename's edit for Scores.fs and the document the test opened
spell the drive letter differently, so a lookup by Uri.path found no edits.
Key the edits by comparablePath(fsPath), merging spellings of one file, and
assert exactly two files are edited.
@MelbourneDeveloper
MelbourneDeveloper merged commit e26a92f into main Oct 21, 2026
59 checks passed
@MelbourneDeveloper
MelbourneDeveloper deleted the feature/397-razor-phase4 branch October 21, 2026 06:00
MelbourneDeveloper added a commit that referenced this pull request Oct 27, 2026
…ate all branches (#494)

Consolidates every open SharpLsp branch into one, per the one-feature-branch rule.

## What's in it
- **Test Explorer (#429):** test builds default to Debug and build only the selected project graph, incrementally. The tree refreshes after builds, and every test host a debug run owns is stopped. Reactive discovery contains failures: a target that cannot be listed becomes an error row with a logged warning, never an unhandled rejection.
- **Diagnostics:** solution-wide accuracy, freshness, cancellation and reactive refresh for C# and F#, with restore convergence, sidecar-restart recovery, severity and per-file limits and project filters. No result reports clean before the compiler is ready (#471, #472).
- **Analyzer diagnostics (#343):** IDE code-style rules and external analyzers such as StyleCop now report in the editor. Each diagnostic carries its rule's documentation link (`codeDescription`), in C# and in F# (FSharpLint).
- **Watched-file reloads** run on a background worker, so requests never wait behind a compiler load:
  - Each compiler reopens the workspace it last opened. A reload never reopens a superseded or settings-replaced selection.
  - A reload replays the buffers the compiler's previous graph owned, so owners adopted for open files survive, and documents an explicit selection excluded stay excluded.
  - Restore outputs a compiler's own open wrote are not reloaded again, and a reload never restores again, so restores cannot feed reload storms.
  - A completed reload or disk sync retires the identities that racing pulls minted.
  - Only the compilers whose loaded graph a change concerns reload; a project no compiler loaded reloads none.
- **Restore gate:** stale assets are restored before a workspace opens. Inputs include inherited props, central package versions, NuGet configuration and `global.json`, including a remembered input that has since been deleted. `dotnet` children resolve the SDK their workspace selects instead of inheriting the C# sidecar's MSBuild registration, and every restore stage is bounded at two minutes.
- **F#:**
  - Wildcard `<Compile>` items come from MSBuild evaluation (#455).
  - A superseded owner adoption never joins a replacement workspace.
  - Script diagnostics track current references, completion resolution goes back to FCS, and function symbols are classified.
  - A design-time build whose project is deleted while it runs is abandoned at once, instead of holding every F# request for three minutes.
- **C# owner adoption:** a project loaded for a file outside the workspace loads under the path its restore recorded. Loading it under another spelling (a Windows drive letter in the other case, or a link) rewrote its `obj` inputs, so the user's next build recompiled it.
- **Compiler recovery:** a respawned compiler is restored without a selected workspace that no longer exists (a deleted scratch solution), instead of failing and respawning forever, and with it again once it is back.
- **Debugger:** a `disconnect` the adapter exits before answering is answered by the router. On Windows netcoredbg exits as its debuggee ends, and VS Code held the dead session until its own timeout.
- **Push clients:** a framework switch publishes its diagnostics before it is announced, even when other pushes compete.
- **CI:**
  - The SDK diagnostic suites have their own chunk.
  - Fixtures build as users' projects, outside SharpLsp's lint policy.
  - Failing Rust shards keep their sidecar logs, and every VS Code shard keeps the server's and sidecars' logs.
- **Merged branches:**
  - `main`.
  - `chore/netfx-run-exit-flake`: the .NET Framework run proof, already on this branch.
  - `feature/397-razor-phase4`: squash-merged into main as #422, then recorded with `-s ours` because its remaining diff was superseded here.
  - `dependabot-upgrades`: all grouped bumps.
- **lspkit:** points at lspkit `main` (`53fd4c6`, Nimblesite/lspkit#30). Pull revisions separate supersession from engine generations, and the .NET profiler refuses non-.NET targets before connecting.

An adversarial review of this branch found eight defects (one critical, five high, one medium, one low). Each is fixed here behind a test that failed first.

## Test plan
- [ ] CI green on every leg
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Razor support: Blazor components, Razor Pages and MVC views

1 participant