- 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
170 lines
7.2 KiB
TypeScript
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;
|
|
}
|
|
}
|