diff --git a/package.json b/package.json index 39fa3d0f..a0e9c724 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,11 @@ }, { "command": "mesonbuild.build", - "title": "Meson: Build" + "title": "Meson: Build", + "icon": { + "dark": "res/build-icon-dark.svg", + "light": "res/build-icon-light.svg" + } }, { "command": "mesonbuild.test", @@ -87,6 +91,36 @@ { "command": "mesonbuild.restartLanguageServer", "title": "Meson: Restart Language Server" + }, + { + "command": "mesonbuild.node.reconfigure", + "title": "Reconfigure", + "icon": { + "dark": "res/meson_32.svg", + "light": "res/meson_32.svg" + } + }, + { + "command": "mesonbuild.node.build", + "title": "Build", + "icon": { + "dark": "res/build-icon-dark.svg", + "light": "res/build-icon-light.svg" + } + }, + { + "command": "mesonbuild.node.clean", + "title": "Clean" + }, + { + "command": "mesonbuild.node.runAll", + "title": "Run all", + "icon": "$(testing-run-all-icon)" + }, + { + "command": "mesonbuild.node.run", + "title": "Run", + "icon": "$(testing-run-icon)" } ], "configuration": { @@ -388,10 +422,65 @@ }, "menus": { "view/item/context": [ + { + "command": "mesonbuild.node.reconfigure", + "when": "view == meson-project && viewItem == meson-projectroot", + "group": "build@0" + }, + { + "command": "mesonbuild.node.build", + "when": "view == meson-project && viewItem == meson-projectroot", + "group": "inline" + }, + { + "command": "mesonbuild.node.build", + "when": "view == meson-project && viewItem == meson-projectroot", + "group": "build@1" + }, + { + "command": "mesonbuild.node.clean", + "when": "view == meson-project && viewItem == meson-projectroot", + "group": "build@2" + }, + { + "command": "mesonbuild.node.build", + "when": "view == meson-project && viewItem == meson-target", + "group": "inline@0" + }, { "command": "mesonbuild.openBuildFile", "when": "view == meson-project && viewItem == meson-target", + "group": "inline@1" + }, + { + "command": "mesonbuild.node.build", + "when": "view == meson-project && viewItem == meson-target", + "group": "build@0" + }, + { + "command": "mesonbuild.openBuildFile", + "when": "view == meson-project && viewItem == meson-target", + "group": "build@1" + }, + { + "command": "mesonbuild.node.runAll", + "when": "view == meson-project && viewItem == meson-test-root", "group": "inline" + }, + { + "command": "mesonbuild.node.runAll", + "when": "view == meson-project && viewItem == meson-test-root", + "group": "run" + }, + { + "command": "mesonbuild.node.run", + "when": "view == meson-project && viewItem == meson-test", + "group": "inline" + }, + { + "command": "mesonbuild.node.run", + "when": "view == meson-project && viewItem == meson-test", + "group": "run" } ], "view/title": [ @@ -405,6 +494,26 @@ { "command": "mesonbuild.openBuildFile", "when": "false" + }, + { + "command": "mesonbuild.node.reconfigure", + "when": "false" + }, + { + "command": "mesonbuild.node.build", + "when": "false" + }, + { + "command": "mesonbuild.node.clean", + "when": "false" + }, + { + "command": "mesonbuild.node.run", + "when": "false" + }, + { + "command": "mesonbuild.node.runAll", + "when": "false" } ] }, diff --git a/res/build-icon-dark.svg b/res/build-icon-dark.svg new file mode 100644 index 00000000..2cd8a0c9 --- /dev/null +++ b/res/build-icon-dark.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/res/build-icon-light.svg b/res/build-icon-light.svg new file mode 100644 index 00000000..79a8a633 --- /dev/null +++ b/res/build-icon-light.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/extension.ts b/src/extension.ts index 7f80ce91..8388cc24 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -21,6 +21,7 @@ import { activateFormatters } from "./formatters"; import { SettingsKey, TaskQuickPickItem } from "./types"; import { createLanguageServerClient } from "./lsp/common"; import { dirname, relative } from "path"; +import { IBuildableNode, IRunnableNode } from "./treeview/nodes/base"; export let extensionPath: string; export let workspaceState: vscode.Memento; @@ -201,6 +202,31 @@ export async function activate(ctx: vscode.ExtensionContext) { }), ); + ctx.subscriptions.push( + vscode.commands.registerCommand("mesonbuild.node.reconfigure", async () => { + runFirstTask("reconfigure"); + }), + ); + + ctx.subscriptions.push( + vscode.commands.registerCommand("mesonbuild.node.build", async (node: IBuildableNode) => node.build()), + ); + + ctx.subscriptions.push( + vscode.commands.registerCommand("mesonbuild.node.clean", async () => { + runFirstTask("clean"); + }), + ); + + // Two commands just to have different icons. + ctx.subscriptions.push( + vscode.commands.registerCommand("mesonbuild.node.runAll", async (node: IRunnableNode) => node.run()), + ); + + ctx.subscriptions.push( + vscode.commands.registerCommand("mesonbuild.node.run", async (node: IRunnableNode) => node.run()), + ); + if (!checkMesonIsConfigured(buildDir)) { let configureOnOpen = configurationChosen || extensionConfiguration(SettingsKey.configureOnOpen); if (configureOnOpen === "ask") { diff --git a/src/tasks.ts b/src/tasks.ts index 6edd3c88..fdf57b38 100644 --- a/src/tasks.ts +++ b/src/tasks.ts @@ -1,7 +1,7 @@ import * as vscode from "vscode"; import { getMesonTargets, getMesonTests, getMesonBenchmarks } from "./introspection"; import { extensionConfiguration, getOutputChannel, getTargetName, getEnvDict } from "./utils"; -import { Test, Target } from "./types"; +import { Test, Target, pseudoAllTarget } from "./types"; import { checkMesonIsConfigured } from "./utils"; import { workspaceState } from "./extension"; @@ -68,29 +68,29 @@ function createReconfigureTask(buildDir: string, sourceDir: string) { export async function getMesonTasks(buildDir: string, sourceDir: string) { try { const defaultBuildTask = new vscode.Task( - { type: "meson", mode: "build" }, + { type: "meson", mode: "build", target: pseudoAllTarget }, "Build all targets", "Meson", new vscode.ShellExecution(extensionConfiguration("mesonPath"), ["compile", "-C", buildDir]), "$meson-gcc", ); const defaultTestTask = new vscode.Task( - { type: "meson", mode: "test" }, + { type: "meson", mode: "test", target: pseudoAllTarget }, "Run all tests", "Meson", new vscode.ShellExecution( extensionConfiguration("mesonPath"), - ["test", ...extensionConfiguration("testOptions")], + ["test", ...extensionConfiguration("testOptions"), pseudoAllTarget], { cwd: buildDir }, ), ); const defaultBenchmarkTask = new vscode.Task( - { type: "meson", mode: "benchmark" }, + { type: "meson", mode: "benchmark", target: pseudoAllTarget }, "Run all benchmarks", "Meson", new vscode.ShellExecution( extensionConfiguration("mesonPath"), - ["test", "--benchmark", ...extensionConfiguration("benchmarkOptions")], + ["test", "--benchmark", ...extensionConfiguration("benchmarkOptions"), pseudoAllTarget], { cwd: buildDir }, ), ); diff --git a/src/treeview/nodes/base.ts b/src/treeview/nodes/base.ts index 866c94ac..ae0b6ec1 100644 --- a/src/treeview/nodes/base.ts +++ b/src/treeview/nodes/base.ts @@ -31,3 +31,13 @@ export abstract class BaseDirectoryNode extends BaseNode { abstract buildFileTree(fpaths: T[]): FolderMap | Thenable>; } + +// A node in the meson tree view that can be built. +export interface IBuildableNode { + build(): Thenable; +} + +// A node in the meson tree view that can be run. +export interface IRunnableNode { + run(): Thenable; +} diff --git a/src/treeview/nodes/targets.ts b/src/treeview/nodes/targets.ts index d61eea57..695cd992 100644 --- a/src/treeview/nodes/targets.ts +++ b/src/treeview/nodes/targets.ts @@ -5,7 +5,7 @@ import { BaseNode } from "../basenode"; import { Target, Targets } from "../../types"; import { TargetSourcesRootNode, TargetGeneratedSourcesRootNode } from "./sources"; import { extensionRelative, getTargetName } from "../../utils"; -import { BaseDirectoryNode } from "./base"; +import { BaseDirectoryNode, IBuildableNode } from "./base"; export class TargetDirectoryNode extends BaseDirectoryNode { constructor(parentId: string, folder: string, targets: Targets) { @@ -72,7 +72,7 @@ export class TargetDirectoryNode extends BaseDirectoryNode { } } -export class TargetNode extends BaseNode { +export class TargetNode extends BaseNode implements IBuildableNode { constructor( parentId: string, private readonly target: Target, @@ -103,7 +103,7 @@ export class TargetNode extends BaseNode { } } - override async getTreeItem() { + override getTreeItem() { const item = super.getTreeItem() as vscode.TreeItem; item.label = this.target.name; @@ -112,17 +112,13 @@ export class TargetNode extends BaseNode { item.collapsibleState = vscode.TreeItemCollapsibleState.Collapsed; item.contextValue = "meson-target"; - const targetName = await getTargetName(this.target); - - item.command = { - title: `Build ${this.target.name}`, - command: "mesonbuild.build", - arguments: [targetName], - }; - return item; } + async build() { + return vscode.commands.executeCommand("mesonbuild.build", await getTargetName(this.target)); + } + private getIconPath() { switch (this.target.type) { case "executable": diff --git a/src/treeview/nodes/tests.ts b/src/treeview/nodes/tests.ts index c6ee4bea..d50d0753 100644 --- a/src/treeview/nodes/tests.ts +++ b/src/treeview/nodes/tests.ts @@ -1,16 +1,21 @@ import * as vscode from "vscode"; import { BaseNode } from "../basenode"; -import { Test, Tests } from "../../types"; +import { Test, Tests, pseudoAllTarget } from "../../types"; import { extensionRelative } from "../../utils"; +import { IRunnableNode } from "./base"; -export class TestRootNode extends BaseNode { +function getTestCommand(isBenchmark: boolean): string { + return isBenchmark ? "benchmark" : "test"; +} + +export class TestRootNode extends BaseNode implements IRunnableNode { constructor( parentId: string, private readonly tests: Tests, private readonly isBenchmark: boolean, ) { - super(`${parentId}-${isBenchmark ? "benchmarks" : "tests"}`); + super(`${parentId}-${getTestCommand(isBenchmark)}`); } override getTreeItem() { @@ -21,39 +26,58 @@ export class TestRootNode extends BaseNode { item.collapsibleState = this.tests.length === 0 ? vscode.TreeItemCollapsibleState.None : vscode.TreeItemCollapsibleState.Collapsed; + // To key in to "when": "view == meson-project && viewItem == meson-test-root" in package.json. + item.contextValue = "meson-test-root"; + return item; } override getChildren() { return this.tests.map((test) => new TestNode(this.id, test, this.isBenchmark)); } + + run() { + return vscode.commands.executeCommand(`mesonbuild.${getTestCommand(this.isBenchmark)}`, pseudoAllTarget); + } } -class TestNode extends BaseNode { +class TestNode extends BaseNode implements IRunnableNode { + private readonly taskName: string; + private readonly command: string; + constructor( parentId: string, private readonly test: Test, private readonly isBenchmark: boolean, ) { super(`${parentId}-${test.suite[0]}-${test.name}`); + + this.command = getTestCommand(this.isBenchmark); + const project = this.test.suite[0].split(":")[0]; + this.taskName = `${project}:${this.test.name}`; } override getTreeItem() { const item = super.getTreeItem() as vscode.TreeItem; - const project = this.test.suite[0].split(":")[0]; - const name = `${project}:${this.test.name}`; item.label = this.test.name; item.iconPath = extensionRelative("res/meson_32.svg"); item.command = { - title: `Run ${this.isBenchmark ? "benchmark" : "test"}`, - command: `mesonbuild.${this.isBenchmark ? "benchmark" : "test"}`, - arguments: [name], + title: `Run ${this.command}`, + command: `mesonbuild.${this.command}`, + arguments: [this.taskName], }; // No children currently, so don't display toggle. item.collapsibleState = vscode.TreeItemCollapsibleState.None; + // To key in to "when": "view == meson-project && viewItem == meson-test" in package.json. + item.contextValue = "meson-test"; + return item; } + + run() { + return vscode.commands.executeCommand(`mesonbuild.${this.command}`, this.taskName); + } } diff --git a/src/treeview/nodes/toplevel.ts b/src/treeview/nodes/toplevel.ts index 4290b0b2..96214bdf 100644 --- a/src/treeview/nodes/toplevel.ts +++ b/src/treeview/nodes/toplevel.ts @@ -1,13 +1,14 @@ import * as vscode from "vscode"; import { BaseNode } from "../basenode"; -import { ProjectInfo, Subproject, Targets, Tests } from "../../types"; +import { ProjectInfo, Subproject, Targets, Tests, pseudoAllTarget } from "../../types"; import { extensionRelative } from "../../utils"; import { TargetDirectoryNode } from "./targets"; import { getMesonBenchmarks, getMesonTargets, getMesonTests } from "../../introspection"; import { TestRootNode } from "./tests"; +import { IBuildableNode } from "./base"; -export class ProjectNode extends BaseNode { +export class ProjectNode extends BaseNode implements IBuildableNode { constructor( private readonly project: ProjectInfo, projectDir: string, @@ -26,6 +27,9 @@ export class ProjectNode extends BaseNode { item.iconPath = extensionRelative("res/meson_32.svg"); item.collapsibleState = vscode.TreeItemCollapsibleState.Expanded; + // To key in to "when": "view == meson-project && viewItem == test" in package.json. + item.contextValue = "meson-projectroot"; + return item; } @@ -60,6 +64,10 @@ export class ProjectNode extends BaseNode { return children; } + + build() { + return vscode.commands.executeCommand("mesonbuild.build", pseudoAllTarget); + } } class SubprojectsRootNode extends BaseNode { diff --git a/src/types.ts b/src/types.ts index dd43f0cd..513d5a14 100644 --- a/src/types.ts +++ b/src/types.ts @@ -130,3 +130,5 @@ export enum SettingsKey { languageServer = "languageServer", configureOnOpen = "configureOnOpen", } + +export const pseudoAllTarget = "'*'"; // Quoted for for ShellExecution.