Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,20 @@
},
"main": "./dist/src/extension.js",
"contributes": {
"menus": {
"editor/title": [
{
"command": "mcpp.run",
"group": "navigation@1",
"when": "mcpp.inProject"
},
{
"command": "mcpp.test",
"group": "navigation@2",
"when": "mcpp.inProject"
}
]
},
"commands": [
{
"command": "mcpp.showMenu",
Expand All @@ -60,11 +74,13 @@
},
{
"command": "mcpp.run",
"title": "mcpp: 运行"
"title": "mcpp: 运行",
"icon": "$(play)"
},
{
"command": "mcpp.test",
"title": "mcpp: 测试"
"title": "mcpp: 测试",
"icon": "$(beaker)"
},
{
"command": "mcpp.clean",
Expand Down
8 changes: 8 additions & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import {
type ModuleSupportState,
} from "./workflow";
import { classifyTaskExit, type TaskCompletion } from "./tasks";
import { MCPP_MANIFEST_GLOB, registerInProjectContext } from "./inProject";

const COMMAND_CONFIGURE = "mcpp.configureClangd";
const COMMAND_REFRESH = "mcpp.refreshCompilationDatabase";
Expand Down Expand Up @@ -849,6 +850,13 @@ async function autoConfigureModulesWizard(
export async function activate(extensionContext: vscode.ExtensionContext): Promise<void> {
moduleStatusByProject.clear();
moduleCheckOperations.clear();
extensionContext.subscriptions.push(
registerInProjectContext({
findMcppManifests: () => vscode.workspace.findFiles(MCPP_MANIFEST_GLOB, null, 1),
setContextValue: (key, value) => vscode.commands.executeCommand("setContext", key, value),
createManifestWatcher: () => vscode.workspace.createFileSystemWatcher(MCPP_MANIFEST_GLOB),
}),
);
const output = vscode.window.createOutputChannel("mcpp");
const status = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 50);

Expand Down
38 changes: 38 additions & 0 deletions src/inProject.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
export const IN_PROJECT_CONTEXT_KEY = "mcpp.inProject";
export const MCPP_MANIFEST_GLOB = "**/mcpp.toml";

export interface FileSystemWatcherLike {
onDidCreate(listener: () => void): { dispose(): unknown };
onDidDelete(listener: () => void): { dispose(): unknown };
dispose(): unknown;
}

export interface InProjectEnvironment {
findMcppManifests(): PromiseLike<readonly unknown[]>;
setContextValue(key: string, value: boolean): PromiseLike<unknown>;
createManifestWatcher(): FileSystemWatcherLike;
}

export async function updateInProjectContext(env: InProjectEnvironment): Promise<boolean> {
const manifests = await env.findMcppManifests();
const inProject = manifests.length > 0;
await env.setContextValue(IN_PROJECT_CONTEXT_KEY, inProject);
return inProject;
}

export function registerInProjectContext(env: InProjectEnvironment): { dispose(): unknown } {
void updateInProjectContext(env);
const watcher = env.createManifestWatcher();
const disposables = [
watcher,
watcher.onDidCreate(() => void updateInProjectContext(env)),
watcher.onDidDelete(() => void updateInProjectContext(env)),
];
return {
dispose: () => {
for (const disposable of disposables) {
disposable.dispose();
}
},
};
}
18 changes: 16 additions & 2 deletions test/artifacts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ interface PackageManifest {
activationEvents?: string[];
capabilities?: { untrustedWorkspaces?: { supported?: string; description?: string } };
contributes?: {
commands?: Array<{ command: string }>;
commands?: Array<{ command: string; icon?: string }>;
menus?: { "editor/title"?: Array<{ command: string; group?: string; when?: string }> };
configuration?: { properties?: Record<string, unknown> };
configurationDefaults?: Record<string, unknown>;
languages?: Array<{ id: string; aliases?: string[]; filenames?: string[]; configuration?: string }>;
Expand All @@ -28,7 +29,7 @@ test("declares the official clangd dependency and mcpp commands", () => {
assert.equal(manifest.version, "0.2.4");
assert.ok(manifest.extensionDependencies?.includes("llvm-vs-code-extensions.vscode-clangd"));
assert.ok(manifest.activationEvents?.includes("workspaceContains:mcpp.toml"));
assert.ok(manifest.activationEvents?.includes("onLanguage:mcpp-build"));
assert.ok(manifest.activationEvents?.includes("onCommand:mcpp.run"));
assert.equal(manifest.capabilities?.untrustedWorkspaces?.supported, "limited");
assert.equal(
manifest.capabilities?.untrustedWorkspaces?.description,
Expand Down Expand Up @@ -61,6 +62,19 @@ test("declares the official clangd dependency and mcpp commands", () => {
});
});

test("shows editor title buttons only inside mcpp projects", () => {
const manifest = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8")) as PackageManifest;
assert.deepEqual(manifest.contributes?.menus?.["editor/title"], [
{ command: "mcpp.run", group: "navigation@1", when: "mcpp.inProject" },
{ command: "mcpp.test", group: "navigation@2", when: "mcpp.inProject" },
]);

const commands = manifest.contributes?.commands ?? [];
assert.equal(commands.find((command) => command.command === "mcpp.run")?.icon, "$(play)");
assert.equal(commands.find((command) => command.command === "mcpp.test")?.icon, "$(beaker)");
assert.ok(!commands.some((command) => command.command === "mcpp.inProject"));
});

test("ships syntax-only C++ highlighting for the exact build.mcpp filename", () => {
const manifest = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8")) as PackageManifest;
const associations = manifest.contributes?.configurationDefaults?.["files.associations"] as
Expand Down
105 changes: 105 additions & 0 deletions test/inProject.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import assert from "node:assert/strict";
import test from "node:test";

import {
IN_PROJECT_CONTEXT_KEY,
MCPP_MANIFEST_GLOB,
registerInProjectContext,
updateInProjectContext,
type InProjectEnvironment,
} from "../src/inProject";

interface FakeState {
manifests: unknown[];
contextValues: Map<string, boolean>;
createListeners: Array<() => void>;
deleteListeners: Array<() => void>;
watcherDisposed: boolean;
}

function fakeEnvironment(manifests: unknown[] = []): { env: InProjectEnvironment; state: FakeState } {
const state: FakeState = {
manifests: [...manifests],
contextValues: new Map(),
createListeners: [],
deleteListeners: [],
watcherDisposed: false,
};
const env: InProjectEnvironment = {
findMcppManifests: () => Promise.resolve(state.manifests),
setContextValue: (key, value) => {
state.contextValues.set(key, value);
return Promise.resolve();
},
createManifestWatcher: () => ({
onDidCreate: (listener) => {
state.createListeners.push(listener);
return { dispose: () => undefined };
},
onDidDelete: (listener) => {
state.deleteListeners.push(listener);
return { dispose: () => undefined };
},
dispose: () => {
state.watcherDisposed = true;
},
}),
};
return { env, state };
}

async function flushAsync(): Promise<void> {
await new Promise((resolve) => setImmediate(resolve));
}

test("exposes the fixed context key and manifest glob", () => {
assert.equal(IN_PROJECT_CONTEXT_KEY, "mcpp.inProject");
assert.equal(MCPP_MANIFEST_GLOB, "**/mcpp.toml");
});

test("sets the context key to true when an mcpp.toml exists", async () => {
const { env, state } = fakeEnvironment(["/work/app/mcpp.toml"]);
const result = await updateInProjectContext(env);
assert.equal(result, true);
assert.equal(state.contextValues.get(IN_PROJECT_CONTEXT_KEY), true);
});

test("sets the context key to false when no mcpp.toml exists", async () => {
const { env, state } = fakeEnvironment();
const result = await updateInProjectContext(env);
assert.equal(result, false);
assert.equal(state.contextValues.get(IN_PROJECT_CONTEXT_KEY), false);
});

test("writes the context on registration and owns the watcher lifecycle", async () => {
const { env, state } = fakeEnvironment(["/work/app/mcpp.toml"]);
const registration = registerInProjectContext(env);
await flushAsync();
assert.equal(state.contextValues.get(IN_PROJECT_CONTEXT_KEY), true);
assert.equal(state.createListeners.length, 1);
assert.equal(state.deleteListeners.length, 1);

registration.dispose();
assert.equal(state.watcherDisposed, true);
});

test("re-evaluates the context when mcpp.toml is created or deleted", async () => {
const { env, state } = fakeEnvironment();
registerInProjectContext(env);
await flushAsync();
assert.equal(state.contextValues.get(IN_PROJECT_CONTEXT_KEY), false);

state.manifests.push("/work/app/mcpp.toml");
for (const listener of state.createListeners) {
listener();
}
await flushAsync();
assert.equal(state.contextValues.get(IN_PROJECT_CONTEXT_KEY), true);

state.manifests.length = 0;
for (const listener of state.deleteListeners) {
listener();
}
await flushAsync();
assert.equal(state.contextValues.get(IN_PROJECT_CONTEXT_KEY), false);
});