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; 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; } /** 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 = 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> & { cwd: string | undefined } { const settings = launchSettings(); const perProject = settings.get>('launch.projects', {}); const own = perProject[projectName] ?? {}; const profile = readLaunchProfile(projectPath, own.profile ?? settings.get('launch.profile', '')); const args = own.args ?? (settings.get('launch.args', []).length ? settings.get('launch.args', []) : profile?.commandLineArgs ? splitCommandLine(profile.commandLineArgs) : []); const env = { ...(profile?.environmentVariables ?? {}), ...settings.get>('launch.env', {}), ...(own.env ?? {}) }; const rawCwd = own.cwd ?? settings.get('launch.cwd', '') ?? ''; const cwd = rawCwd ? path.resolve(projectDir, rawCwd) : profile?.workingDirectory ? path.resolve(projectDir, profile.workingDirectory) : undefined; const console = own.console ?? settings.get('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 { 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('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 { return { name: LAUNCH_NAME, type: launchSettings().get('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 { 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; } }