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
This commit is contained in:
@@ -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).
|
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
|
- 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.
|
`Test | x64`, `Release | Any CPU` — exactly as `MyGame.sln` lists them.
|
||||||
- The **debug button** builds with that configuration and launches the startup project.
|
- The **debug button** builds with that configuration and launches the startup project.
|
||||||
- The **flame** starts it under `dotnet watch` with hot reload instead.
|
- 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
|
## Why, when DotRush already has a status bar item
|
||||||
|
|
||||||
@@ -141,6 +143,26 @@ 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
|
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.
|
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.
|
||||||
|
|
||||||
|
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
|
## Usage
|
||||||
|
|
||||||
| Action | How |
|
| Action | How |
|
||||||
|
|||||||
@@ -129,6 +129,22 @@
|
|||||||
"title": "Hot Reload: Actions",
|
"title": "Hot Reload: Actions",
|
||||||
"category": ".NET Solution"
|
"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",
|
"command": "dotnetSolution.generateLaunchConfig",
|
||||||
"title": "Create launch.json and tasks.json entries",
|
"title": "Create launch.json and tasks.json entries",
|
||||||
@@ -190,6 +206,10 @@
|
|||||||
{
|
{
|
||||||
"command": "dotnetSolution.hotReload.menu",
|
"command": "dotnetSolution.hotReload.menu",
|
||||||
"when": "false"
|
"when": "false"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "dotnetSolution.languageServerStatus",
|
||||||
|
"when": "false"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"debug/toolBar": [
|
"debug/toolBar": [
|
||||||
|
|||||||
+153
@@ -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
@@ -7,11 +7,13 @@ import { ALL_TARGETS, createTask, runTask, SolutionTaskProvider, TASK_TYPE, task
|
|||||||
import { launchOptionsFor } from './launch';
|
import { launchOptionsFor } from './launch';
|
||||||
import { StatusBar } from './status';
|
import { StatusBar } from './status';
|
||||||
import { HotReloadController } from './hotreload/controller';
|
import { HotReloadController } from './hotreload/controller';
|
||||||
|
import { LanguageServerIndicator } from './dotrush';
|
||||||
|
|
||||||
let model: SolutionModel;
|
let model: SolutionModel;
|
||||||
let status: StatusBar;
|
let status: StatusBar;
|
||||||
let log: vscode.OutputChannel;
|
let log: vscode.OutputChannel;
|
||||||
let hotReload: HotReloadController;
|
let hotReload: HotReloadController;
|
||||||
|
let languageServer: LanguageServerIndicator;
|
||||||
|
|
||||||
function settings() {
|
function settings() {
|
||||||
return vscode.workspace.getConfiguration('dotnetSolution');
|
return vscode.workspace.getConfiguration('dotnetSolution');
|
||||||
@@ -306,13 +308,14 @@ export function activate(context: vscode.ExtensionContext): void {
|
|||||||
model = new SolutionModel(context.workspaceState, log);
|
model = new SolutionModel(context.workspaceState, log);
|
||||||
status = new StatusBar(model);
|
status = new StatusBar(model);
|
||||||
hotReload = new HotReloadController(model, log, () => build('build'));
|
hotReload = new HotReloadController(model, log, () => build('build'));
|
||||||
|
languageServer = new LanguageServerIndicator(model, log);
|
||||||
|
|
||||||
const active = () => model.active;
|
const active = () => model.active;
|
||||||
const command = (id: string, handler: (...args: unknown[]) => unknown) =>
|
const command = (id: string, handler: (...args: unknown[]) => unknown) =>
|
||||||
vscode.commands.registerCommand(id, handler);
|
vscode.commands.registerCommand(id, handler);
|
||||||
|
|
||||||
context.subscriptions.push(
|
context.subscriptions.push(
|
||||||
log, model, status, hotReload,
|
log, model, status, hotReload, languageServer,
|
||||||
|
|
||||||
vscode.tasks.registerTaskProvider(TASK_TYPE, new SolutionTaskProvider(model)),
|
vscode.tasks.registerTaskProvider(TASK_TYPE, new SolutionTaskProvider(model)),
|
||||||
vscode.debug.registerDebugConfigurationProvider(
|
vscode.debug.registerDebugConfigurationProvider(
|
||||||
@@ -340,6 +343,9 @@ export function activate(context: vscode.ExtensionContext): void {
|
|||||||
command('dotnetSolution.hotReload.detach', () => hotReload.detach()),
|
command('dotnetSolution.hotReload.detach', () => hotReload.detach()),
|
||||||
command('dotnetSolution.hotReload.showTerminal', () => hotReload.showTerminal()),
|
command('dotnetSolution.hotReload.showTerminal', () => hotReload.showTerminal()),
|
||||||
command('dotnetSolution.hotReload.menu', () => hotReload.menu()),
|
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.reload', () => model.reload()),
|
||||||
command('dotnetSolution.showOutput', () => log.show()),
|
command('dotnetSolution.showOutput', () => log.show()),
|
||||||
command('dotnetSolution.setStartupProject', async (resource?: unknown) => {
|
command('dotnetSolution.setStartupProject', async (resource?: unknown) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user