- Visual Studio 2022/2026 with the Visual Studio extension development workload, plus the VisualStudio.Extensibility component (VS.Extensibility is the primary API this extension uses; VSSDK is a fallback only for capabilities VS.Extensibility doesn't expose yet — see below)
- .NET SDK 10.0 or later (builds/publishes the LSP server bundled into the VSIX) and the .NET Framework 4.8.1 targeting pack (the extension itself is net481)
src/VisualStudio/
Reqnroll.IdeSupport.VisualStudio.Extension ← the VSIX: VS.Extensibility LSP client + commands
ExtensionEntrypoint.cs ← extension entry point, DI service registration
ReqnrollLanguageClient.cs ← LanguageServerProvider (the actual LSP client)
LspInterception/ ← LspServerConnectionService, LspInterceptingPipe,
per-message interceptors
LspNotifications/ ← VsProjectEventMonitor + preload-pipe pusher —
push DTE project state to the server
FindStepUsages/, GoToHooks/, StepCodeLens/,
RenameStep/, CommentToggle/, FindUnusedStepDefinitions/ ← per-feature VS-side client logic
Reqnroll.IdeSupport.VisualStudio.VSSDKIntegration ← MEF classifications, analytics transmitter,
VsIdeScope, VSSDK fallback pieces (CodeLens, etc.)
Reqnroll.IdeSupport.VisualStudio.Wizards(.Core/.UI) ← New Project / New Item wizards, welcome dialog
Reqnroll.IdeSupport.VisualStudio.ItemTemplates,
Reqnroll.IdeSupport.VisualStudio.ProjectTemplate ← VSIX template packaging
Start with docs/LSP-IDE-Support-Architecture.md §6.2 for the as-built mechanism (extension activation, eager server startup, the LspInterceptingPipe send/receive pipelines). docs/LSP-IDE-Support-Feature-Designs.md covers each feature's VS-specific surfacing.
dotnet build src/VisualStudio/Reqnroll.IdeSupport.VisualStudio.Extension/Reqnroll.IdeSupport.VisualStudio.Extension.csprojThis also republishes the LSP server self-contained (win-x64, net10.0) into the VSIX under
LSPServer/ (target IncludeLspServerInVsix). After any change to src/LSP/, rebuild this
project to pick it up before testing in VS — a stale bundled server is a common source of "my fix
doesn't seem to be running" confusion.
The extension deploys into VS's experimental instance (a separate hive, e.g.
…\AppData\Local\Microsoft\VisualStudio\<ver>_<id>Exp\Extensions\<id>\), not your everyday VS.
Launch it via Debug → Start New Instance (or F5) from the Extension project — this starts a
second devenv.exe with the extension loaded, isolated from your main VS install/extensions.
Runtime logs land in %LocalAppData%\Reqnroll\:
reqnroll-vs-ext-debug-<date>.log— the extension's own (client-side) log output.reqnroll-vs-server-debug-<date>.log— the LSP server's own log output (parses, discovery, handler activity). Appended across server process launches sharing a day, so multiple sessions' entries can interleave in one file — check PIDs (=== Reqnroll LSP Server started — …, PID N ===) when correlating.reqnroll-vs-inspector-<datetime>.log— client-side JSON-RPC trace fromLspInspectorLoggeron theLspInterceptingPipe, one line per message. This is the source of truth for what actually crossed the wire (legend negotiation, semanticTokens requests/responses, customreqnroll/*traffic) — lsp-inspector-tool compatible format.
When debugging coloring/binding/CodeLens behavior, the ext-debug and server-debug logs together usually tell the whole story; the inspector log is what to reach for when you suspect a protocol mismatch specifically.
LspServerConnectionService.ServerArguments is the full command line the extension passes to the
LSP server process, including the three verbosity flags described in
../LSP/CONTRIBUTING.md
(--log-level, --protocol-log-level, --trace). It's build-configuration-dependent:
- DEBUG (a developer F5-ing the Extension project):
--log-level Verbose --protocol-log-level Info --trace Verbose— the chattiest reasonable defaults across all three. - RELEASE (an installed VSIX, what real users run):
--log-level Warning --protocol-log-level Warning --trace Off— quiet by default.
Unlike VS Code's reqnroll.trace.server setting, VS currently has no end-user UI to raise these
levels at runtime. To get more verbose logs out of a RELEASE build for a bug report, either
change ServerArguments and rebuild, or use the DEBUG configuration directly.
dotnet test tests/VisualStudio/Reqnroll.VisualStudio.Tests/Reqnroll.VisualStudio.Tests.csprojReqnroll.VisualStudio.Tests (net481, xUnit + NSubstitute + AwesomeAssertions — note: Should()
is AwesomeAssertions, not FluentAssertions; signed with reqnroll.snk) is the home for VS-client
unit tests. It reaches internal types via InternalsVisibleTo (see the
AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo" entries in the
target csproj files).
Testing philosophy for this project — read before writing a new test here. The VS extension
is a thin LSP client: most of its services (FindStepUsagesService, CommentToggleService,
StepCodeLensService, etc.) mostly serialize parameters and send them over LspInterceptingPipe
to the server — the actual behavior lives server-side.
- Behavior (toggle/format/rename/find-usages logic) belongs in the LSP server's specs
(
tests/LSP/Reqnroll.IdeSupport.LSP.Server.Specs), not here. - Client parse/transform logic — mapping a raw JSON-RPC response into a view model, building a
request payload — belongs here, in
Reqnroll.VisualStudio.Tests. - VS-COM glue (Running Document Table /
IVsTextLinesbuffer writes,IVsFindAllReferencestable controls, DTE navigation, package autoload) generally isn't unit-testable — it needs a real VS host. Don't force a mock-heavy test around COM/ThreadHelper.JoinableTaskFactorycode just to get coverage; a documented manual/smoke-test note in the PR description is more honest than a test that mocks away the only thing worth verifying. - When client-side logic is worth testing, extract the pure part into a small
internal staticmethod (seeRenameStepService.ParseWorkspaceEdit,FindStepUsagesService.MapResult,WorkspaceEditApplier.ApplyEditsToTextfor the established pattern) so the transport (the pipe) is separable from the mapping you actually want to verify.
Do not port the legacy Reqnroll.VisualStudio.Specs feature files to test this extension —
those scenarios test behavior that now lives server-side and is already covered by the LSP specs.
- Gate every VS-specific workaround behind
ClientIdeContext.IsVisualStudio(or the equivalent flag), even in server-side code that happens to be triggered from here — a fix for a VS quirk should never silently change behavior for VS Code/Rider. - VS.Extensibility contribution classes are not documented as injectable into each other.
If one
[VisualStudioContribution]class needs data another one owns, register a small mutable "state holder" singleton inExtensionEntrypoint.InitializeServicesand inject that into both, rather than trying to inject one contribution into another's constructor — the latter fails silently (no exception inActivityLog.xml, the command just never dispatches). ReqnrollLanguageClient.OnServerInitializationResultAsyncmay run on a background thread. Any DTE/COM access there (or anywhere else that isn't guaranteed on the UI thread already) needs an explicitawait ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(...)first.LspServerConnectionServicestarts the server process eagerly, resolved fromExtensionEntrypoint.OnInitializedAsyncrather than fromReqnrollLanguageClient's constructor — the latter is only constructed when VS actually activates theLanguageServerProvider(i.e. on.feature-file open), which turned out not to be early enough. See the as-built note in the Architecture doc before changing this — the current design was arrived at after two rounds of the "obvious" approach turning out not to actually be eager, verified against real VS session logs both times.