Compare commits
2
Commits
86b21c15d0
...
0781418aaf
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0781418aaf | ||
|
|
9e64be0520 |
@@ -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 |
|
||||
|
||||
@@ -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
@@ -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 { 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) => {
|
||||
|
||||
Reference in New Issue
Block a user