CMake the way CLion does it (profiles and presets, targets, run and debug configurations, CTest) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Tomáš Černík <Tom.cernik@gmail.com> Reviewed-on: #1
This commit was merged in pull request #1.
This commit is contained in:
1 parent
f8032fd65d
commit
093cf7ee9c
38 files changed
+9352
No files matched your search
@@ -0,0 +1,211 @@
|
||||
/**
|
||||
* The run configurations editor, as CLion's Run/Debug Configurations for
|
||||
* "CMake Application": every target on the left (executables, custom targets,
|
||||
* libraries), the selected one's fields on the right: the executable it runs
|
||||
* (its own, another target's, or a file), program arguments, working
|
||||
* directory, environment, build before launch. OK / Cancel / Apply. It edits
|
||||
* the workspace setting `cmakeHelper.runConfigurations` (by target name).
|
||||
*/
|
||||
import * as crypto from "node:crypto";
|
||||
import * as path from "node:path";
|
||||
import * as vscode from "vscode";
|
||||
import { editorIcon, editorPage } from "./editorPage";
|
||||
import { executableOf } from "./fileApi";
|
||||
import { type RunConfiguration, runConfiguration, saveRunConfigurations } from "./launcher";
|
||||
import type { CMakeProject } from "./project";
|
||||
import { formatEnv, parseEnv, settings } from "./util";
|
||||
|
||||
let panel: vscode.WebviewPanel | undefined;
|
||||
|
||||
/** A target's row in the editor: what it is, and its configuration as the form's fields. */
|
||||
type Row = {
|
||||
name: string;
|
||||
kind: "executable" | "utility" | "library";
|
||||
own: string;
|
||||
executable: string;
|
||||
args: string;
|
||||
workingDirectory: string;
|
||||
environment: string;
|
||||
/** null: the setting's default (buildBeforeRun). */
|
||||
buildBeforeRun: boolean | null;
|
||||
};
|
||||
|
||||
const kindOf = (type: string): Row["kind"] => (type === "EXECUTABLE" ? "executable" : type === "UTILITY" ? "utility" : "library");
|
||||
|
||||
function rowsOf(project: CMakeProject): Row[] {
|
||||
return project.visibleTargets().map((t) => {
|
||||
const c = runConfiguration(t.name);
|
||||
return {
|
||||
name: t.name,
|
||||
kind: kindOf(t.type),
|
||||
own: executableOf(t) ? path.relative(project.buildDir, executableOf(t)!) : "",
|
||||
executable: c.executable ?? "",
|
||||
args: c.args ?? "",
|
||||
workingDirectory: c.workingDirectory ?? "",
|
||||
environment: formatEnv(c.environment),
|
||||
buildBeforeRun: c.buildBeforeRun ?? null,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const toConfiguration = (r: Row): RunConfiguration => ({
|
||||
executable: r.executable.trim() || undefined,
|
||||
args: r.args.trim() || undefined,
|
||||
workingDirectory: r.workingDirectory.trim() || undefined,
|
||||
environment: parseEnv(r.environment),
|
||||
buildBeforeRun: r.buildBeforeRun ?? undefined,
|
||||
});
|
||||
|
||||
/** Opens the editor (or brings it forward) on `select` (a target's name; else the active target). */
|
||||
export function openRunConfigEditor(project: CMakeProject, select?: string): void {
|
||||
const init = () => ({
|
||||
type: "init",
|
||||
rows: rowsOf(project),
|
||||
// Another target's program, for the Executable field (CLion's combo): relative to the build directory.
|
||||
programs: project.visibleTargets().map((t) => executableOf(t)).filter((p): p is string => !!p).map((p) => path.relative(project.buildDir, p)),
|
||||
active: project.activeTarget?.name ?? "",
|
||||
buildBefore: settings().get<boolean>("buildBeforeRun", true),
|
||||
defaultCwd: settings().get<string>("defaultWorkingDirectory") || "${targetDir}",
|
||||
profile: project.activeProfile.name,
|
||||
select: select ?? project.activeTarget?.name,
|
||||
});
|
||||
if (panel) {
|
||||
panel.reveal();
|
||||
void panel.webview.postMessage(init());
|
||||
return;
|
||||
}
|
||||
panel = vscode.window.createWebviewPanel("cmakeHelper.runConfigurations", "Run/Debug Configurations", vscode.ViewColumn.Active, { enableScripts: true, retainContextWhenHidden: true });
|
||||
panel.iconPath = editorIcon();
|
||||
const nonce = crypto.randomBytes(16).toString("base64");
|
||||
panel.webview.html = editorPage(panel.webview, nonce, { noun: "configuration", extra: '<datalist id="programs"></datalist>', script: SCRIPT, tools: false });
|
||||
panel.onDidDispose(() => (panel = undefined));
|
||||
panel.webview.onDidReceiveMessage(async (msg: { type: string; rows?: Row[]; close?: boolean }) => {
|
||||
if (msg.type === "ready") {
|
||||
void panel?.webview.postMessage(init());
|
||||
} else if (msg.type === "cancel") {
|
||||
panel?.dispose();
|
||||
} else if (msg.type === "save" && msg.rows) {
|
||||
try {
|
||||
await saveRunConfigurations(Object.fromEntries(msg.rows.map((r) => [r.name, toConfiguration(r)])), msg.rows.map((r) => r.name));
|
||||
} catch (e) {
|
||||
void panel?.webview.postMessage({ type: "error", text: e instanceof Error ? e.message : String(e) });
|
||||
return;
|
||||
}
|
||||
if (msg.close) panel?.dispose();
|
||||
else void panel?.webview.postMessage({ ...init(), type: "saved" });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** The editor's script: the targets by kind, the selected one's form. */
|
||||
const SCRIPT = String.raw`
|
||||
const vscode = acquireVsCodeApi();
|
||||
let rows = [], sel = null, dirty = false, active = "", buildBefore = true, defaultCwd = "", profile = "";
|
||||
const $ = (id) => document.getElementById(id);
|
||||
const esc = (s) => String(s ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]));
|
||||
const current = () => rows.find((r) => r.name === sel);
|
||||
const GROUPS = [["executable", "CMake Applications"], ["utility", "Custom Targets"], ["library", "Libraries"]];
|
||||
|
||||
function setDirty(v) {
|
||||
dirty = v;
|
||||
$("apply").disabled = !v;
|
||||
if (v) $("err").textContent = "";
|
||||
}
|
||||
|
||||
/** A target with something set shows it in the list (CLion's configurations exist only once made). */
|
||||
const customised = (r) => !!(r.executable || r.args || r.workingDirectory || r.environment || r.buildBeforeRun !== null);
|
||||
|
||||
function renderList() {
|
||||
$("list").innerHTML = GROUPS.map(([kind, label]) => {
|
||||
const of = rows.filter((r) => r.kind === kind);
|
||||
if (!of.length) return "";
|
||||
return '<li class="group">' + label + "</li>" + of.map((r) => {
|
||||
const tag = r.name === active ? '<span class="tag">active</span>' : customised(r) ? '<span class="tag">set</span>' : "";
|
||||
return '<li class="' + (r.name === sel ? "sel" : "") + '" data-name="' + esc(r.name) + '">' + esc(r.name) + tag + "</li>";
|
||||
}).join("");
|
||||
}).join("");
|
||||
}
|
||||
|
||||
function input(label, key, value, o = {}) {
|
||||
const attrs = (o.list ? ' list="' + o.list + '"' : "") + (o.placeholder ? ' placeholder="' + esc(o.placeholder) + '"' : "");
|
||||
const field = o.area
|
||||
? '<textarea data-k="' + key + '"' + attrs + ">" + esc(value) + "</textarea>"
|
||||
: '<input type="text" data-k="' + key + '" value="' + esc(value) + '"' + attrs + ">";
|
||||
return "<label>" + label + ":</label><div>" + field + (o.hint ? '<div class="hint">' + o.hint + "</div>" : "") + "</div>";
|
||||
}
|
||||
|
||||
const sec = (t) => '<div class="sec">' + t + "</div>";
|
||||
const VARS = "$" + "{buildDir}, $" + "{sourceDir}, $" + "{targetDir} and $" + "{env:X} are expanded.";
|
||||
|
||||
function renderForm() {
|
||||
const r = current();
|
||||
if (!r) {
|
||||
$("form").innerHTML = '<p class="empty">' + (rows.length ? "Select a target." : "The project has no targets yet: reload CMake.") + "</p>";
|
||||
return;
|
||||
}
|
||||
const own = r.kind === "executable";
|
||||
const before = r.buildBeforeRun === null ? buildBefore : r.buildBeforeRun;
|
||||
const exeHint = own
|
||||
? "Empty: the target's own (" + esc(r.own) + "). Another runs instead after building it: a script, a test runner."
|
||||
: "What ▶ and 🐞 run after building " + esc(r.name) + ": another target's executable (pick one) or a file. Empty: " + (r.kind === "utility" ? "▶ only builds it." : "it can't be run.");
|
||||
$("form").innerHTML = '<div class="grid">' +
|
||||
'<label>Name:</label><div><input type="text" value="' + esc(r.name) + '" disabled><div class="hint">' + (r.kind === "executable" ? "CMake Application" : r.kind === "utility" ? "Custom target (add_custom_target)" : "Library") + ", profile " + esc(profile) + "</div></div>" +
|
||||
sec("Run") +
|
||||
input("Executable", "executable", r.executable, { list: "programs", placeholder: own ? r.own : "Not selected", hint: exeHint + " Relative to the build directory." }) +
|
||||
input("Program arguments", "args", r.args, { placeholder: "--verbose input.txt", hint: "As on a command line. " + VARS }) +
|
||||
input("Working directory", "workingDirectory", r.workingDirectory, { placeholder: defaultCwd + " (default)", hint: "Relative paths are from the source directory." }) +
|
||||
input("Environment variables", "environment", r.environment, { area: true, placeholder: "A=1;B=2", hint: "NAME=value, separated by ;. On top of the profile's environment." }) +
|
||||
sec("Before launch") +
|
||||
'<span></span><label class="check"><input type="checkbox" data-k="buildBeforeRun"' + (before ? " checked" : "") + "> Build " + esc(r.name) + "</label>" +
|
||||
(r.buildBeforeRun === null ? "" : '<span></span><div class="hint"><a href="#" id="default-build">Use the default</a> (' + (buildBefore ? "build" : "don't build") + ", cmakeHelper.buildBeforeRun)</div>") +
|
||||
"</div>";
|
||||
const d = $("default-build");
|
||||
if (d) d.onclick = (e) => { e.preventDefault(); r.buildBeforeRun = null; setDirty(true); render(); };
|
||||
}
|
||||
|
||||
function render() {
|
||||
renderList();
|
||||
renderForm();
|
||||
}
|
||||
|
||||
$("list").addEventListener("click", (e) => {
|
||||
const li = e.target.closest("li[data-name]");
|
||||
if (!li) return;
|
||||
sel = li.dataset.name;
|
||||
render();
|
||||
});
|
||||
|
||||
$("form").addEventListener("input", (e) => {
|
||||
const r = current();
|
||||
const k = e.target.dataset.k;
|
||||
if (!r || !k) return;
|
||||
r[k] = e.target.type === "checkbox" ? e.target.checked : e.target.value;
|
||||
setDirty(true);
|
||||
if (k === "buildBeforeRun") renderForm();
|
||||
renderList();
|
||||
});
|
||||
|
||||
function save(close) {
|
||||
if (!dirty && close) return vscode.postMessage({ type: "cancel" });
|
||||
vscode.postMessage({ type: "save", rows, close });
|
||||
}
|
||||
$("ok").onclick = () => save(true);
|
||||
$("apply").onclick = () => save(false);
|
||||
$("cancel").onclick = () => vscode.postMessage({ type: "cancel" });
|
||||
|
||||
window.addEventListener("message", (e) => {
|
||||
const m = e.data;
|
||||
if (m.type === "init" || m.type === "saved") {
|
||||
const keep = sel;
|
||||
rows = m.rows; active = m.active; buildBefore = m.buildBefore; defaultCwd = m.defaultCwd; profile = m.profile;
|
||||
$("programs").innerHTML = m.programs.map((p) => '<option value="' + esc(p) + '">').join("");
|
||||
sel = (m.type === "saved" ? keep : m.select) ?? null;
|
||||
if (!rows.some((r) => r.name === sel)) sel = rows[0]?.name ?? null;
|
||||
setDirty(false);
|
||||
render();
|
||||
} else if (m.type === "error") {
|
||||
$("err").textContent = m.text;
|
||||
}
|
||||
});
|
||||
vscode.postMessage({ type: "ready" });
|
||||
`;
|
||||
Reference in new issue
Block a user