Debug custom targets, and a Docker image to build and develop in
Build / build (pull_request) Successful in 9m40s

- 🐞 on a custom target without an executable set debugs the program its
  command runs, as the build system has it (Ninja's edge, a Makefile's
  recipe): its arguments, directory and cmake -E env variables; what the
  target depends on is built first, not the target itself
- Debug for custom targets in the status bar, the Targets view and the
  code lenses above add_custom_target
- Dockerfile (Node, CMake, Ninja, g++, GDB), scripts/docker.sh to run a
  command in it, a dev container, and example/ to try the extension on

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Tomáš ČerníkandClaude Opus 5.5 committed 2026-10-26 09:38:08 +01:00
1 parent fe6fd8be69
commit 27d924ed06
18 files changed
+300 -9

No files matched your search

+14
View File
@@ -0,0 +1,14 @@
{
"name": "VS Code CMake Helper",
"build": { "dockerfile": "../Dockerfile", "context": ".." },
"workspaceFolder": "/workspace",
"workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind",
"remoteUser": "node",
"postCreateCommand": "npm ci",
"customizations": {
"vscode": {
// C/C++ for example/, and the debugger the extension's Debug uses.
"extensions": ["ms-vscode.cpptools"]
}
}
}
+6
View File
@@ -0,0 +1,6 @@
node_modules/
out/
*.vsix
.git/
example/build/
example/cmake-build-*/
+2
View File
@@ -1,3 +1,5 @@
node_modules/ node_modules/
out/ out/
*.vsix *.vsix
example/build/
example/cmake-build-*/
+8
View File
@@ -8,6 +8,14 @@
"args": ["--extensionDevelopmentPath=${workspaceFolder}"], "args": ["--extensionDevelopmentPath=${workspaceFolder}"],
"outFiles": ["${workspaceFolder}/out/**/*.js"], "outFiles": ["${workspaceFolder}/out/**/*.js"],
"preLaunchTask": "npm: compile" "preLaunchTask": "npm: compile"
},
{
"name": "Run Extension (example)",
"type": "extensionHost",
"request": "launch",
"args": ["--extensionDevelopmentPath=${workspaceFolder}", "${workspaceFolder}/example"],
"outFiles": ["${workspaceFolder}/out/**/*.js"],
"preLaunchTask": "npm: compile"
} }
] ]
} }
+6
View File
@@ -4,3 +4,9 @@ tsconfig.json
**/*.map **/*.map
.gitignore .gitignore
.vscode/** .vscode/**
Dockerfile
.dockerignore
.devcontainer/**
.gitea/**
scripts/**
example/**
+13
View File
@@ -0,0 +1,13 @@
# Everything the extension needs to be built and packaged (Node), and what it
# drives, to try it on example/: CMake, Ninja, a C++ compiler, GDB.
# Used by scripts/docker.sh and by the dev container (.devcontainer/).
FROM node:22-bookworm
RUN apt-get update \
&& apt-get install -y --no-install-recommends cmake ninja-build make g++ gdb gdbserver git ca-certificates \
&& rm -rf /var/lib/apt/lists/*
ENV NPM_CONFIG_UPDATE_NOTIFIER=false
WORKDIR /workspace
USER node
CMD ["bash"]
+19 -2
View File
@@ -7,10 +7,10 @@ CMake in VS Code the way CLion does it. Standalone: it doesn't need (or want) Mi
- **Profiles** (the Profiles view, the `$(layers)` item in the status bar): each one is a build directory with its own build type, generator, toolchain, cache variables and environment. They come from the `cmakeHelper.profiles` setting and from the configure presets in `CMakePresets.json` / `CMakeUserPresets.json`. With neither, there's CLion's default: **Debug** in `cmake-build-debug`. Switching profiles reads that directory's last configure if there is one, and configures otherwise. - **Profiles** (the Profiles view, the `$(layers)` item in the status bar): each one is a build directory with its own build type, generator, toolchain, cache variables and environment. They come from the `cmakeHelper.profiles` setting and from the configure presets in `CMakePresets.json` / `CMakeUserPresets.json`. With neither, there's CLion's default: **Debug** in `cmake-build-debug`. Switching profiles reads that directory's last configure if there is one, and configures otherwise.
- **Targets** (the Targets view): executables, libraries and custom targets (`add_custom_target`; `showUtilityTargets` turns them off; CTest's dashboard targets are never listed), each with its sources in CMake's source groups. Inline buttons ▶ Run, 🐞 Debug, 🔨 Build. The menu has *Set as Active*, *Edit Run Configuration…*, *Rebuild*, *Go to Definition in CMakeLists* and *Copy Path of the Built File*. - **Targets** (the Targets view): executables, libraries and custom targets (`add_custom_target`; `showUtilityTargets` turns them off; CTest's dashboard targets are never listed), each with its sources in CMake's source groups. Inline buttons ▶ Run, 🐞 Debug, 🔨 Build. The menu has *Set as Active*, *Edit Run Configuration…*, *Rebuild*, *Go to Definition in CMakeLists* and *Copy Path of the Built File*.
- **One active target** in the status bar: `$(target) app`, then 🔨 ▶ 🐞. Keys as in CLion: **Ctrl+F9** build, **Shift+F10** run, **Shift+F9** debug, **Alt+Shift+F10** choose the target. - **One active target** in the status bar: `$(target) app`, then 🔨 ▶ 🐞. Keys as in CLion: **Ctrl+F9** build, **Shift+F10** run, **Shift+F9** debug, **Alt+Shift+F10** choose the target.
- **Run configurations per target**, edited as CLion's *Run/Debug Configurations* (*Edit Run Configuration…* on a target, or *Edit Configurations…* in the target picker): every target on the left, by kind; on the right its executable (the target's own, another target's, or a file: what a custom target or a library runs after it's built), program arguments, working directory (default: the executable's directory), environment (`A=1;B=2`) and *Before launch: Build*. A custom target with no executable is only built by ▶, as in CLion. They're kept in the workspace settings under `cmakeHelper.runConfigurations`. - **Run configurations per target**, edited as CLion's *Run/Debug Configurations* (*Edit Run Configuration…* on a target, or *Edit Configurations…* in the target picker): every target on the left, by kind; on the right its executable (the target's own, another target's, or a file: what a custom target or a library runs after it's built), program arguments, working directory (default: the executable's directory), environment (`A=1;B=2`) and *Before launch: Build*. A custom target with no executable is only built by ▶, as in CLion; 🐞 debugs the program its command runs (`COMMAND app --flag`: `app` with `--flag`, in the command's directory, with `cmake -E env`'s variables), after building what the target depends on (not the target itself, which would run the command). The command comes from the build system, so this needs a Ninja or Makefile generator. They're kept in the workspace settings under `cmakeHelper.runConfigurations`.
- **Debugging** through C/C++ (`cppdbg`, or `cppvsdbg` with MSVC), CodeLLDB or LLDB DAP: the first one installed, or the one set in `cmakeHelper.debugger`. Extra launch properties go in `cmakeHelper.debugConfiguration`. - **Debugging** through C/C++ (`cppdbg`, or `cppvsdbg` with MSVC), CodeLLDB or LLDB DAP: the first one installed, or the one set in `cmakeHelper.debugger`. Extra launch properties go in `cmakeHelper.debugConfiguration`.
- **CTest in the Test Explorer**: tests are grouped by the target whose executable they run, and by suite for names like `Suite.Test`. Each one sits at its `TEST(…)` / `TEST_CASE("…")` / `BOOST_AUTO_TEST_CASE(…)` in the sources (with ▶ in the gutter), or else at its `add_test`. Running builds the tests' targets first, then calls `ctest` and reads the results from JUnit. Debugging starts the test's command under the debugger. Tests that `gtest_discover_tests` finds only after a build show up once the build is done. - **CTest in the Test Explorer**: tests are grouped by the target whose executable they run, and by suite for names like `Suite.Test`. Each one sits at its `TEST(…)` / `TEST_CASE("…")` / `BOOST_AUTO_TEST_CASE(…)` in the sources (with ▶ in the gutter), or else at its `add_test`. Running builds the tests' targets first, then calls `ctest` and reads the results from JUnit. Debugging starts the test's command under the debugger. Tests that `gtest_discover_tests` finds only after a build show up once the build is done.
- **Code lenses**: ▶ Run / 🐞 Debug above `main()` in an executable's sources, and Run / Debug / Build above each `add_executable` / `add_library` in CMake files. - **Code lenses**: ▶ Run / 🐞 Debug above `main()` in an executable's sources, and Run / Debug / Build above each `add_executable` / `add_custom_target`, and Build above each `add_library`, in CMake files.
- **Load CMake Project** on a `CMakeLists.txt` (the explorer's and the editor's context menu, as in CLion): that folder becomes the project (`cmakeHelper.sourceDirectory`) and it's configured; on the project's own `CMakeLists.txt` the menu has *Reload CMake Project*. - **Load CMake Project** on a `CMakeLists.txt` (the explorer's and the editor's context menu, as in CLion): that folder becomes the project (`cmakeHelper.sourceDirectory`) and it's configured; on the project's own `CMakeLists.txt` the menu has *Reload CMake Project*.
- **Reload**: when one of the project's CMake files changes, a notification and a `Reload CMake` item in the status bar appear (`cmakeHelper.autoReload`: `ask`, `always`, `never`). *Reset Cache and Reload Project* removes `CMakeCache.txt` and `CMakeFiles/` first. A generator mismatch offers the reset by itself. - **Reload**: when one of the project's CMake files changes, a notification and a `Reload CMake` item in the status bar appear (`cmakeHelper.autoReload`: `ask`, `always`, `never`). *Reset Cache and Reload Project* removes `CMakeCache.txt` and `CMakeFiles/` first. A generator mismatch offers the reset by itself.
- `compile_commands.json` is exported (`exportCompileCommands`), and can be copied where clangd looks for it (`copyCompileCommands`, e.g. `${sourceDir}/compile_commands.json`). - `compile_commands.json` is exported (`exportCompileCommands`), and can be copied where clangd looks for it (`copyCompileCommands`, e.g. `${sourceDir}/compile_commands.json`).
@@ -70,6 +70,23 @@ Updates** looks now; `cmakeHelper.updates.check` turns it off.
A release is made by the CI on a `v*` tag matching the version in `package.json`. A release is made by the CI on a `v*` tag matching the version in `package.json`.
## Development
Everything runs in the image of the `Dockerfile`: Node for the extension, and
CMake, Ninja, g++ and GDB for what it drives.
- **Dev container:** open this folder in VS Code and *Reopen in Container*
(Dev Containers extension). `npm ci` runs on creation; F5 **Run Extension
(example)** opens `example/`, a small project (a library, an executable, a
CTest test) to try it on.
- **Without VS Code:** `./scripts/docker.sh <command>` runs a command in the
image, in this folder and as you, e.g. `./scripts/docker.sh npm run package --
--skip-license`; without a command it's a shell.
The title bar patch (`cmakeHelper.toolbar`: `titleBar`) can't be tried in the
container: there the extension runs remotely and VS Code's installation is
the host's.
## Building ## Building
``` ```
+25
View File
@@ -0,0 +1,25 @@
# A small project to try the extension on: F5 "Run Extension (example)".
cmake_minimum_required(VERSION 3.20)
project(example CXX)
set(CMAKE_CXX_STANDARD 17)
add_library(greet src/greet.cpp)
target_include_directories(greet PUBLIC src)
add_executable(hello src/main.cpp)
target_link_libraries(hello PRIVATE greet)
enable_testing()
add_executable(greet_test tests/greet_test.cpp)
target_link_libraries(greet_test PRIVATE greet)
add_test(NAME greet COMMAND greet_test)
# Custom targets: ▶ builds them (runs their command), 🐞 debugs the program their command runs.
add_custom_target(run_hello
COMMAND hello CMake
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
COMMENT "Running hello")
add_custom_target(run_hello_env
COMMAND ${CMAKE_COMMAND} -E env GREETING=hi $<TARGET_FILE:hello> "with spaces"
COMMAND ${CMAKE_COMMAND} -E echo done)
+5
View File
@@ -0,0 +1,5 @@
#include "greet.h"
std::string greet(const std::string& name) {
return "Hello, " + name + "!";
}
+4
View File
@@ -0,0 +1,4 @@
#pragma once
#include <string>
std::string greet(const std::string& name);
+7
View File
@@ -0,0 +1,7 @@
#include <iostream>
#include "greet.h"
int main(int argc, char** argv) {
std::cout << greet(argc > 1 ? argv[1] : "world") << std::endl;
return 0;
}
+10
View File
@@ -0,0 +1,10 @@
#include <iostream>
#include "greet.h"
int main() {
if (greet("CMake") != "Hello, CMake!") {
std::cerr << "unexpected greeting: " << greet("CMake") << std::endl;
return 1;
}
return 0;
}
+2 -2
View File
@@ -377,7 +377,7 @@
}, },
{ {
"command": "cmakeHelper.debug", "command": "cmakeHelper.debug",
"when": "view == cmakeHelper.targets && viewItem =~ /^target\\.executable|\\.run$/", "when": "view == cmakeHelper.targets && viewItem =~ /^target\\.(executable|utility)|\\.run$/",
"group": "inline@2" "group": "inline@2"
}, },
{ {
@@ -397,7 +397,7 @@
}, },
{ {
"command": "cmakeHelper.debug", "command": "cmakeHelper.debug",
"when": "view == cmakeHelper.targets && viewItem =~ /^target\\.executable|\\.run$/", "when": "view == cmakeHelper.targets && viewItem =~ /^target\\.(executable|utility)|\\.run$/",
"group": "2_run@2" "group": "2_run@2"
}, },
{ {
+10
View File
@@ -0,0 +1,10 @@
#!/bin/sh
# Runs a command in the development image, in this folder, as you (so the
# files it makes are yours): ./scripts/docker.sh npm run package
# Without a command: a shell. The image is (re)built when the Dockerfile changes.
set -e
root=$(cd "$(dirname "$0")/.." && pwd)
image=vs-code-cmake-helper-dev
docker build -q -t "$image" "$root" >/dev/null
tty=; [ -t 0 ] && tty=-it
exec docker run --rm $tty -u "$(id -u):$(id -g)" -e HOME=/tmp -v "$root:/workspace" -w /workspace "$image" "${@:-bash}"
+2 -1
View File
@@ -49,7 +49,8 @@ export class LensProvider implements vscode.CodeLensProvider {
const d = t.definition; const d = t.definition;
if (!d || !samePath(d.file, doc.fileName) || d.line > doc.lineCount) continue; if (!d || !samePath(d.file, doc.fileName) || d.line > doc.lineCount) continue;
const range = doc.lineAt(d.line - 1).range; const range = doc.lineAt(d.line - 1).range;
if (t.type === "EXECUTABLE") { // A custom target's ▶ builds it (runs its command), 🐞 debugs the program its command runs.
if (t.type === "EXECUTABLE" || t.type === "UTILITY") {
lenses.push( lenses.push(
new vscode.CodeLens(range, { title: "$(play) Run", command: "cmakeHelper.run", arguments: [t.name] }), new vscode.CodeLens(range, { title: "$(play) Run", command: "cmakeHelper.run", arguments: [t.name] }),
new vscode.CodeLens(range, { title: "$(debug-alt) Debug", command: "cmakeHelper.debug", arguments: [t.name] }), new vscode.CodeLens(range, { title: "$(debug-alt) Debug", command: "cmakeHelper.debug", arguments: [t.name] }),
+118
View File
@@ -0,0 +1,118 @@
/**
* What a custom target (add_custom_target) runs, so it can be debugged: the
* File API doesn't say, the build system does. Ninja gives the command of the
* target's edge (`ninja -t commands -s`), a Makefile has it as the recipe in
* the target's build.make. Each command is "cd <dir> && <program> <args> && …";
* `cmake -E env A=B <program>` sets its environment, and CMake's own steps
* (`cmake -E echo`…) aren't what's debugged.
*/
import * as fs from "node:fs";
import * as path from "node:path";
import type { Target } from "./fileApi";
import type { CMakeProject } from "./project";
import { exists, isWindows, run, samePath, splitArgs } from "./util";
export interface CustomCommand {
program: string;
args: string[];
cwd: string;
env: Record<string, string>;
/** The program is one of the project's executables. */
ownExecutable: boolean;
}
/** The programs the target's commands run, or undefined when its generator isn't one this knows (Visual Studio, Xcode). */
export async function customCommands(project: CMakeProject, target: Target): Promise<CustomCommand[] | undefined> {
const model = project.model;
if (!model) return undefined;
let lines: string[] | undefined;
if (/Ninja/.test(model.generator)) lines = await ninjaCommands(project, target);
else if (/Makefiles/.test(model.generator)) lines = makeCommands(project, target);
if (!lines) return undefined;
const cmake = model.cache.get("CMAKE_COMMAND");
const executables = project.targets.filter((t) => t.type === "EXECUTABLE").flatMap((t) => t.artifacts);
const out: CustomCommand[] = [];
for (const line of lines) {
let cwd = model.buildDir;
for (const step of steps(line)) {
if (step[0] === "cd" && step.length === 2) {
cwd = path.resolve(cwd, step[1]!);
continue;
}
const c = command(step, cwd, cmake);
if (c) out.push({ ...c, ownExecutable: executables.some((e) => samePath(e, c.program)) });
}
}
return out;
}
/** The edge `<dir>/CMakeFiles/<name>` (`-<config>` with Ninja Multi-Config) is the one with the target's commands. */
async function ninjaCommands(project: CMakeProject, target: Target): Promise<string[]> {
const model = project.model!;
const rel = path.relative(model.buildDir, target.buildDir).split(path.sep).join("/");
const config = /Multi-Config/.test(model.generator) ? `-${project.configuration}` : "";
const edge = `${rel ? `${rel}/` : ""}CMakeFiles/${target.name}${config}`;
const ninja = model.cache.get("CMAKE_MAKE_PROGRAM") || "ninja";
const r = await run(ninja, ["-C", model.buildDir, "-t", "commands", "-s", edge]);
return r.code === 0 ? r.stdout.split(/\r?\n/).filter((l) => l.trim()) : [];
}
/** The recipe of `…/CMakeFiles/<name>:` in the target's build.make; its lines run in the build directory. */
function makeCommands(project: CMakeProject, target: Target): string[] {
const file = path.join(target.buildDir, "CMakeFiles", `${target.name}.dir`, "build.make");
if (!exists(file)) return [];
const cmake = project.model!.cache.get("CMAKE_COMMAND") ?? "cmake";
const header = new RegExp(`^(\\S*/)?CMakeFiles/${target.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}:`);
const lines: string[] = [];
let inRule = false;
for (const line of fs.readFileSync(file, "utf8").split(/\r?\n/)) {
if (line.startsWith("\t")) {
if (inRule) lines.push(line.slice(1).replace(/^[@-]+/, "").replace(/\$\(CMAKE_COMMAND\)/g, cmake));
} else {
inRule = header.test(line);
}
}
return lines;
}
/** "a b && c d" to [["a", "b"], ["c", "d"]]. */
function steps(line: string): string[][] {
const out: string[][] = [[]];
for (const word of splitArgs(line)) {
if (word === "&&") out.push([]);
else out[out.length - 1]!.push(word);
}
return out.filter((s) => s.length);
}
/** A step to a program to debug: `cmake -E env` unwrapped, CMake's own steps (echo, copy…) left out. */
function command(step: string[], cwd: string, cmake: string | undefined): Omit<CustomCommand, "ownExecutable"> | undefined {
const isCMake = (p: string) => (cmake && samePath(p, cmake)) || /^cmake(\.exe)?$/i.test(path.basename(p));
const env: Record<string, string> = {};
let words = step;
if (isCMake(words[0]!) && words[1] === "-E" && words[2] === "env") {
words = words.slice(3);
while (words.length && (/^[A-Za-z_]\w*=/.test(words[0]!) || words[0]!.startsWith("--"))) {
const w = words.shift()!;
if (w === "--") break;
const i = w.indexOf("=");
if (!w.startsWith("--")) env[w.slice(0, i)] = w.slice(i + 1);
}
}
if (!words.length || isCMake(words[0]!)) return undefined;
return { program: resolveProgram(words[0]!, cwd), args: words.slice(1), cwd, env };
}
/** A path as the shell would take it: relative to the directory, a bare name from PATH. */
function resolveProgram(program: string, cwd: string): string {
if (path.isAbsolute(program)) return program;
if (program.includes("/") || (isWindows && program.includes("\\"))) return path.resolve(cwd, program);
const exts = isWindows ? (process.env.PATHEXT ?? ".EXE").split(";") : [""];
for (const dir of (process.env.PATH ?? "").split(path.delimiter)) {
for (const ext of exts) {
const p = path.join(dir, program + ext);
if (dir && exists(p)) return p;
}
}
return program;
}
+45 -1
View File
@@ -4,11 +4,13 @@
* working directory, environment, build before run), kept in the setting * working directory, environment, build before run), kept in the setting
* `cmakeHelper.runConfigurations` by target name. An executable runs itself * `cmakeHelper.runConfigurations` by target name. An executable runs itself
* unless another is set; a custom target or a library runs the executable set * unless another is set; a custom target or a library runs the executable set
* for it, and without one ▶ only builds it. * for it, and without one ▶ only builds it, and 🐞 debugs the program the
* custom target's command runs (customTarget.ts).
*/ */
import * as path from "node:path"; import * as path from "node:path";
import * as vscode from "vscode"; import * as vscode from "vscode";
import { type Target, executableOf } from "./fileApi"; import { type Target, executableOf } from "./fileApi";
import { type CustomCommand, customCommands } from "./customTarget";
import type { CMakeProject } from "./project"; import type { CMakeProject } from "./project";
import { errorText, exists, expand, isWindows, onPath, settings, splitArgs } from "./util"; import { errorText, exists, expand, isWindows, onPath, settings, splitArgs } from "./util";
@@ -105,12 +107,54 @@ export async function runTarget(project: CMakeProject, target: Target): Promise<
/** 🐞 Debug: builds it (if set to), then starts a debug session on its program. */ /** 🐞 Debug: builds it (if set to), then starts a debug session on its program. */
export async function debugTarget(project: CMakeProject, target: Target): Promise<void> { export async function debugTarget(project: CMakeProject, target: Target): Promise<void> {
if (!programOf(project, target) && target.type === "UTILITY") return debugCustomTarget(project, target);
if (!programOf(project, target)) return noProgram(target, "debug"); if (!programOf(project, target)) return noProgram(target, "debug");
if (!(await buildFirst(project, target))) return; if (!(await buildFirst(project, target))) return;
const l = launchOf(project, target); const l = launchOf(project, target);
if (l) await debugProgram(project, l); if (l) await debugProgram(project, l);
} }
/**
* 🐞 on a custom target without an executable set: the program its command runs (the project's
* own executable, if one; else a choice), as the command runs it. What it depends on is built
* first, not the target itself: that would run the command.
*/
async function debugCustomTarget(project: CMakeProject, target: Target): Promise<void> {
const commands = await customCommands(project, target);
if (!commands) {
void vscode.window.showWarningMessage(`Debugging a custom target needs a Ninja or Makefile generator (this is ${project.model?.generator}): set an executable for ${target.name} in its run configuration.`);
return;
}
const own = commands.filter((c) => c.ownExecutable);
let c: CustomCommand | undefined = own.length === 1 ? own[0] : commands.length === 1 ? commands[0] : undefined;
if (!c && commands.length) {
const pick = await vscode.window.showQuickPick(
(own.length ? own : commands).map((x) => ({ label: path.basename(x.program), description: x.args.join(" "), detail: x.program, command: x })),
{ title: `Debug ${target.name}: which of its commands?` },
);
if (!pick) return;
c = pick.command;
}
if (!c) return noProgram(target, "debug");
const conf = runConfiguration(target.name);
if (conf.buildBeforeRun ?? settings().get<boolean>("buildBeforeRun", true)) {
const deps = target.dependencies.map((id) => project.targets.find((t) => t.id === id)?.name).filter((n): n is string => !!n);
if (deps.length && !(await project.build(deps))) {
const action = await vscode.window.showErrorMessage(`Building what ${target.name} depends on failed.`, "Debug Anyway");
if (action !== "Debug Anyway") return;
}
}
if (!exists(c.program)) {
void vscode.window.showErrorMessage(`${c.program} isn't there. Build what ${target.name} depends on first.`);
return;
}
const vars = project.vars(target);
const env: Record<string, string> = { ...project.activeProfile.env, ...c.env };
for (const [k, v] of Object.entries(conf.environment ?? {})) env[k] = expand(String(v), vars);
const cwd = conf.workingDirectory ? path.resolve(project.sourceDir, expand(conf.workingDirectory, vars)) : c.cwd;
await debugProgram(project, { name: target.name, program: c.program, args: [...c.args, ...splitArgs(expand(conf.args ?? "", vars))], cwd, env });
}
/** Starts a debugger on a program (also a test's command). */ /** Starts a debugger on a program (also a test's command). */
export async function debugProgram(project: CMakeProject, l: Launch): Promise<boolean> { export async function debugProgram(project: CMakeProject, l: Launch): Promise<boolean> {
const config = debugConfiguration(project, l); const config = debugConfiguration(project, l);
+4 -3
View File
@@ -98,13 +98,14 @@ export class StatusBar implements vscode.Disposable {
this.build.tooltip = busy ? "Busy…" : `Build ${t.name} (Ctrl+F9)`; this.build.tooltip = busy ? "Busy…" : `Build ${t.name} (Ctrl+F9)`;
this.build.text = p.state === "building" ? "$(loading~spin)" : "$(tools)"; this.build.text = p.state === "building" ? "$(loading~spin)" : "$(tools)";
this.build.show(); this.build.show();
// ▶ for what has a program to run, and for a custom target (it's built); 🐞 only with a program. // ▶ for what has a program to run, and for a custom target (it's built); 🐞 for those too.
const program = !!programOf(p, t); const program = !!programOf(p, t);
this.run.tooltip = program ? `Run ${t.name} (Shift+F10)` : `Build ${t.name}: it runs no program (Shift+F10)`; this.run.tooltip = program ? `Run ${t.name} (Shift+F10)` : `Build ${t.name}: it runs no program (Shift+F10)`;
this.debug.tooltip = `Debug ${t.name} (Shift+F9)`; // A custom target's 🐞 debugs the program its command runs.
this.debug.tooltip = program || t.type !== "UTILITY" ? `Debug ${t.name} (Shift+F9)` : `Debug the program ${t.name}'s command runs (Shift+F9)`;
if (program || t.type === "UTILITY") this.run.show(); if (program || t.type === "UTILITY") this.run.show();
else this.run.hide(); else this.run.hide();
if (program) this.debug.show(); if (program || t.type === "UTILITY") this.debug.show();
else this.debug.hide(); else this.debug.hide();
} }