Per-project launch options, publish/test tasks, explorer badge, keybindings

- dotnetSolution.launch.projects: args, env, cwd, console and profile
  per project name, layered over the global settings and
  Properties/launchSettings.json. Applied to the debug button and,
  through resolveDebugConfiguration, to the launch.json entry too.
- publish and test task targets with the same configuration/platform.
- Problems panel opens when a task fails.
- The startup csproj and its folder are marked in the explorer.
- F5 / Ctrl+F5 / Ctrl+Shift+B map to debug/run/build until a launch.json
  entry exists; Ctrl+Alt+C and Ctrl+Alt+P open the pickers.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0169iPWwKHZoBTNN9qwXiwqk
This commit is contained in:
max
2026-09-08 13:33:12 +02:00
co-authored by Claude Fable 5.1
parent 3433162cf4
commit 63de8a31f6
7 changed files with 504 additions and 68 deletions
+69 -4
View File
@@ -3,7 +3,8 @@ import * as path from 'path';
import { buildLaunchConfiguration, LAUNCH_NAME, launchJsonEntry, SolutionDebugConfigurationProvider } from './launch';
import { SolutionModel, StartupProject } from './model';
import { configurationKey, projectConfigurationFor, SolutionConfiguration } from './sln';
import { createTask, runTask, SolutionTaskProvider, TASK_TYPE, taskLabel, BuildTarget } from './tasks';
import { ALL_TARGETS, createTask, runTask, SolutionTaskProvider, TASK_TYPE, taskLabel, BuildTarget } from './tasks';
import { launchOptionsFor } from './launch';
import { StatusBar } from './status';
let model: SolutionModel;
@@ -109,8 +110,12 @@ async function build(target: BuildTarget): Promise<number> {
try {
const code = await runTask(task);
if (code !== 0) {
if (settings().get<boolean>('showProblemsOnFailure', true)) {
// The $msCompile matcher has already filled the Problems panel by now.
void vscode.commands.executeCommand('workbench.actions.view.problems');
}
void vscode.window.showErrorMessage(
`${task.name} failed (exit code ${code}). See the terminal or the Problems panel.`);
`${task.name} failed (exit code ${code}). See the Problems panel or the terminal.`);
}
return code;
} finally {
@@ -167,8 +172,7 @@ async function generateLaunchConfig(): Promise<void> {
const tasks = vscode.workspace.getConfiguration('tasks', folder.uri);
const existing = (tasks.get<Record<string, unknown>[]>('tasks') ?? [])
.filter(entry => entry.type !== TASK_TYPE);
const targets: BuildTarget[] = ['build', 'rebuild', 'clean'];
for (const target of targets) {
for (const target of ALL_TARGETS) {
existing.push({
label: taskLabel(target),
type: TASK_TYPE,
@@ -253,6 +257,46 @@ async function syncWorkspaceProperties(): Promise<void> {
}
}
// ---- explorer badge ---------------------------------------------------------------
/** Marks the startup project file, and the folder holding it, in the explorer. */
class StartupProjectDecorations implements vscode.FileDecorationProvider {
private readonly changed = new vscode.EventEmitter<vscode.Uri | vscode.Uri[] | undefined>();
readonly onDidChangeFileDecorations = this.changed.event;
private current: string | undefined;
update(projectPath: string | undefined): void {
this.current = projectPath ? path.normalize(projectPath).toLowerCase() : undefined;
this.changed.fire(undefined);
}
provideFileDecoration(uri: vscode.Uri): vscode.FileDecoration | undefined {
if (!this.current || uri.scheme !== 'file') {
return undefined;
}
const fsPath = path.normalize(uri.fsPath).toLowerCase();
const isProject = fsPath === this.current;
const isFolder = fsPath === path.dirname(this.current);
if (!isProject && !isFolder) {
return undefined;
}
const decoration = new vscode.FileDecoration('▶', 'Startup project (.NET Solution)',
new vscode.ThemeColor('debugIcon.startForeground'));
decoration.propagate = false;
return decoration;
}
}
/** `when` clause contexts for the keybindings. */
function updateContexts(): void {
const active = model.active;
void vscode.commands.executeCommand('setContext', 'dotnetSolution.active', active !== undefined);
const folder = active ? vscode.workspace.getWorkspaceFolder(vscode.Uri.file(active.solution.fsPath)) : undefined;
const configurations = vscode.workspace.getConfiguration('launch', folder?.uri).get<{ name?: string }[]>('configurations') ?? [];
void vscode.commands.executeCommand('setContext', 'dotnetSolution.hasLaunchEntry',
configurations.some(entry => entry.name === LAUNCH_NAME));
}
// ---- activation -------------------------------------------------------------------
export function activate(context: vscode.ExtensionContext): void {
@@ -279,6 +323,8 @@ export function activate(context: vscode.ExtensionContext): void {
command('dotnetSolution.build', () => build('build')),
command('dotnetSolution.rebuild', () => build('rebuild')),
command('dotnetSolution.clean', () => build('clean')),
command('dotnetSolution.publish', () => build('publish')),
command('dotnetSolution.test', () => build('test')),
command('dotnetSolution.debug', () => launch(false)),
command('dotnetSolution.run', () => launch(true)),
command('dotnetSolution.generateLaunchConfig', generateLaunchConfig),
@@ -301,6 +347,11 @@ export function activate(context: vscode.ExtensionContext): void {
}),
command('dotnetSolution.activeTargetPath', async () => (await model.resolveTarget()).targetPath),
command('dotnetSolution.activeTargetDir', async () => (await model.resolveTarget()).targetDir.replace(/[\\/]+$/, '')),
command('dotnetSolution.activeCwd', async () => {
const info = active()?.project.info;
const own = info ? launchOptionsFor(info.name, info.fsPath, path.dirname(info.fsPath)).cwd : undefined;
return own ?? (await model.resolveTarget()).targetDir.replace(/[\\/]+$/, '');
}),
command('dotnetSolution.activeProjectPath', () => active()?.project.info.fsPath),
command('dotnetSolution.activeProjectName', () => active()?.project.info.name),
command('dotnetSolution.activeSolutionPath', () => active()?.solution.fsPath),
@@ -320,6 +371,20 @@ export function activate(context: vscode.ExtensionContext): void {
}),
);
const decorations = new StartupProjectDecorations();
context.subscriptions.push(
vscode.window.registerFileDecorationProvider(decorations),
model.onDidChange(() => {
decorations.update(model.startupProject?.info.fsPath);
updateContexts();
}),
vscode.workspace.onDidChangeConfiguration(event => {
if (event.affectsConfiguration('launch')) {
updateContexts();
}
}),
);
connectDotRush(context);
void model.reload().then(() => offerLaunchJson(context));
}