Files
vs-code-dotnet-solution-lau…/src/launch.ts
T
maxandClaude Fable 5.1 0300dfa42f 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
2026-09-08 13:33:12 +02:00

170 lines
7.2 KiB
TypeScript

import * as vscode from 'vscode';
import * as fs from 'fs';
import * as path from 'path';
import { SolutionModel } from './model';
import { taskLabel } from './tasks';
export const LAUNCH_NAME = '.NET Solution: Debug startup project';
function launchSettings() {
return vscode.workspace.getConfiguration('dotnetSolution');
}
/** Launch options for one project, from settings or launchSettings.json. */
export interface ProjectLaunchOptions {
args?: string[];
env?: Record<string, string>;
cwd?: string;
console?: string;
/** Profile name in Properties/launchSettings.json; empty picks the first "Project" profile. */
profile?: string;
}
interface LaunchProfile {
commandName?: string;
commandLineArgs?: string;
workingDirectory?: string;
environmentVariables?: Record<string, string>;
}
/** Splits a launchSettings commandLineArgs string the way the SDK does: on whitespace, honouring quotes. */
export function splitCommandLine(text: string): string[] {
const args: string[] = [];
const pattern = /"((?:[^"\\]|\\.)*)"|'([^']*)'|(\S+)/g;
for (const match of text.matchAll(pattern)) {
args.push(match[1] !== undefined ? match[1].replace(/\\(.)/g, '$1') : match[2] ?? match[3]);
}
return args;
}
/** Reads the profile that applies to a project, if it has a Properties/launchSettings.json. */
export function readLaunchProfile(projectPath: string, profileName: string | undefined): LaunchProfile | undefined {
const file = path.join(path.dirname(projectPath), 'Properties', 'launchSettings.json');
if (!fs.existsSync(file)) {
return undefined;
}
try {
const profiles: Record<string, LaunchProfile> = JSON.parse(fs.readFileSync(file, 'utf8')).profiles ?? {};
if (profileName) {
return profiles[profileName];
}
// The first profile that launches the project itself, not IIS Express or a container.
return Object.values(profiles).find(profile => (profile.commandName ?? 'Project') === 'Project')
?? Object.values(profiles)[0];
} catch {
return undefined;
}
}
/**
* The launch options for one project: per-project settings override the global ones,
* and both override Properties/launchSettings.json.
*
* Builder and Editor want different arguments and working directories, which is why
* `dotnetSolution.launch.projects` is keyed by project name.
*/
export function launchOptionsFor(projectName: string, projectPath: string, projectDir: string): Required<Pick<ProjectLaunchOptions, 'args' | 'env' | 'console'>> & { cwd: string | undefined } {
const settings = launchSettings();
const perProject = settings.get<Record<string, ProjectLaunchOptions>>('launch.projects', {});
const own = perProject[projectName] ?? {};
const profile = readLaunchProfile(projectPath, own.profile ?? settings.get<string>('launch.profile', ''));
const args = own.args ?? (settings.get<string[]>('launch.args', []).length
? settings.get<string[]>('launch.args', [])
: profile?.commandLineArgs ? splitCommandLine(profile.commandLineArgs) : []);
const env = { ...(profile?.environmentVariables ?? {}), ...settings.get<Record<string, string>>('launch.env', {}), ...(own.env ?? {}) };
const rawCwd = own.cwd ?? settings.get<string>('launch.cwd', '') ?? '';
const cwd = rawCwd
? path.resolve(projectDir, rawCwd)
: profile?.workingDirectory ? path.resolve(projectDir, profile.workingDirectory) : undefined;
const console = own.console ?? settings.get<string>('launch.console', 'internalConsole');
return { args, env, cwd, console };
}
/**
* A complete launch configuration for the startup project under the active configuration.
*
* `program` is the apphost .exe when the project makes one, because vsdbg wants an
* executable. Projects without an apphost run through the dotnet host instead. The
* debugger options DotRush normally fills in (justMyCode, symbol servers) are left out so
* its own provider still adds them.
*/
export async function buildLaunchConfiguration(
model: SolutionModel, options: { noDebug?: boolean; preLaunchTask?: string }): Promise<vscode.DebugConfiguration> {
const active = model.active;
if (!active) {
throw new Error('No startup project selected.');
}
const target = await model.resolveTarget();
const info = active.project.info;
const launch = launchOptionsFor(info.name, info.fsPath, path.dirname(info.fsPath));
const cwd = launch.cwd ?? target.targetDir.replace(/[\\/]+$/, '');
const program = target.executablePath ?? 'dotnet';
const args = target.executablePath ? launch.args : [target.targetPath, ...launch.args];
return {
name: `${info.name} (${active.solutionConfiguration.configuration}|${active.solutionPlatform})`,
type: launchSettings().get<string>('debugType', 'coreclr'),
request: 'launch',
program,
args,
cwd,
env: launch.env,
console: launch.console,
noDebug: options.noDebug ?? false,
preLaunchTask: options.preLaunchTask,
// launchSettings.json is applied above so per-project settings can override it.
// DotRush still points vsdbg at the file; vsdbg only takes commandLineArgs from it
// when `args` is empty, and the environment merge is idempotent.
};
}
/** The static entry written to launch.json: values come back through `${command:…}` at launch time. */
export function launchJsonEntry(): Record<string, unknown> {
return {
name: LAUNCH_NAME,
type: launchSettings().get<string>('debugType', 'coreclr'),
request: 'launch',
program: '${command:dotnetSolution.activeProgram}',
args: [],
cwd: '${command:dotnetSolution.activeCwd}',
preLaunchTask: taskLabel('build'),
};
}
/**
* Offers the dynamic "debug the startup project" entry in the Run and Debug dropdown,
* and fills in `program` for launch.json entries that use this extension's variables
* but omitted them.
*/
export class SolutionDebugConfigurationProvider implements vscode.DebugConfigurationProvider {
constructor(private readonly model: SolutionModel) { }
async provideDebugConfigurations(): Promise<vscode.DebugConfiguration[]> {
if (!this.model.active) {
return [];
}
return [launchJsonEntry() as vscode.DebugConfiguration];
}
/**
* The launch.json entry carries no arguments, since ${command:} variables can only be
* strings. Fill in the per-project options here so F5 behaves like the debug button.
*/
resolveDebugConfiguration(_folder: vscode.WorkspaceFolder | undefined, config: vscode.DebugConfiguration): vscode.DebugConfiguration {
const active = this.model.active;
if (config.name !== LAUNCH_NAME || !active) {
return config;
}
const info = active.project.info;
const launch = launchOptionsFor(info.name, info.fsPath, path.dirname(info.fsPath));
if (!config.args || (Array.isArray(config.args) && config.args.length === 0)) {
config.args = launch.args;
}
config.env = { ...launch.env, ...(config.env ?? {}) };
config.console ??= launch.console;
return config;
}
}