From 607e404ba8eca1abe1ae0e5479891ebe8554ac8a Mon Sep 17 00:00:00 2001 From: Ximiaw Date: Wed, 5 Aug 2026 21:17:56 +0800 Subject: [PATCH 1/4] feat: add run/test buttons to editor title bar --- package.json | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 86b6b0c..e1e4cdf 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,20 @@ }, "main": "./dist/src/extension.js", "contributes": { + "menus": { + "editor/title": [ + { + "command": "mcpp.run", + "group": "navigation@1", + "when": "editorLangId == cpp" + }, + { + "command": "mcpp.test", + "group": "navigation@2", + "when": "editorLangId == cpp" + } + ] + }, "commands": [ { "command": "mcpp.showMenu", @@ -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", From 551072eaec246533efa69f4ce47e85fd10d5f766 Mon Sep 17 00:00:00 2001 From: Ximiaw Date: Thu, 6 Aug 2026 01:04:38 +0800 Subject: [PATCH 2/4] feat: gate editor title buttons behind mcpp project detection --- package.json | 6 +-- src/extension.ts | 8 ++++ src/inProject.ts | 38 +++++++++++++++ test/artifacts.test.ts | 18 ++++++- test/inProject.test.ts | 105 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 170 insertions(+), 5 deletions(-) create mode 100644 src/inProject.ts create mode 100644 test/inProject.test.ts diff --git a/package.json b/package.json index e1e4cdf..b43d185 100644 --- a/package.json +++ b/package.json @@ -54,12 +54,12 @@ { "command": "mcpp.run", "group": "navigation@1", - "when": "editorLangId == cpp" + "when": "mcpp.inProject" }, { "command": "mcpp.test", "group": "navigation@2", - "when": "editorLangId == cpp" + "when": "mcpp.inProject" } ] }, @@ -80,7 +80,7 @@ { "command": "mcpp.test", "title": "mcpp: 测试", - "icon":"$(beaker)" + "icon": "$(beaker)" }, { "command": "mcpp.clean", diff --git a/src/extension.ts b/src/extension.ts index 3749f5f..3ec9356 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -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"; @@ -849,6 +850,13 @@ async function autoConfigureModulesWizard( export async function activate(extensionContext: vscode.ExtensionContext): Promise { 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); diff --git a/src/inProject.ts b/src/inProject.ts new file mode 100644 index 0000000..bcc0e44 --- /dev/null +++ b/src/inProject.ts @@ -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; + setContextValue(key: string, value: boolean): PromiseLike; + createManifestWatcher(): FileSystemWatcherLike; +} + +export async function updateInProjectContext(env: InProjectEnvironment): Promise { + 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(); + } + }, + }; +} diff --git a/test/artifacts.test.ts b/test/artifacts.test.ts index b8a2912..26e0011 100644 --- a/test/artifacts.test.ts +++ b/test/artifacts.test.ts @@ -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 }; configurationDefaults?: Record; languages?: Array<{ id: string; aliases?: string[]; filenames?: string[]; configuration?: string }>; @@ -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, @@ -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 diff --git a/test/inProject.test.ts b/test/inProject.test.ts new file mode 100644 index 0000000..2476d11 --- /dev/null +++ b/test/inProject.test.ts @@ -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; + 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 { + 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); +}); From f2da6bafac656566ffbe0a52f4c99e5ee856057a Mon Sep 17 00:00:00 2001 From: wellwei Date: Thu, 6 Aug 2026 13:13:57 +0800 Subject: [PATCH 3/4] fix: scope editor actions to active mcpp member --- src/discovery.ts | 22 ++++++++++++++- src/extension.ts | 45 +++++++++++++++++------------ src/inProject.ts | 18 ++++-------- test/discovery.test.ts | 36 ++++++++++++++++++++++++ test/inProject.test.ts | 64 ++++++++++++++++++------------------------ 5 files changed, 117 insertions(+), 68 deletions(-) diff --git a/src/discovery.ts b/src/discovery.ts index 837bc9b..e5d0efc 100644 --- a/src/discovery.ts +++ b/src/discovery.ts @@ -18,8 +18,21 @@ function unique(values: string[]): string[] { return values.filter((value, index) => values.indexOf(value) === index); } -export function findNearestMcppProject(startPath: string): McppProjectDiscovery | undefined { +function isPathWithin(candidate: string, root: string): boolean { + const relative = path.relative(root, candidate); + return relative === "" || ( + relative !== ".." + && !relative.startsWith(`..${path.sep}`) + && !path.isAbsolute(relative) + ); +} + +export function findNearestMcppProject( + startPath: string, + workspaceRoot?: string, +): McppProjectDiscovery | undefined { let current = path.resolve(startPath); + const boundary = workspaceRoot === undefined ? undefined : path.resolve(workspaceRoot); try { if (statSync(current).isFile()) { @@ -29,6 +42,10 @@ export function findNearestMcppProject(startPath: string): McppProjectDiscovery // 新创建的 VS Code 工作区路径可能尚不存在,此时按目录处理。 } + if (boundary !== undefined && !isPathWithin(current, boundary)) { + return undefined; + } + while (true) { const manifestPath = path.join(current, "mcpp.toml"); if (existsSync(manifestPath)) { @@ -39,6 +56,9 @@ export function findNearestMcppProject(startPath: string): McppProjectDiscovery }; } + if (boundary !== undefined && current === boundary) { + return undefined; + } const parent = path.dirname(current); if (parent === current) { return undefined; diff --git a/src/extension.ts b/src/extension.ts index 3ec9356..802b186 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -79,16 +79,21 @@ const moduleCheckOperations = createLatestOperationTracker(); let lastReconciledProjectRoot: string | undefined; function findCurrentProject(): McppProjectDiscovery | undefined { - const activePath = vscode.window.activeTextEditor?.document.uri.scheme === "file" - ? vscode.window.activeTextEditor.document.uri.fsPath - : undefined; - const searchPaths = [ - ...(activePath === undefined ? [] : [activePath]), - ...(vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath) ?? []), - ]; - - for (const searchPath of searchPaths) { - const project = findNearestMcppProject(searchPath); + const activeEditor = vscode.window.activeTextEditor; + if (activeEditor !== undefined) { + const activeUri = activeEditor.document.uri; + if (activeUri.scheme !== "file") { + return undefined; + } + const workspaceFolder = vscode.workspace.getWorkspaceFolder(activeUri); + if (workspaceFolder === undefined) { + return undefined; + } + return findNearestMcppProject(activeUri.fsPath, workspaceFolder.uri.fsPath); + } + + for (const workspaceFolder of vscode.workspace.workspaceFolders ?? []) { + const project = findNearestMcppProject(workspaceFolder.uri.fsPath); if (project !== undefined) { return project; } @@ -850,13 +855,6 @@ async function autoConfigureModulesWizard( export async function activate(extensionContext: vscode.ExtensionContext): Promise { 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); @@ -870,8 +868,18 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi void vscode.window.showErrorMessage(`mcpp:${message}`); } }; - const manifestWatcher = vscode.workspace.createFileSystemWatcher("**/mcpp.toml"); + const manifestWatcher = vscode.workspace.createFileSystemWatcher(MCPP_MANIFEST_GLOB); const compilationDatabaseWatcher = vscode.workspace.createFileSystemWatcher("**/compile_commands.json"); + const inProjectContext = registerInProjectContext({ + currentProject: findCurrentProject, + setContextValue: (key, value) => vscode.commands.executeCommand("setContext", key, value), + subscribe: (listener) => [ + vscode.window.onDidChangeActiveTextEditor(listener), + vscode.workspace.onDidChangeWorkspaceFolders(listener), + manifestWatcher.onDidCreate(listener), + manifestWatcher.onDidDelete(listener), + ], + }); const executeWithWorkspaceClangd = createSerialExecutor(); const reconcileProjectContext = async ( project: McppProjectDiscovery | undefined, @@ -1162,6 +1170,7 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi })), configurationWatcher, trustWatcher, + inProjectContext, vscode.window.onDidChangeActiveTextEditor(() => { refreshStatus(); const current = findCurrentProject(); diff --git a/src/inProject.ts b/src/inProject.ts index bcc0e44..7b901e1 100644 --- a/src/inProject.ts +++ b/src/inProject.ts @@ -1,33 +1,25 @@ 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 }; +export interface DisposableLike { dispose(): unknown; } export interface InProjectEnvironment { - findMcppManifests(): PromiseLike; + currentProject(): unknown | undefined; setContextValue(key: string, value: boolean): PromiseLike; - createManifestWatcher(): FileSystemWatcherLike; + subscribe(listener: () => void): readonly DisposableLike[]; } export async function updateInProjectContext(env: InProjectEnvironment): Promise { - const manifests = await env.findMcppManifests(); - const inProject = manifests.length > 0; + const inProject = env.currentProject() !== undefined; 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)), - ]; + const disposables = env.subscribe(() => void updateInProjectContext(env)); return { dispose: () => { for (const disposable of disposables) { diff --git a/test/discovery.test.ts b/test/discovery.test.ts index 6a91d01..2a44bae 100644 --- a/test/discovery.test.ts +++ b/test/discovery.test.ts @@ -24,6 +24,42 @@ test("finds the nearest mcpp manifest and root compilation database", () => { } }); +test("selects the nearest member inside a multi-member workspace", () => { + const root = mkdtempSync(path.join(os.tmpdir(), "mcpp-vscode-members-")); + try { + const memberA = path.join(root, "A"); + const memberB = path.join(root, "B"); + const sourceA = path.join(memberA, "src"); + const sourceB = path.join(memberB, "src"); + mkdirSync(sourceA, { recursive: true }); + mkdirSync(sourceB, { recursive: true }); + writeFileSync(path.join(root, "mcpp.toml"), "[workspace]\nmembers = ['A', 'B']\n"); + writeFileSync(path.join(memberA, "mcpp.toml"), "[package]\nname = 'A'\n"); + writeFileSync(path.join(memberB, "mcpp.toml"), "[package]\nname = 'B'\n"); + + assert.equal(findNearestMcppProject(sourceA, root)?.root, memberA); + assert.equal(findNearestMcppProject(sourceB, root)?.root, memberB); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); + +test("does not discover an mcpp project outside the opened workspace folder", () => { + const root = mkdtempSync(path.join(os.tmpdir(), "mcpp-vscode-workspace-boundary-")); + try { + const openedMember = path.join(root, "A"); + const externalMemberSource = path.join(root, "B", "src"); + mkdirSync(openedMember, { recursive: true }); + mkdirSync(externalMemberSource, { recursive: true }); + writeFileSync(path.join(openedMember, "mcpp.toml"), "[package]\nname = 'A'\n"); + writeFileSync(path.join(root, "B", "mcpp.toml"), "[package]\nname = 'B'\n"); + + assert.equal(findNearestMcppProject(externalMemberSource, openedMember), undefined); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); + test("derives sibling, xlings and PATH clangd candidates", () => { assert.deepEqual( deriveClangdCandidates("/tools/xim-x-llvm/22.1.8/bin/clang++"), diff --git a/test/inProject.test.ts b/test/inProject.test.ts index 2476d11..ba4e942 100644 --- a/test/inProject.test.ts +++ b/test/inProject.test.ts @@ -10,40 +10,33 @@ import { } from "../src/inProject"; interface FakeState { - manifests: unknown[]; + project: unknown | undefined; contextValues: Map; - createListeners: Array<() => void>; - deleteListeners: Array<() => void>; - watcherDisposed: boolean; + changeListeners: Array<() => void>; + disposed: boolean; } -function fakeEnvironment(manifests: unknown[] = []): { env: InProjectEnvironment; state: FakeState } { +function fakeEnvironment(project?: unknown): { env: InProjectEnvironment; state: FakeState } { const state: FakeState = { - manifests: [...manifests], + project, contextValues: new Map(), - createListeners: [], - deleteListeners: [], - watcherDisposed: false, + changeListeners: [], + disposed: false, }; const env: InProjectEnvironment = { - findMcppManifests: () => Promise.resolve(state.manifests), + currentProject: () => state.project, 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; - }, - }), + subscribe: (listener) => { + state.changeListeners.push(listener); + return [{ + dispose: () => { + state.disposed = true; + }, + }]; + }, }; return { env, state }; } @@ -57,47 +50,46 @@ test("exposes the fixed context key and manifest glob", () => { 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"]); +test("sets the context key to true when the active resource resolves to an mcpp project", async () => { + const { env, state } = fakeEnvironment({ root: "/work/A" }); 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 () => { +test("sets the context key to false when the active resource has no mcpp project", 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"]); +test("writes the context on registration and owns the change subscriptions", async () => { + const { env, state } = fakeEnvironment({ root: "/work/A" }); 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); + assert.equal(state.changeListeners.length, 1); registration.dispose(); - assert.equal(state.watcherDisposed, true); + assert.equal(state.disposed, true); }); -test("re-evaluates the context when mcpp.toml is created or deleted", async () => { +test("re-evaluates the context when the active project changes", 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) { + state.project = { root: "/work/A" }; + for (const listener of state.changeListeners) { listener(); } await flushAsync(); assert.equal(state.contextValues.get(IN_PROJECT_CONTEXT_KEY), true); - state.manifests.length = 0; - for (const listener of state.deleteListeners) { + state.project = undefined; + for (const listener of state.changeListeners) { listener(); } await flushAsync(); From 81e3b767a05652b8565be15dc48f220c3aba3e22 Mon Sep 17 00:00:00 2001 From: wellwei Date: Thu, 6 Aug 2026 14:39:45 +0800 Subject: [PATCH 4/4] chore: release mcpp-vscode 0.2.5 --- CHANGELOG.md | 6 ++++++ README.md | 10 +++++----- package-lock.json | 4 ++-- package.json | 2 +- src/cliController.ts | 4 ++-- src/commands.ts | 3 ++- test/artifacts.test.ts | 2 +- test/commands.test.ts | 7 +++++-- 8 files changed, 24 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 17587c8..a51eb06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # 更新日志 +## 0.2.5 + +- 编辑器标题栏的 mcpp 运行/测试操作按当前活动文件所属的 mcpp member 作用域执行;工作区外 + 打开的文件不会显示这些按钮。 +- 将状态栏快捷菜单名称明确为 `$(tools) mcpp: 快捷菜单`,与模块可用性状态按钮区分。 + ## 0.2.4 - 修复 hermetic mcpp 编译数据库被插件追加 `--query-driver` 后导致的 clangd 标准库和 diff --git a/README.md b/README.md index cfc88b6..4e0d263 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ 把 mcpp 工程、C++ 模块语法和官方 clangd 扩展接入 VS Code。 -当前版本为 `0.2.4`。扩展负责工程发现、clangd 配置、模块状态检查以及常用 +当前版本为 `0.2.5`。扩展负责工程发现、clangd 配置、模块状态检查以及常用 mcpp CLI 操作;它不实现新的 C++ 语言服务器,也不替代 mcpp 的构建逻辑。 > 当前完整的模块语义能力只支持 LLVM/Clang 工具链。GCC 和 MSVC 工程仍可使用 @@ -36,7 +36,7 @@ mcpp CLI 操作;它不实现新的 C++ 语言服务器,也不替代 mcpp 的 VSIX,然后在 VS Code 中执行 **Extensions: Install from VSIX...**,或者运行: ```sh -code --install-extension /path/to/mcpp-vscode-0.2.4.vsix +code --install-extension /path/to/mcpp-vscode-0.2.5.vsix ``` 安装后确认当前 VS Code profile 中同时存在 `mcpp-community.mcpp-vscode` 和 @@ -131,7 +131,7 @@ Window。非 LLVM 工具链(GCC/MSVC)项目会得到清晰的引导说明, ### mcpp CLI 与工具链管理 -状态栏中的 `$(tools) mcpp` 菜单提供: +状态栏中的 `$(tools) mcpp: 快捷菜单` 提供: - 在当前工程根目录执行 `mcpp build`、`run`、`test` 和 `clean`。 - 在 VS Code 任务终端中实时显示完整输出。 @@ -381,8 +381,8 @@ API、状态栏、任务和 clangd 集成。 版本完全一致的 tag: ```sh -git tag -a v0.2.4 -m "mcpp-vscode 0.2.4" -git push origin v0.2.4 +git tag -a v0.2.5 -m "mcpp-vscode 0.2.5" +git push origin v0.2.5 ``` `.github/workflows/release.yml` 会校验 tag,执行测试和打包,生成 VSIX 与 SHA-256 文件, diff --git a/package-lock.json b/package-lock.json index 9613a23..7cd3fcc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mcpp-vscode", - "version": "0.2.4", + "version": "0.2.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mcpp-vscode", - "version": "0.2.4", + "version": "0.2.5", "license": "Apache-2.0", "devDependencies": { "@types/node": "^24.0.0", diff --git a/package.json b/package.json index b43d185..e83188c 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "mcpp-vscode", "displayName": "mcpp", "description": "mcpp 与 C++ 模块的 VS Code 集成", - "version": "0.2.4", + "version": "0.2.5", "publisher": "mcpp-community", "license": "Apache-2.0", "icon": "images/logo.png", diff --git a/src/cliController.ts b/src/cliController.ts index e3453df..77766cf 100644 --- a/src/cliController.ts +++ b/src/cliController.ts @@ -22,7 +22,7 @@ import { type ProjectTaskKind, type TaskCompletion, } from "./tasks"; -import { CLI_COMMANDS, quickMenuItems } from "./commands"; +import { CLI_COMMANDS, quickMenuItems, quickMenuStatusText } from "./commands"; export interface McppCliControllerOptions { output: vscode.OutputChannel; @@ -74,7 +74,7 @@ export class McppCliController { public constructor(private readonly options: McppCliControllerOptions) { this.status = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 40); this.status.command = CLI_COMMANDS.showMenu; - this.status.text = "$(tools) mcpp"; + this.status.text = quickMenuStatusText; this.status.tooltip = "打开 mcpp 项目和工具链快捷菜单"; } diff --git a/src/commands.ts b/src/commands.ts index 766b3b9..bd1c8fb 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -10,6 +10,8 @@ export const CLI_COMMANDS = { autoConfigureModules: "mcpp.autoConfigureModules", } as const; +export const quickMenuStatusText = "$(tools) mcpp: 快捷菜单"; + export interface QuickMenuItem { label: string; command: string; @@ -29,4 +31,3 @@ export const quickMenuItems: readonly QuickMenuItem[] = [ { label: "$(check) 检查模块支持", command: "mcpp.checkModuleSupport", group: "ide" }, { label: "$(rocket) 一键配置模块代码提示", command: CLI_COMMANDS.autoConfigureModules, group: "ide" }, ]; - diff --git a/test/artifacts.test.ts b/test/artifacts.test.ts index 26e0011..f2d24cb 100644 --- a/test/artifacts.test.ts +++ b/test/artifacts.test.ts @@ -26,7 +26,7 @@ const root = path.resolve(process.cwd()); test("declares the official clangd dependency and mcpp commands", () => { const manifest = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8")) as PackageManifest; - assert.equal(manifest.version, "0.2.4"); + assert.equal(manifest.version, "0.2.5"); assert.ok(manifest.extensionDependencies?.includes("llvm-vs-code-extensions.vscode-clangd")); assert.ok(manifest.activationEvents?.includes("workspaceContains:mcpp.toml")); assert.ok(manifest.activationEvents?.includes("onCommand:mcpp.run")); diff --git a/test/commands.test.ts b/test/commands.test.ts index 28f329b..6e59248 100644 --- a/test/commands.test.ts +++ b/test/commands.test.ts @@ -1,7 +1,11 @@ import assert from "node:assert/strict"; import test from "node:test"; -import { CLI_COMMANDS, quickMenuItems } from "../src/commands"; +import { CLI_COMMANDS, quickMenuItems, quickMenuStatusText } from "../src/commands"; + +test("状态栏快捷菜单名称与模块状态易于区分", () => { + assert.equal(quickMenuStatusText, "$(tools) mcpp: 快捷菜单"); +}); test("CLI 命令覆盖项目、工具链和 IDE", () => { assert.deepEqual(Object.values(CLI_COMMANDS), [ @@ -33,4 +37,3 @@ test("CLI 命令覆盖项目、工具链和 IDE", () => { ); assert.ok(quickMenuItems.every((item) => item.label.length > 0)); }); -