Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0169iPWwKHZoBTNN9qwXiwqk
261 lines
14 KiB
Markdown
261 lines
14 KiB
Markdown
# .NET Solution Launcher
|
|
|
|
Startup project and solution configuration in the status bar, with build, debug, run and
|
|
hot reload that actually use them. Built for [DotRush](https://github.com/JaneySprings/DotRush).
|
|
|
|
```
|
|
$(project) MyGame.Editor $(settings-gear) Test | x64 $(debug-alt) $(flame) $(database) 4/4
|
|
```
|
|
|
|
- The **project item** shows which `.csproj` is the startup project, and picks another one
|
|
from the executables in the solution (Runtime is a library, so it is not offered).
|
|
- The **configuration item** shows the *solution* configuration — `Debug | x64`,
|
|
`Test | x64`, `Release | Any CPU` — exactly as `MyGame.sln` lists them.
|
|
- The **debug button** builds with that configuration and launches the startup project.
|
|
- The **flame** starts it under `dotnet watch` with hot reload instead.
|
|
- The **database counter** says how many projects of the solution DotRush's language server has actually loaded.
|
|
- The **database counter** says how many projects of the solution DotRush's language server has actually loaded.
|
|
|
|
## Why, when DotRush already has a status bar item
|
|
|
|
DotRush shows `Debug | net10.0`. Two things are missing from that:
|
|
|
|
1. **The project name.** It is in the tooltip only. With three executables in the
|
|
solution (Builder, Editor, and whatever comes next) that is the thing you need to see.
|
|
2. **The platform.** DotRush's build task runs `dotnet build <project> -p:Configuration=X`
|
|
with no `-p:Platform`. MSBuild then defaults to `AnyCPU`, and every property group
|
|
conditioned on `'$(Configuration)|$(Platform)' == 'Debug|x64'` is skipped. In this
|
|
solution that is not cosmetic:
|
|
|
|
| `MyGame.Runtime`, Configuration=Debug | `DefineConstants` |
|
|
| --- | --- |
|
|
| without `-p:Platform` (what DotRush runs) | `TRACE;DEBUG` |
|
|
| with `-p:Platform=x64` | `TRACE;LOG_INFO;PROFILING;DEBUG` |
|
|
|
|
So logging and profiling silently vanish, `Optimize` is never set for Test/Release,
|
|
and `MyGame.Builder` loses `AllowUnsafeBlocks` and fails to compile.
|
|
|
|
DotRush also cannot be *told* a configuration from outside — its selection lives in its
|
|
own workspace state — so this extension owns the build rather than wrapping
|
|
`dotrush: Build`. The startup *project* is synced with DotRush in both directions
|
|
(`dotnetSolution.syncDotRush`), so its test explorer, `launchSettings.json` lookup and
|
|
`${command:dotrush.activeProjectPath}` all agree with what the status bar says.
|
|
|
|
## What a build does
|
|
|
|
`dotnetSolution.buildScope` decides (default `solution`):
|
|
|
|
| Scope | Command | Notes |
|
|
| --- | --- | --- |
|
|
| `solution` | `dotnet build MyGame.sln -c Test -p:Platform=x64` | Like Visual Studio's *Build Solution*. MSBuild maps every project through the `.sln`: MyGame.* build as `Test\|x64`, MoonWorks as `Debug\|Any CPU`, and the Editor → Builder `ProjectDependencies` entry is honoured, so the Builder exists before the Editor's post-build step needs it. |
|
|
| `project` | `dotnet build MyGame.Editor.csproj -c Test -p:Platform=x64` | Only the startup project and its `ProjectReference`s. Faster, but solution-only dependencies are ignored. |
|
|
|
|
Rebuild adds `--no-incremental`; Clean runs `dotnet clean`. Two more targets use the same
|
|
selection:
|
|
|
|
| Target | Command | Notes |
|
|
| --- | --- | --- |
|
|
| `publish` | `dotnet publish MyGame.Editor.csproj -c Release -p:Platform=x64 [-r win-x64]` | Always the startup project. `dotnetSolution.publish.runtime` sets `-r`, which `PublishAot` needs; `publish.args` adds the rest |
|
|
| `test` | `dotnet test MyGame.sln -c Test -p:Platform=x64` | Follows `buildScope`; `dotnetSolution.test.args` adds filters or `--no-build` |
|
|
|
|
All five are tasks of type `dotnet-solution`, so they show up under *Run Task* and can be
|
|
used as `preLaunchTask`. When one fails the Problems panel opens
|
|
(`dotnetSolution.showProblemsOnFailure`).
|
|
|
|
## What a launch does
|
|
|
|
`program` comes from MSBuild, not from guessing: the extension evaluates the startup
|
|
project with the mapped configuration and platform (`dotnet msbuild -getProperty:TargetPath …`),
|
|
which is the only way to get it right here — `OutDir` comes from `Directory.Build.props`
|
|
and `AppendTargetFrameworkToOutputPath` is off. The result is cached until a project or
|
|
solution file changes.
|
|
|
|
The apphost `.exe` is launched when the project produces one, since vsdbg wants an
|
|
executable; otherwise it runs `dotnet <TargetPath>`. DotRush's debug configuration provider
|
|
still runs after this one and fills in `justMyCode` and symbol options.
|
|
|
|
### Arguments, environment, working directory
|
|
|
|
Builder and Editor want different arguments, so these are layered, most specific wins:
|
|
|
|
1. `dotnetSolution.launch.projects`, keyed by project name:
|
|
```jsonc
|
|
"dotnetSolution.launch.projects": {
|
|
"MyGame.Builder": { "args": ["-build", "-resourcePath", "Resources"], "cwd": "../MyGame.Editor" },
|
|
"MyGame.Editor": { "console": "integratedTerminal" }
|
|
}
|
|
```
|
|
`cwd` is relative to the project folder. `profile` names a launchSettings.json profile.
|
|
2. The global `dotnetSolution.launch.args` / `.env` / `.cwd` / `.console`.
|
|
3. `Properties/launchSettings.json` next to the project: `commandLineArgs`,
|
|
`environmentVariables` and `workingDirectory` of the first `"commandName": "Project"`
|
|
profile, or the one named by `dotnetSolution.launch.profile`.
|
|
4. Otherwise no arguments and `TargetDir` as the working directory.
|
|
|
|
The same layering applies to F5 through the launch.json entry: its `args` stay empty in
|
|
the file and are filled in at launch time.
|
|
|
|
## Hot reload
|
|
|
|
The third way to start the startup project, next to Debug and Run, and a rewrite of the
|
|
earlier `dotnet-hot-reload` extension now that the selection exists to build on:
|
|
|
|
```
|
|
dotnet watch run --project MyGame.Editor.csproj -c Debug --property:Platform=x64 -- <args>
|
|
```
|
|
|
|
Same project, configuration, platform, arguments, environment and working directory as
|
|
F5. It runs in an integrated terminal named after the project, and the status bar item
|
|
next to the debug button follows the watcher's output: *watching*, *applied*, *failed*,
|
|
*restart needed*. Save a file and the change is applied; the flame in the terminal's
|
|
title is the same session.
|
|
|
|
| Action | How |
|
|
| --- | --- |
|
|
| Start | The `$(flame)` status bar item (icon only; it gains a word — *failed*, *restart needed* — only when something needs you), *.NET Solution: Run Startup Project with Hot Reload*, or `Ctrl+Alt+F5` |
|
|
| Apply, restart, attach, stop | Click the status bar item while it runs — one menu for all of them |
|
|
| Restart the application | Also `Ctrl+R` inside the watch terminal |
|
|
|
|
**The debugger is not attached by default.** That is the biggest change from the old
|
|
extension, and deliberate. `dotnet watch` replaces the application process on every
|
|
rude edit, restart or crash, and each of those ends an attached debug session; keeping a
|
|
debugger attached across that meant guessing whether an ended session was a stop or a
|
|
swap, racing the outgoing process, and giving up on crash loops. Now *Attach Debugger*
|
|
finds the application below the watcher (by assembly name, so MSBuild is never picked)
|
|
and attaches when you ask. If the process is later replaced while the watcher still
|
|
runs, one notification says so and offers to re-attach. While attached, the flame and
|
|
restart buttons also appear in the debug toolbar.
|
|
|
|
**Rude edits** — changes hot reload cannot apply — follow `dotnetSolution.hotReload.rudeEdit`:
|
|
|
|
| Value | Behaviour |
|
|
| --- | --- |
|
|
| `restart` (default) | dotnet watch restarts the application on its own |
|
|
| `ask` | A notification offers *Restart* / *Keep running*; the answer goes to dotnet watch's console question |
|
|
| `warn` | Keeps the old code running and shows a warning with a *Restart* button |
|
|
|
|
**Optimised builds cannot hot reload.** SDK 10's `dotnet watch` refuses when `Optimize`
|
|
is true and restarts on every change instead. Test and Release set it here, so starting
|
|
hot reload under those asks whether to switch to Debug first.
|
|
|
|
**Solution build first.** `dotnet watch` only builds the project it runs. With
|
|
`dotnetSolution.hotReload.buildSolutionFirst` (default on) the normal solution build runs
|
|
before the watcher starts, so the Editor's post-build step finds the Builder even on a
|
|
clean checkout; the watcher's own build is then incremental.
|
|
|
|
## Language server status
|
|
|
|
DotRush shows a spinner while it loads the workspace and nothing afterwards, so when Find
|
|
References comes back empty there is no way to tell "no references" from "not indexed".
|
|
DotRush does raise one `projectLoaded` event per project through its exports; the
|
|
`$(database) 3/4` item counts those against the solution in the status bar and turns
|
|
warning-coloured in two cases:
|
|
|
|
- the **startup project was never loaded**, so IntelliSense and references miss it;
|
|
- projects were loaded from **outside the selected solution** (`+1`), which is what a
|
|
`dotrush.roslyn.projectOrSolutionFiles` pointing at another checkout looks like.
|
|
DotRush's own picker writes absolute paths there; a workspace-relative one such as
|
|
`["MyGame.sln"]` works too (the server resolves it against the workspace root) and
|
|
is the one to commit.
|
|
|
|
The tooltip lists loaded and missing projects. Clicking offers *Reload Workspace* (which
|
|
also resets the count), DotRush's solution picker, and its output channel. The
|
|
`dotnetSolution.languageServerStatus` command returns the same data for other
|
|
extensions; *colored-references* uses it to say *why* a search found nothing.
|
|
|
|
The count only includes events raised while this extension was listening, so a window
|
|
where DotRush finished before this extension activated shows 0 until a reload.
|
|
|
|
## Usage
|
|
|
|
| Action | How |
|
|
| --- | --- |
|
|
| Pick the startup project | Click the project name (`Ctrl+Alt+P`), or right-click a `.csproj` → *Set as Startup Project (.NET Solution)*. The explorer marks it with ▶ |
|
|
| Pick the configuration | Click `Debug \| x64` (`Ctrl+Alt+C`). Entries the solution does not build the project under are marked with `$(warning)` |
|
|
| Build / Rebuild / Clean | *.NET Solution: Build* etc., or the `dotnet-solution` tasks. `Ctrl+Shift+B` until a launch.json entry exists, after that VS Code's default build task |
|
|
| Publish / Test | *.NET Solution: Publish Startup Project*, *.NET Solution: Run Tests* |
|
|
| Debug | The `$(debug-alt)` button, *.NET Solution: Debug Startup Project*, or F5 |
|
|
| Hot reload | The `$(flame)` item, or `Ctrl+Alt+F5`. See above |
|
|
| Run without debugging | *.NET Solution: Run Startup Project (without debugging)*, or `Ctrl+F5` |
|
|
| Several `.sln` files | *.NET Solution: Select Solution*, or `dotnetSolution.solution` |
|
|
|
|
### F5
|
|
|
|
Without a launch.json entry, `F5`, `Ctrl+F5` and `Ctrl+Shift+B` are bound to this
|
|
extension's debug, run and build commands (only while a solution is loaded and no debug
|
|
session is running). Once the entry below exists those keys go back to VS Code's own
|
|
handling, which then uses the entry — same result, but editable in launch.json.
|
|
|
|
The first time it sees a solution the extension offers to write a `launch.json` entry.
|
|
Later, run *.NET Solution: Create launch.json and tasks.json entries* from the command
|
|
palette, or pick it at the bottom of the startup-project list (click the project name):
|
|
|
|
```jsonc
|
|
{
|
|
"name": ".NET Solution: Debug startup project",
|
|
"type": "coreclr",
|
|
"request": "launch",
|
|
"program": "${command:dotnetSolution.activeProgram}",
|
|
"cwd": "${command:dotnetSolution.activeCwd}",
|
|
"preLaunchTask": "dotnet-solution: Build"
|
|
}
|
|
```
|
|
|
|
The same entry is offered dynamically in the *Run and Debug* dropdown even without a
|
|
`launch.json`. It replaces the DotRush template's `${command:dotrush.activeTargetPath}` +
|
|
`preLaunchTask: "dotrush: Build"` pair, which is the one that builds without a platform.
|
|
|
|
### Variables for your own launch.json / tasks.json
|
|
|
|
| `${command:…}` | Value |
|
|
| --- | --- |
|
|
| `dotnetSolution.activeProgram` | apphost `.exe`, or the `.dll` when there is none |
|
|
| `dotnetSolution.activeTargetPath` | the built assembly |
|
|
| `dotnetSolution.activeTargetDir` | its directory |
|
|
| `dotnetSolution.activeCwd` | the working directory after the layering above |
|
|
| `dotnetSolution.activeProjectPath`, `activeProjectName` | the startup project |
|
|
| `dotnetSolution.activeSolutionPath` | the solution |
|
|
| `dotnetSolution.activeConfiguration`, `activePlatform` | solution configuration, e.g. `Test`, `Any CPU` |
|
|
| `dotnetSolution.activeProjectConfiguration`, `activeProjectPlatform` | what the project gets, e.g. `Test`, `AnyCPU` |
|
|
|
|
## Settings
|
|
|
|
| Setting | Default | |
|
|
| --- | --- | --- |
|
|
| `dotnetSolution.solution` | `""` | Solution to use; empty picks the shallowest, then the largest |
|
|
| `dotnetSolution.buildScope` | `solution` | See above |
|
|
| `dotnetSolution.targetFramework` | `""` | For `TargetFrameworks` projects; empty takes the first |
|
|
| `dotnetSolution.launch.args` / `.env` / `.cwd` / `.console` | | Passed to the launched application |
|
|
| `dotnetSolution.launch.projects` | `{}` | The same, per project name; wins over the global ones |
|
|
| `dotnetSolution.launch.profile` | `""` | launchSettings.json profile to read |
|
|
| `dotnetSolution.publish.runtime` / `.args` | `""` / `[]` | `-r` and extra arguments for publish |
|
|
| `dotnetSolution.test.args` | `[]` | Extra arguments for test |
|
|
| `dotnetSolution.showProblemsOnFailure` | `true` | Open Problems when a task fails |
|
|
| `dotnetSolution.hotReload.rudeEdit` | `restart` | `restart`, `ask` or `warn` |
|
|
| `dotnetSolution.hotReload.buildSolutionFirst` | `true` | Solution build before `dotnet watch` |
|
|
| `dotnetSolution.hotReload.watchArgs` | `[]` | Extra arguments for `dotnet watch` itself |
|
|
| `dotnetSolution.debugType` | `coreclr` | |
|
|
| `dotnetSolution.syncDotRush` | `true` | Push the startup project to DotRush and follow its changes |
|
|
| `dotnetSolution.syncDotRushWorkspaceProperties` | `false` | Write `Configuration=…;Platform=…` into `dotrush.roslyn.workspaceProperties` so IntelliSense sees the same `DefineConstants`. Off because DotRush reloads its workspace on every change |
|
|
| `dotnetSolution.additionalBuildArguments` | `[]` | Appended to every build |
|
|
|
|
`dotrush.roslyn.dotnetSdkDirectory` and `dotrush.msbuild.additionalEnvironment` are
|
|
honoured, so both extensions use the same SDK.
|
|
|
|
## Tips
|
|
|
|
- DotRush's own `Debug | net10.0` item can be hidden: right-click the status bar and
|
|
untick *DotRush*. Nothing else depends on it once this extension drives the build.
|
|
- The status bar goes to a spinner while a build runs; the build output is in the
|
|
terminal and the *Problems* panel (`$msCompile`).
|
|
- The *.NET Solution* output channel logs every `dotnet` invocation.
|
|
|
|
## Development
|
|
|
|
```
|
|
npm install
|
|
npm run compile
|
|
npm run test:unit # parser tests, against D:\Projects\MyGame when present
|
|
npm test # launches VS Code on that folder (SOLUTION_TEST_FOLDER overrides)
|
|
```
|