Files
VS-Code-CMake-helper/src/views.ts
T
093cf7ee9c
Build / build (push) Successful in 9m40s
VS Code CMake Helper: first implementation (#1)
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
2026-10-26 17:56:59 +00:00

159 lines
7.1 KiB
TypeScript

/**
* The CMake view container: Targets (executables, libraries and the rest,
* each with its sources), and Profiles.
*/
import * as path from "node:path";
import * as vscode from "vscode";
import { type Target, type TargetType, isLibrary } from "./fileApi";
import type { CMakeProject } from "./project";
import { programOf } from "./launcher";
const TYPE_LABEL: Record<TargetType, string> = {
EXECUTABLE: "executable",
STATIC_LIBRARY: "static library",
SHARED_LIBRARY: "shared library",
MODULE_LIBRARY: "module library",
OBJECT_LIBRARY: "object library",
INTERFACE_LIBRARY: "interface library",
UTILITY: "utility",
};
export type TargetNode =
| { kind: "group"; label: string; targets: Target[]; icon: string }
| { kind: "target"; target: Target }
| { kind: "sourceGroup"; target: Target; label: string; files: string[] }
| { kind: "source"; target: Target; file: string; generated: boolean };
export class TargetsProvider implements vscode.TreeDataProvider<TargetNode> {
private readonly changed = new vscode.EventEmitter<TargetNode | undefined>();
readonly onDidChangeTreeData = this.changed.event;
constructor(private readonly project: () => CMakeProject | undefined) {}
refresh(): void {
this.changed.fire(undefined);
}
getChildren(node?: TargetNode): TargetNode[] {
const p = this.project();
if (!p?.model) return [];
if (!node) {
const targets = p.visibleTargets();
const groups: TargetNode[] = [];
const add = (label: string, icon: string, list: Target[]) => {
if (list.length) groups.push({ kind: "group", label, icon, targets: list });
};
add("Executables", "play-circle", targets.filter((t) => t.type === "EXECUTABLE"));
add("Libraries", "library", targets.filter(isLibrary));
add("Utility", "gear", targets.filter((t) => t.type === "UTILITY"));
return groups;
}
if (node.kind === "group") return node.targets.map((target) => ({ kind: "target", target }));
if (node.kind === "target") {
const t = node.target;
const groups = new Map<string, string[]>();
for (const s of t.sources) {
const list = groups.get(s.group) ?? [];
list.push(s.path);
groups.set(s.group, list);
}
// One group (CMake's own "Source Files") isn't worth a level.
if (groups.size <= 1) return sources(t, t.sources.map((s) => s.path));
return [...groups].map(([label, files]) => ({ kind: "sourceGroup", target: t, label: label || "Other", files }));
}
if (node.kind === "sourceGroup") return sources(node.target, node.files);
return [];
}
getTreeItem(node: TargetNode): vscode.TreeItem {
const p = this.project();
switch (node.kind) {
case "group": {
const item = new vscode.TreeItem(node.label, vscode.TreeItemCollapsibleState.Expanded);
item.iconPath = new vscode.ThemeIcon(node.icon);
item.description = String(node.targets.length);
item.contextValue = "group";
return item;
}
case "target": {
const t = node.target;
const active = p?.activeTarget?.name === t.name;
const item = new vscode.TreeItem(t.name, t.sources.length ? vscode.TreeItemCollapsibleState.Collapsed : vscode.TreeItemCollapsibleState.None);
item.id = `target:${t.id}`;
item.description = [active ? "● active" : "", t.type === "EXECUTABLE" ? "" : TYPE_LABEL[t.type]].filter(Boolean).join(" ");
item.iconPath = new vscode.ThemeIcon(t.type === "EXECUTABLE" ? "terminal" : t.type === "UTILITY" ? "gear" : "package", active ? new vscode.ThemeColor("charts.green") : undefined);
// ".run": a custom target or a library with an executable set in its run configuration.
item.contextValue = (t.type === "EXECUTABLE" ? "target.executable" : t.type === "UTILITY" ? "target.utility" : "target.library") + (t.type !== "EXECUTABLE" && p && programOf(p, t) ? ".run" : "");
const md = new vscode.MarkdownString(`**${t.name}** — ${TYPE_LABEL[t.type]}`);
if (t.artifacts.length) md.appendMarkdown(`\n\n${t.artifacts.map((a) => `\`${a}\``).join(" \n")}`);
if (t.definition) md.appendMarkdown(`\n\nDefined in \`${path.relative(p?.sourceDir ?? "", t.definition.file)}:${t.definition.line}\``);
item.tooltip = md;
return item;
}
case "sourceGroup": {
const item = new vscode.TreeItem(node.label, vscode.TreeItemCollapsibleState.Collapsed);
item.id = `group:${node.target.id}:${node.label}`;
item.iconPath = vscode.ThemeIcon.Folder;
item.description = String(node.files.length);
return item;
}
case "source": {
const uri = vscode.Uri.file(node.file);
const item = new vscode.TreeItem(uri, vscode.TreeItemCollapsibleState.None);
item.id = `source:${node.target.id}:${node.file}`;
const rel = path.relative(node.target.sourceDir, node.file);
item.label = rel.startsWith("..") ? path.relative(p?.sourceDir ?? "", node.file) : rel;
item.description = node.generated ? "generated" : undefined;
item.command = { command: "vscode.open", title: "Open", arguments: [uri] };
item.contextValue = "source";
return item;
}
}
}
}
function sources(target: Target, files: string[]): TargetNode[] {
const generated = new Set(target.sources.filter((s) => s.generated).map((s) => s.path));
return [...files]
.sort((a, b) => a.localeCompare(b))
.map((file) => ({ kind: "source", target, file, generated: generated.has(file) }));
}
export interface ProfileNode {
key: string;
}
export class ProfilesProvider implements vscode.TreeDataProvider<ProfileNode> {
private readonly changed = new vscode.EventEmitter<ProfileNode | undefined>();
readonly onDidChangeTreeData = this.changed.event;
constructor(private readonly project: () => CMakeProject | undefined) {}
refresh(): void {
this.changed.fire(undefined);
}
getChildren(node?: ProfileNode): ProfileNode[] {
if (node) return [];
return this.project()?.profiles.map((p) => ({ key: p.key })) ?? [];
}
getTreeItem(node: ProfileNode): vscode.TreeItem {
const project = this.project()!;
const p = project.profiles.find((x) => x.key === node.key)!;
const active = project.activeProfile.key === p.key;
const item = new vscode.TreeItem(p.name);
item.id = p.key;
item.description = [p.buildType, path.relative(project.sourceDir, p.buildDir), p.source === "preset" ? "preset" : ""].filter(Boolean).join(" · ");
item.iconPath = new vscode.ThemeIcon(active ? "pass-filled" : "circle-large-outline", active ? new vscode.ThemeColor("charts.green") : undefined);
item.tooltip = new vscode.MarkdownString(
[`**${p.name}**${p.description ? ` — ${p.description}` : ""}`, `Build type: ${p.buildType}`, `Build directory: \`${p.buildDir}\``, p.generator ? `Generator: ${p.generator}` : "", `\`cmake ${p.configureArgs.join(" ")}\``]
.filter(Boolean)
.join("\n\n"),
);
item.command = { command: "cmakeHelper.selectProfile", title: "Select", arguments: [p.key] };
item.contextValue = p.source === "preset" ? "profile.preset" : p.key.startsWith("default:") ? "profile.default" : "profile.settings";
return item;
}
}