mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-07 17:22:55 -10:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff3d6213a4 |
No files matched your search
@@ -3,8 +3,10 @@ import {
|
||||
caretAfterReformat,
|
||||
expandIncludes,
|
||||
fragmentResolver,
|
||||
composerPsr4,
|
||||
inferGoPackage,
|
||||
inferJvmPackage,
|
||||
inferPhpNamespace,
|
||||
planFile,
|
||||
reformatsOnCreate,
|
||||
renderFileTemplate,
|
||||
@@ -92,6 +94,58 @@ describe("file naming", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("PHP namespaces", () => {
|
||||
const psr4 = composerPsr4(
|
||||
JSON.stringify({
|
||||
autoload: { "psr-4": { "App\\": "src/", "Legacy\\": ["src/legacy/", "lib"] } },
|
||||
"autoload-dev": { "psr-4": { "App\\Tests\\": "./tests" } },
|
||||
}),
|
||||
);
|
||||
|
||||
it("reads autoload and autoload-dev PSR-4 maps", () => {
|
||||
expect(psr4).toEqual([
|
||||
["App\\", "src"],
|
||||
["Legacy\\", "src/legacy"],
|
||||
["Legacy\\", "lib"],
|
||||
["App\\Tests\\", "tests"],
|
||||
]);
|
||||
expect(composerPsr4("{ not json")).toEqual([]);
|
||||
});
|
||||
|
||||
it("uses the longest mapped folder and adds the folders below it", () => {
|
||||
expect(inferPhpNamespace("/src/Http", psr4)).toBe("App\\Http");
|
||||
expect(inferPhpNamespace("/src", psr4)).toBe("App");
|
||||
expect(inferPhpNamespace("/src/legacy/Billing", psr4)).toBe("Legacy\\Billing");
|
||||
expect(inferPhpNamespace("/tests/Service", psr4)).toBe("App\\Tests\\Service");
|
||||
expect(inferPhpNamespace("/config", psr4)).toBe("");
|
||||
expect(inferPhpNamespace("/src/my-dir", psr4)).toBe("");
|
||||
expect(inferPhpNamespace("/x", [["", ""]])).toBe("x");
|
||||
});
|
||||
|
||||
it("writes the namespace into PHP classes, and leaves it out without one", () => {
|
||||
const kinds = tpl("php-class").kinds;
|
||||
const cls = renderFileTemplate(kinds[0]!.body, {
|
||||
name: "UserController",
|
||||
fileName: "UserController.php",
|
||||
packageName: "App\\Http",
|
||||
});
|
||||
expect(cls.text).toBe("<?php\n\nnamespace App\\Http;\n\nclass UserController\n{\n \n}\n");
|
||||
const bare = renderFileTemplate(kinds[0]!.body, { name: "Foo", fileName: "Foo.php" });
|
||||
expect(bare.text).toBe("<?php\n\nclass Foo\n{\n \n}\n");
|
||||
const test = renderFileTemplate(kinds.find((k) => k.id === "test")!.body, {
|
||||
name: "MailerTest",
|
||||
fileName: "MailerTest.php",
|
||||
packageName: "App\\Tests\\Service",
|
||||
});
|
||||
expect(test.text).toBe(
|
||||
"<?php\n\nnamespace App\\Tests\\Service;\n\nuse PHPUnit\\Framework\\TestCase;\n\nfinal class MailerTest extends TestCase\n{\n public function testSomething(): void\n {\n \n }\n}\n",
|
||||
);
|
||||
expect(renderFileTemplate(tpl("php").kinds[0]!.body, { name: "bootstrap", fileName: "bootstrap.php" }).text).toBe(
|
||||
"<?php\n\n\n",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("rendering", () => {
|
||||
it("substitutes variables and places the caret", () => {
|
||||
const kind = tpl("java").kinds[0]!;
|
||||
|
||||
@@ -233,7 +233,11 @@ function NameStep({
|
||||
{error ?? (
|
||||
<>
|
||||
{where}
|
||||
{template.packageStyle && pkg ? ` · package ${pkg}` : ""}
|
||||
{template.packageStyle && pkg
|
||||
? template.packageStyle === "php"
|
||||
? ` · namespace ${pkg}`
|
||||
: ` · package ${pkg}`
|
||||
: ""}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -31,6 +31,11 @@ export const BUILTIN_CODE_TEMPLATES: readonly TemplateFragment[] = [
|
||||
description: "TypeScript File › Test",
|
||||
body: 'it("works", () => {\n $END$\n});\n',
|
||||
},
|
||||
{
|
||||
name: "PHPUnit Test Method",
|
||||
description: "PHP Class › PHPUnit Test",
|
||||
body: "public function testSomething(): void\n{\n $END$\n}\n",
|
||||
},
|
||||
{
|
||||
name: "pytest Test Function",
|
||||
description: "Python File › Test (pytest)",
|
||||
@@ -149,6 +154,49 @@ export const BUILTIN_FILE_TEMPLATES: readonly FileTemplate[] = [
|
||||
defaultName: "go",
|
||||
kinds: [{ id: "module", label: "Module", body: "module $PROJECT_NAME$\n\ngo 1.20\n" }],
|
||||
},
|
||||
{
|
||||
id: "php",
|
||||
name: "PHP File",
|
||||
icon: "file-php",
|
||||
extension: "php",
|
||||
nameStyle: "asis",
|
||||
kinds: [{ id: "file", label: "File", body: "<?php\n\n$END$\n" }],
|
||||
},
|
||||
{
|
||||
id: "php-class",
|
||||
name: "PHP Class",
|
||||
icon: "class",
|
||||
extension: "php",
|
||||
nameStyle: "pascal",
|
||||
packageStyle: "php",
|
||||
kinds: [
|
||||
{
|
||||
id: "class",
|
||||
label: "Class",
|
||||
body: '<?php\n\nnamespace $PACKAGE$;\n\n#parse("File Header")\nclass $NAME$\n{\n $END$\n}\n',
|
||||
},
|
||||
{
|
||||
id: "interface",
|
||||
label: "Interface",
|
||||
body: '<?php\n\nnamespace $PACKAGE$;\n\n#parse("File Header")\ninterface $NAME$\n{\n $END$\n}\n',
|
||||
},
|
||||
{
|
||||
id: "trait",
|
||||
label: "Trait",
|
||||
body: '<?php\n\nnamespace $PACKAGE$;\n\n#parse("File Header")\ntrait $NAME$\n{\n $END$\n}\n',
|
||||
},
|
||||
{
|
||||
id: "enum",
|
||||
label: "Enum",
|
||||
body: '<?php\n\nnamespace $PACKAGE$;\n\n#parse("File Header")\nenum $NAME$\n{\n $END$\n}\n',
|
||||
},
|
||||
{
|
||||
id: "test",
|
||||
label: "PHPUnit Test",
|
||||
body: '<?php\n\nnamespace $PACKAGE$;\n\nuse PHPUnit\\Framework\\TestCase;\n\n#parse("File Header")\nfinal class $NAME$ extends TestCase\n{\n #parse("PHPUnit Test Method")\n}\n',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "java",
|
||||
name: "Java Class",
|
||||
|
||||
@@ -10,8 +10,10 @@ import { editorService, maybeEditorService, notifications } from "@/workbench/co
|
||||
import {
|
||||
GO_PACKAGE_RE,
|
||||
caretAfterReformat,
|
||||
composerPsr4,
|
||||
inferGoPackage,
|
||||
inferJvmPackage,
|
||||
inferPhpNamespace,
|
||||
planFile,
|
||||
reformatsOnCreate,
|
||||
renderFileTemplate,
|
||||
@@ -46,10 +48,29 @@ function packageRoots(): string[] {
|
||||
.map((r) => r.path);
|
||||
}
|
||||
|
||||
/** The namespace from the nearest composer.json between `directory` and the project root (PSR-4). */
|
||||
async function composerNamespace(directory: string, root: string | null): Promise<string> {
|
||||
if (!isTauri()) return "";
|
||||
let dir = directory.replace(/\/+$/, "");
|
||||
const top = root?.replace(/\/+$/, "") ?? null;
|
||||
for (;;) {
|
||||
let text: string | null = null;
|
||||
try {
|
||||
text = await call(commands.fsReadText(`${dir}/composer.json`));
|
||||
} catch {
|
||||
/* no composer.json here */
|
||||
}
|
||||
if (text !== null) return inferPhpNamespace(directory.slice(dir.length), composerPsr4(text));
|
||||
if (!top || dir === top || !dir.startsWith(`${top}/`)) return "";
|
||||
dir = dirname(dir);
|
||||
}
|
||||
}
|
||||
|
||||
export async function packageFor(t: FileTemplate, directory: string): Promise<string> {
|
||||
const root = useProject.getState().project?.root ?? null;
|
||||
if (t.packageStyle === "jvm") return inferJvmPackage(directory, root, packageRoots());
|
||||
if (t.packageStyle === "go") return inferGoPackage(directory, root, await siblingGoPackage(directory));
|
||||
if (t.packageStyle === "php") return composerNamespace(directory, root);
|
||||
return "";
|
||||
}
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ export interface FileTemplate {
|
||||
extension: string;
|
||||
nameStyle: NameStyle;
|
||||
/** Package inference: `jvm` (Java/Kotlin source roots) or `go` (directory). */
|
||||
packageStyle?: "jvm" | "go";
|
||||
packageStyle?: "jvm" | "go" | "php";
|
||||
/** Dotted names (`com.acme.Foo`) create package directories. */
|
||||
dottedPackages?: boolean;
|
||||
kinds: FileTemplateKind[];
|
||||
@@ -223,6 +223,47 @@ export function inferGoPackage(directory: string, projectRoot: string | null, si
|
||||
|
||||
export const GO_PACKAGE_RE = /^\s*package\s+([A-Za-z_]\w*)/m;
|
||||
|
||||
/** `autoload.psr-4` and `autoload-dev.psr-4` of a composer.json: `[namespace prefix, folder]` (folder relative to it). */
|
||||
export function composerPsr4(json: string): Array<[string, string]> {
|
||||
let data: unknown;
|
||||
try {
|
||||
data = JSON.parse(json);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
const out: Array<[string, string]> = [];
|
||||
for (const section of ["autoload", "autoload-dev"]) {
|
||||
const map = (data as Record<string, { "psr-4"?: Record<string, unknown> } | undefined> | null)?.[section]?.[
|
||||
"psr-4"
|
||||
];
|
||||
if (!map || typeof map !== "object") continue;
|
||||
for (const [prefix, value] of Object.entries(map)) {
|
||||
for (const dir of Array.isArray(value) ? value : [value])
|
||||
if (typeof dir === "string")
|
||||
out.push([prefix, dir.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "")]);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* PHP namespace for `directory` (relative to the composer.json folder): the PSR-4 prefix of the
|
||||
* longest mapped folder that contains it, then the folders below it (`App\\` + `src/` → `src/Http`
|
||||
* gives `App\Http`). Outside every mapping: no namespace.
|
||||
*/
|
||||
export function inferPhpNamespace(directory: string, psr4: ReadonlyArray<readonly [string, string]>): string {
|
||||
const rel = directory.split("/").filter(Boolean);
|
||||
let best: { prefix: string; depth: number } | null = null;
|
||||
for (const [prefix, dir] of psr4) {
|
||||
const segs = dir.split("/").filter((s) => s && s !== ".");
|
||||
if (segs.length > rel.length || segs.some((s, i) => s !== rel[i])) continue;
|
||||
if (!best || segs.length > best.depth) best = { prefix, depth: segs.length };
|
||||
}
|
||||
if (!best) return "";
|
||||
const parts = [...best.prefix.split("\\").filter(Boolean), ...rel.slice(best.depth)];
|
||||
return parts.every((p) => /^[A-Za-z_\x80-\uffff][\w\x80-\uffff]*$/.test(p)) ? parts.join("\\") : "";
|
||||
}
|
||||
|
||||
export interface FileTemplateVars {
|
||||
name: string;
|
||||
fileName: string;
|
||||
|
||||
@@ -1153,6 +1153,7 @@
|
||||
"Issue navigation links": "问题导航链接",
|
||||
"Italic": "斜体",
|
||||
"JUnit 5 Test": "JUnit 5 测试",
|
||||
"PHPUnit Test": "PHPUnit 测试",
|
||||
"JUnit test": "JUnit 测试",
|
||||
"JVM options": "JVM 选项",
|
||||
"Jump outside closing bracket/quote with Tab": "使用 Tab 跳出右括号/引号",
|
||||
@@ -3334,6 +3335,7 @@
|
||||
"Rust File › Test module": "Rust 文件 › 测试模块",
|
||||
"TypeScript File › Test": "TypeScript 文件 › 测试",
|
||||
"Python File › Test (pytest)": "Python 文件 › 测试 (pytest)",
|
||||
"PHP Class › PHPUnit Test": "PHP 类 › PHPUnit 测试",
|
||||
"CSS File": "CSS 文件",
|
||||
"SCSS File": "SCSS 文件",
|
||||
"Sass File": "Sass 文件",
|
||||
|
||||
Reference in new issue
Block a user