Compare commits

...
2 Commits
Author SHA1 Message Date
maxandClaude Fable 5.1 0781418aaf README: relative paths work for dotrush.roslyn.projectOrSolutionFiles
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0169iPWwKHZoBTNN9qwXiwqk
2026-09-08 18:55:44 +02:00
maxandClaude Fable 5.1 9e64be0520 Language server status item: projects DotRush has loaded
Counts DotRush's projectLoaded events against the active solution and
warns when the startup project is missing or projects came from outside
the solution. Exposes the data as dotnetSolution.languageServerStatus
for other extensions.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0169iPWwKHZoBTNN9qwXiwqk
2026-09-08 18:53:19 +02:00
4 changed files with 206 additions and 2 deletions
+26 -1
View File
@@ -4,7 +4,7 @@ Startup project and solution configuration in the status bar, with build, debug,
hot reload that actually use them. Built for [DotRush](https://github.com/JaneySprings/DotRush).
```
$(project) MyGame.Editor $(settings-gear) Test | x64 $(debug-alt) $(flame)
$(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
@@ -13,6 +13,8 @@ $(project) MyGame.Editor $(settings-gear) Test | x64 $(debug-alt) $(flame)
`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
@@ -141,6 +143,29 @@ hot reload under those asks whether to switch to Debug first.
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 |
+20
View File
@@ -129,6 +129,22 @@
"title": "Hot Reload: Actions",
"category": ".NET Solution"
},
{
"command": "dotnetSolution.languageServerReload",
"title": "Reload Language Server Workspace (DotRush)",
"category": ".NET Solution",
"icon": "$(refresh)"
},
{
"command": "dotnetSolution.languageServerMenu",
"title": "Language Server Status",
"category": ".NET Solution"
},
{
"command": "dotnetSolution.languageServerStatus",
"title": "Language Server Status (as data)",
"category": ".NET Solution"
},
{
"command": "dotnetSolution.generateLaunchConfig",
"title": "Create launch.json and tasks.json entries",
@@ -190,6 +206,10 @@
{
"command": "dotnetSolution.hotReload.menu",
"when": "false"
},
{
"command": "dotnetSolution.languageServerStatus",
"when": "false"
}
],
"debug/toolBar": [
+153
View File
@@ -0,0 +1,153 @@
import * as vscode from 'vscode';
import * as path from 'path';
import { SolutionModel } from './model';
/** What DotRush sends per project in its `dotrush/projectLoaded` notification. */
interface LoadedProject {
name: string;
path: string;
frameworks?: string[];
isTestProject?: boolean;
}
interface DotRushExports {
onProjectLoaded?: { add(callback: (project: LoadedProject) => void): void };
}
/** Answer of the `dotnetSolution.languageServerStatus` command, for other extensions. */
export interface LanguageServerStatus {
/** Projects of the active solution the language server reported loaded. */
loaded: string[];
/** Projects of the active solution it has not reported. */
missing: string[];
/** Loaded projects that are not in the active solution at all. */
foreign: string[];
/** True when DotRush is not installed, so nothing can be known. */
unavailable: boolean;
}
function samePath(a: string, b: string): boolean {
return path.normalize(a).toLowerCase() === path.normalize(b).toLowerCase();
}
/**
* A status bar item saying how much of the solution DotRush's language server has
* actually loaded.
*
* DotRush shows a spinner while loading and nothing afterwards, so when Find References
* comes back empty there is no way to tell "no references" from "not indexed". It does
* raise one `projectLoaded` event per project through its exports; this collects them
* and compares against the solution in the status bar. Two mismatches get the warning
* colour: a startup project that was never loaded, and projects loaded from *outside*
* the selected solution — which is what a `dotrush.roslyn.projectOrSolutionFiles`
* pointing at another checkout looks like.
*/
export class LanguageServerIndicator implements vscode.Disposable {
private readonly item: vscode.StatusBarItem;
private readonly loaded = new Map<string, LoadedProject>();
private available = false;
private readonly subscriptions: vscode.Disposable[] = [];
constructor(private readonly model: SolutionModel, private readonly log: vscode.OutputChannel) {
this.item = vscode.window.createStatusBarItem('dotnetSolution.languageServer', vscode.StatusBarAlignment.Left, 100.2);
this.item.name = '.NET Solution: Language Server';
this.item.command = 'dotnetSolution.languageServerMenu';
this.subscriptions.push(this.item, model.onDidChange(() => this.render()));
const dotrush = vscode.extensions.getExtension<DotRushExports>('nromanov.dotrush');
if (dotrush) {
this.available = true;
void dotrush.activate().then(exports => {
exports?.onProjectLoaded?.add(project => {
if (!project?.path) {
return;
}
this.loaded.set(path.normalize(project.path).toLowerCase(), project);
this.log.appendLine(`DotRush loaded ${project.name} (${project.path})`);
this.render();
});
}, error => this.log.appendLine(`DotRush did not activate: ${error}`));
}
this.render();
}
status(): LanguageServerStatus {
const solution = this.model.activeSolution;
const projects = solution?.projects ?? [];
const loaded: string[] = [];
const missing: string[] = [];
for (const project of projects) {
(this.loaded.has(path.normalize(project.fsPath).toLowerCase()) ? loaded : missing).push(project.name);
}
const foreign = [...this.loaded.values()]
.filter(entry => !projects.some(project => samePath(project.fsPath, entry.path)))
.map(entry => entry.name);
return { loaded, missing, foreign, unavailable: !this.available };
}
/** Forgets what was loaded and asks DotRush to load again, so the count restarts from zero. */
async reload(): Promise<void> {
this.loaded.clear();
this.render();
await vscode.commands.executeCommand('dotrush.reloadWorkspace');
}
render(): void {
if (!this.available || !this.model.activeSolution) {
this.item.hide();
return;
}
const { loaded, missing, foreign } = this.status();
const total = loaded.length + missing.length;
const startup = this.model.startupProject?.info.name;
const startupMissing = startup !== undefined && missing.includes(startup);
const warn = startupMissing || foreign.length > 0;
const icon = total > 0 && missing.length === 0 ? 'database' : loaded.length === 0 ? 'loading~spin' : 'database';
this.item.text = `$(${icon}) ${loaded.length}/${total}${foreign.length ? ` +${foreign.length}` : ''}`;
this.item.backgroundColor = warn ? new vscode.ThemeColor('statusBarItem.warningBackground') : undefined;
const lines = [`**Language server (DotRush):** ${loaded.length} of ${total} projects of \`${this.model.activeSolution.name}\` loaded`];
if (loaded.length) {
lines.push('', 'Loaded: ' + loaded.map(name => `\`${name}\``).join(', '));
}
if (missing.length) {
lines.push('', 'Not loaded: ' + missing.map(name => `\`${name}\``).join(', '));
}
if (startupMissing) {
lines.push('', `$(warning) The startup project \`${startup}\` is not loaded — Find References and IntelliSense will miss it.`);
}
if (foreign.length) {
lines.push('', `$(warning) Loaded from outside this solution: ${foreign.map(name => `\`${name}\``).join(', ')}. ` +
'Check `dotrush.roslyn.projectOrSolutionFiles` — it may point at another checkout.');
}
if (loaded.length === 0 && missing.length > 0) {
lines.push('', 'Still loading, or the events fired before this extension was listening. Click → *Reload Workspace* to resync.');
}
lines.push('', 'Click for reload and solution picking.');
const tooltip = new vscode.MarkdownString(lines.join(' \n'));
tooltip.supportThemeIcons = true;
this.item.tooltip = tooltip;
this.item.show();
}
async menu(): Promise<void> {
type Item = vscode.QuickPickItem & { run: () => unknown };
const { missing, foreign } = this.status();
const items: Item[] = [
{ label: '$(refresh) Reload Workspace', description: 'DotRush: Reload Workspace, counting from zero', run: () => this.reload() },
{ label: '$(file-submodule) Pick solution for DotRush', description: 'dotrush.roslyn.projectOrSolutionFiles', run: () => vscode.commands.executeCommand('dotrush.pickProjectOrSolutionFiles') },
{ label: '$(output) Show DotRush output', run: () => vscode.commands.executeCommand('workbench.action.output.show.extension-output-nromanov.dotrush-#1-DotRush').then(undefined, () => vscode.commands.executeCommand('workbench.action.output.toggleOutput')) },
];
const picked = await vscode.window.showQuickPick(items, {
title: 'Language server' + (missing.length ? `${missing.length} not loaded` : '') + (foreign.length ? `${foreign.length} foreign` : ''),
});
await picked?.run();
}
dispose(): void {
for (const subscription of this.subscriptions) {
subscription.dispose();
}
}
}
+7 -1
View File
@@ -7,11 +7,13 @@ import { ALL_TARGETS, createTask, runTask, SolutionTaskProvider, TASK_TYPE, task
import { launchOptionsFor } from './launch';
import { StatusBar } from './status';
import { HotReloadController } from './hotreload/controller';
import { LanguageServerIndicator } from './dotrush';
let model: SolutionModel;
let status: StatusBar;
let log: vscode.OutputChannel;
let hotReload: HotReloadController;
let languageServer: LanguageServerIndicator;
function settings() {
return vscode.workspace.getConfiguration('dotnetSolution');
@@ -306,13 +308,14 @@ export function activate(context: vscode.ExtensionContext): void {
model = new SolutionModel(context.workspaceState, log);
status = new StatusBar(model);
hotReload = new HotReloadController(model, log, () => build('build'));
languageServer = new LanguageServerIndicator(model, log);
const active = () => model.active;
const command = (id: string, handler: (...args: unknown[]) => unknown) =>
vscode.commands.registerCommand(id, handler);
context.subscriptions.push(
log, model, status, hotReload,
log, model, status, hotReload, languageServer,
vscode.tasks.registerTaskProvider(TASK_TYPE, new SolutionTaskProvider(model)),
vscode.debug.registerDebugConfigurationProvider(
@@ -340,6 +343,9 @@ export function activate(context: vscode.ExtensionContext): void {
command('dotnetSolution.hotReload.detach', () => hotReload.detach()),
command('dotnetSolution.hotReload.showTerminal', () => hotReload.showTerminal()),
command('dotnetSolution.hotReload.menu', () => hotReload.menu()),
command('dotnetSolution.languageServerMenu', () => languageServer.menu()),
command('dotnetSolution.languageServerReload', () => languageServer.reload()),
command('dotnetSolution.languageServerStatus', () => languageServer.status()),
command('dotnetSolution.reload', () => model.reload()),
command('dotnetSolution.showOutput', () => log.show()),
command('dotnetSolution.setStartupProject', async (resource?: unknown) => {