Compare commits
2
Commits
| 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).
|
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,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
|
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.
|
||||||
|
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
|
## 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