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
84 changes: 78 additions & 6 deletions src/analysis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ export interface CompilationDatabaseAnalysis {
directory?: string;
arguments?: string[];
hasPrebuiltModules?: boolean;
modulePcmSourceDirectories?: string[];
modulePcmConsumerDirectories?: string[];
reason: string;
}

Expand Down Expand Up @@ -56,6 +58,13 @@ interface CompilationCommand {
command?: unknown;
}

interface CompilationCandidate extends CompilationDatabaseAnalysis {
moduleInterface: boolean;
projectSource: boolean;
moduleCompatibilityKey: string;
prebuiltModuleDirectory?: string;
}

function unavailable(reason: string): CompilationDatabaseAnalysis {
return {
kind: "unknown",
Expand Down Expand Up @@ -145,6 +154,37 @@ function compilerKind(compilerPath: string): CompilerKind {
return "unknown";
}

function prebuiltModuleDirectory(arguments_: readonly string[]): string | undefined {
const flag = arguments_.find((argument) => argument.startsWith("-fprebuilt-module-path="));
if (flag === undefined) {
return undefined;
}
const value = flag.slice("-fprebuilt-module-path=".length);
return value.length >= 2 && value.startsWith('"') && value.endsWith('"')
? value.slice(1, -1)
: value;
}

function moduleCompatibilityKey(arguments_: readonly string[]): string {
const result: string[] = [];
for (let index = 1; index < arguments_.length; index += 1) {
const argument = arguments_[index];
if (argument === "-c" || argument === "-o" || argument === "-I") {
index += 1;
continue;
}
if (
argument.startsWith("-I")
|| argument.startsWith("-fmodule-file=")
|| argument.startsWith("-fprebuilt-module-path=")
) {
continue;
}
result.push(argument);
}
return result.join("\0");
}

export function analyzeCompilationDatabase(contents: string): CompilationDatabaseAnalysis {
let parsed: unknown;
try {
Expand All @@ -157,7 +197,7 @@ export function analyzeCompilationDatabase(contents: string): CompilationDatabas
return unavailable("compile_commands.json 至少需要包含一条编译命令");
}

const candidates: CompilationDatabaseAnalysis[] = [];
const candidates: CompilationCandidate[] = [];

for (const value of parsed) {
if (value === null || typeof value !== "object") {
Expand All @@ -183,6 +223,10 @@ export function analyzeCompilationDatabase(contents: string): CompilationDatabas
const sourceFile = typeof command.file === "string"
? resolveCompilationSourceFile(directory, command.file)
: undefined;
const moduleInterface = /\.(?:cppm|ixx|mpp|ccm)$/i.test(sourceFile ?? "");
const projectSource = directory !== undefined
&& sourceFile !== undefined
&& isProjectSource(directory, sourceFile);
candidates.push({
kind,
capability: kind === "llvm" ? "full" : "syntax-only",
Expand All @@ -191,6 +235,10 @@ export function analyzeCompilationDatabase(contents: string): CompilationDatabas
directory,
arguments: args,
hasPrebuiltModules,
moduleInterface,
projectSource,
moduleCompatibilityKey: moduleCompatibilityKey(args),
prebuiltModuleDirectory: prebuiltModuleDirectory(args),
reason: kind === "llvm"
? "Clang 编译命令可以由 clangd 使用"
: `${kind.toUpperCase()} 模块产物不能由 clangd 使用`,
Expand All @@ -201,17 +249,41 @@ export function analyzeCompilationDatabase(contents: string): CompilationDatabas
return unavailable("compile_commands.json 不包含受支持的编译器命令");
}

const score = (candidate: CompilationDatabaseAnalysis): number => {
const score = (candidate: CompilationCandidate): number => {
const sourceFile = candidate.sourceFile ?? "";
const moduleInterface = /\.(?:cppm|ixx|mpp|ccm)$/i.test(sourceFile);
const inProject = candidate.directory !== undefined && isWithinDirectory(candidate.directory, sourceFile);
return (inProject ? 200 : 0)
+ (candidate.directory !== undefined && isProjectSource(candidate.directory, sourceFile) ? 200 : 0)
+ (moduleInterface ? 100 : 0)
+ (candidate.projectSource ? 200 : 0)
+ (candidate.moduleInterface ? 0 : 100)
+ (candidate.hasPrebuiltModules ? 10 : 0);
};

return candidates.reduce((best, candidate) => (score(candidate) > score(best) ? candidate : best));
const selected = candidates.reduce(
(best, candidate) => (score(candidate) > score(best) ? candidate : best),
);
const uniqueDirectories = (moduleInterface: boolean): string[] => [...new Set(
candidates
.filter((candidate) => (
candidate.kind === "llvm"
&& candidate.projectSource
&& candidate.moduleInterface === moduleInterface
&& candidate.compilerPath === selected.compilerPath
&& candidate.moduleCompatibilityKey === selected.moduleCompatibilityKey
))
.flatMap((candidate) => candidate.prebuiltModuleDirectory ?? []),
)];
const {
moduleInterface: _moduleInterface,
projectSource: _projectSource,
moduleCompatibilityKey: _moduleCompatibilityKey,
prebuiltModuleDirectory: _prebuiltModuleDirectory,
...analysis
} = selected;
return {
...analysis,
modulePcmSourceDirectories: uniqueDirectories(true),
modulePcmConsumerDirectories: uniqueDirectories(false),
};
}

function isWithinDirectory(directory: string, file: string): boolean {
Expand Down
25 changes: 25 additions & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ import {
type ModuleSetupBlockedReason,
type ModuleSetupStepResult,
} from "./moduleSetup";
import { stageAvailableProjectPcms } from "./pcm";

const COMMAND_CONFIGURE = "mcpp.configureClangd";
const COMMAND_REFRESH = "mcpp.refreshCompilationDatabase";
Expand Down Expand Up @@ -187,6 +188,22 @@ function hasUsableCompilationDatabase(project: McppProjectDiscovery): boolean {
return loadProjectContext(project)?.analysis.capability !== "unavailable";
}

function stageProjectPcms(context: ProjectContext, output: vscode.OutputChannel): boolean {
try {
const copied = stageAvailableProjectPcms(context.analysis);
if (copied > 0) {
appendOutputLine(output, `[PCM] 已为模块消费者暂存 ${copied} 个现有 PCM。`);
}
return copied > 0;
} catch (error) {
appendOutputLine(
output,
`[PCM] 暂存现有 PCM 失败:${error instanceof Error ? error.message : String(error)}`,
);
return false;
}
}

function moduleSetupBlockedMessage(reason: ModuleSetupBlockedReason): string {
switch (reason) {
case "project-toolchain-override":
Expand Down Expand Up @@ -578,6 +595,10 @@ async function runModuleSupportCheck(
return moduleStatus;
}

if (stageProjectPcms(context, output)) {
await restartClangd(output);
}

const checkToken = moduleCheckOperations.begin(context.project.root);
moduleStatusByProject.delete(context.project.root);
if (shouldRenderProjectStatus(findCurrentProject()?.root, context.project.root)) {
Expand Down Expand Up @@ -800,6 +821,7 @@ async function autoConfigureModulesWizard(
return { stage: "reload", state: "failed", detail: "无法重新加载 mcpp 工程。" };
}
currentContext = refreshed;
stageProjectPcms(currentContext, output);
updateStatusBar(status, currentContext);
if (currentContext.analysis.capability !== "full" || currentContext.analysis.kind !== "llvm") {
return {
Expand Down Expand Up @@ -1032,6 +1054,9 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi
}
}

if (workspaceAllowsToolExecution(vscode.workspace.isTrusted)) {
forceRestart ||= stageProjectPcms(context, output);
}
updateStatusBar(status, context);

const configured = await configureClangd(
Expand Down
44 changes: 44 additions & 0 deletions src/pcm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from "node:fs";
import path from "node:path";
import process from "node:process";

import type { CompilationDatabaseAnalysis } from "./analysis";

function sameDirectory(left: string, right: string): boolean {
const normalize = (value: string): string => {
const resolved = path.resolve(value);
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
};
return normalize(left) === normalize(right);
}

export function stageAvailableProjectPcms(analysis: CompilationDatabaseAnalysis): number {
const sources = analysis.modulePcmSourceDirectories ?? [];
const destinations = analysis.modulePcmConsumerDirectories ?? [];
let copied = 0;

for (const sourceDirectory of sources) {
if (!existsSync(sourceDirectory)) {
continue;
}
const pcms = readdirSync(sourceDirectory, { withFileTypes: true })
.filter((entry) => entry.isFile() && entry.name.toLowerCase().endsWith(".pcm"));
for (const destinationDirectory of destinations) {
if (sameDirectory(sourceDirectory, destinationDirectory) || pcms.length === 0) {
continue;
}
mkdirSync(destinationDirectory, { recursive: true });
for (const pcm of pcms) {
const source = path.join(sourceDirectory, pcm.name);
const destination = path.join(destinationDirectory, pcm.name);
if (existsSync(destination) && statSync(source).mtimeMs <= statSync(destination).mtimeMs) {
continue;
}
copyFileSync(source, destination);
copied += 1;
}
}
}

return copied;
}
45 changes: 45 additions & 0 deletions test/analysis.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,51 @@ test("prefers a project module interface over an earlier external command", () =
assert.equal(result.hasPrebuiltModules, true);
});

test("checks a project module consumer and records its separate PCM directory", () => {
const result = analyzeCompilationDatabase(JSON.stringify([
{
directory: "/work/app",
file: "/work/app/src/demo.cppm",
arguments: [
"/tools/clang++",
"-std=c++23",
"-O0",
"-fprebuilt-module-path=/work/app/target/build/pcm.cache",
"-c",
"/work/app/src/demo.cppm",
],
},
{
directory: "/work/app",
file: "/work/app/tests/demo_test.cpp",
arguments: [
"/tools/clang++",
"-std=c++23",
"-O0",
"-fprebuilt-module-path=/work/app/target/test/pcm.cache",
"-c",
"/work/app/tests/demo_test.cpp",
],
},
{
directory: "/work/app",
file: "/work/app/tests/release_test.cpp",
arguments: [
"/tools/clang++",
"-std=c++23",
"-O2",
"-fprebuilt-module-path=/work/app/target/release/pcm.cache",
"-c",
"/work/app/tests/release_test.cpp",
],
},
]));

assert.equal(result.sourceFile, "/work/app/tests/demo_test.cpp");
assert.deepEqual(result.modulePcmSourceDirectories, ["/work/app/target/build/pcm.cache"]);
assert.deepEqual(result.modulePcmConsumerDirectories, ["/work/app/target/test/pcm.cache"]);
});

test("prefers a project source over an external module interface", () => {
const result = analyzeCompilationDatabase(JSON.stringify([
{
Expand Down
9 changes: 9 additions & 0 deletions test/artifacts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,15 @@ test("IDE commands expose progress and bound clangd restart waits", () => {
assert.match(source, /output\.show\(true\)/);
});

test("module checks restore available PCM before invoking clangd", () => {
const source = readFileSync(path.join(root, "src/extension.ts"), "utf8");
const start = source.indexOf("async function runModuleSupportCheck");
const end = source.indexOf("async function updateModuleSupportForContext", start);
const method = source.slice(start, end);
assert.ok(method.indexOf("stageProjectPcms(context, output)") < method.indexOf("runClangdCheck("));
assert.match(method, /stageProjectPcms\(context, output\)[\s\S]*await restartClangd\(output\)/);
});

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
54 changes: 54 additions & 0 deletions test/pcm.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import assert from "node:assert/strict";
import {
existsSync,
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
utimesSync,
writeFileSync,
} from "node:fs";
import os from "node:os";
import path from "node:path";
import test from "node:test";

import { stageAvailableProjectPcms } from "../src/pcm";

test("stages missing and newer module PCMs for a consumer", () => {
const root = mkdtempSync(path.join(os.tmpdir(), "mcpp-vscode-pcm-"));
const source = path.join(root, "build", "pcm.cache");
const destination = path.join(root, "test", "pcm.cache");
try {
mkdirSync(source, { recursive: true });
mkdirSync(destination, { recursive: true });
writeFileSync(path.join(source, "mcpplibs.demo.pcm"), "module");
writeFileSync(path.join(source, "std.pcm"), "std");
writeFileSync(path.join(destination, "std.pcm"), "existing");

const copied = stageAvailableProjectPcms({
kind: "llvm",
capability: "full",
reason: "test",
modulePcmSourceDirectories: [source],
modulePcmConsumerDirectories: [destination],
});

assert.equal(copied, 1);
assert.equal(existsSync(path.join(destination, "mcpplibs.demo.pcm")), true);
assert.equal(readFileSync(path.join(destination, "std.pcm"), "utf8"), "existing");

writeFileSync(path.join(source, "std.pcm"), "updated");
const newer = new Date(Date.now() + 1_000);
utimesSync(path.join(source, "std.pcm"), newer, newer);
assert.equal(stageAvailableProjectPcms({
kind: "llvm",
capability: "full",
reason: "test",
modulePcmSourceDirectories: [source],
modulePcmConsumerDirectories: [destination],
}), 1);
assert.equal(readFileSync(path.join(destination, "std.pcm"), "utf8"), "updated");
} finally {
rmSync(root, { recursive: true, force: true });
}
});