Skip to content
Merged
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# 更新日志

## 0.2.5

- 编辑器标题栏的 mcpp 运行/测试操作按当前活动文件所属的 mcpp member 作用域执行;工作区外
打开的文件不会显示这些按钮。
- 将状态栏快捷菜单名称明确为 `$(tools) mcpp: 快捷菜单`,与模块可用性状态按钮区分。

## 0.2.4

- 修复 hermetic mcpp 编译数据库被插件追加 `--query-driver` 后导致的 clangd 标准库和
Expand Down
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 工程仍可使用
Expand Down Expand Up @@ -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`
Expand Down Expand Up @@ -131,7 +131,7 @@ Window。非 LLVM 工具链(GCC/MSVC)项目会得到清晰的引导说明,

### mcpp CLI 与工具链管理

状态栏中的 `$(tools) mcpp` 菜单提供
状态栏中的 `$(tools) mcpp: 快捷菜单` 提供

- 在当前工程根目录执行 `mcpp build``run``test``clean`
- 在 VS Code 任务终端中实时显示完整输出。
Expand Down Expand Up @@ -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 文件,
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 19 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down 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
4 changes: 2 additions & 2 deletions src/cliController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 项目和工具链快捷菜单";
}

Expand Down
3 changes: 2 additions & 1 deletion src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -29,4 +31,3 @@ export const quickMenuItems: readonly QuickMenuItem[] = [
{ label: "$(check) 检查模块支持", command: "mcpp.checkModuleSupport", group: "ide" },
{ label: "$(rocket) 一键配置模块代码提示", command: CLI_COMMANDS.autoConfigureModules, group: "ide" },
];

22 changes: 21 additions & 1 deletion src/discovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()) {
Expand All @@ -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)) {
Expand All @@ -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;
Expand Down
39 changes: 28 additions & 11 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 @@ -78,16 +79,21 @@ const moduleCheckOperations = createLatestOperationTracker<string>();
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;
}
Expand Down Expand Up @@ -862,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,
Expand Down Expand Up @@ -1154,6 +1170,7 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi
})),
configurationWatcher,
trustWatcher,
inProjectContext,
vscode.window.onDidChangeActiveTextEditor(() => {
refreshStatus();
const current = findCurrentProject();
Expand Down
30 changes: 30 additions & 0 deletions src/inProject.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
export const IN_PROJECT_CONTEXT_KEY = "mcpp.inProject";
export const MCPP_MANIFEST_GLOB = "**/mcpp.toml";

export interface DisposableLike {
dispose(): unknown;
}

export interface InProjectEnvironment {
currentProject(): unknown | undefined;
setContextValue(key: string, value: boolean): PromiseLike<unknown>;
subscribe(listener: () => void): readonly DisposableLike[];
}

export async function updateInProjectContext(env: InProjectEnvironment): Promise<boolean> {
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 disposables = env.subscribe(() => void updateInProjectContext(env));
return {
dispose: () => {
for (const disposable of disposables) {
disposable.dispose();
}
},
};
}
20 changes: 17 additions & 3 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 @@ -25,10 +26,10 @@ 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("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
7 changes: 5 additions & 2 deletions test/commands.test.ts
Original file line number Diff line number Diff line change
@@ -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), [
Expand Down Expand Up @@ -33,4 +37,3 @@ test("CLI 命令覆盖项目、工具链和 IDE", () => {
);
assert.ok(quickMenuItems.every((item) => item.label.length > 0));
});

36 changes: 36 additions & 0 deletions test/discovery.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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++"),
Expand Down
Loading