Files
openidea/src/editor/lsp/phpConstructs.test.ts
T
l0ng-ai b2c183dbd4 fix(php): document language constructs and clarify rename failures (#354)
PHP language constructs such as empty and isset had no completion documentation, hover or quick documentation because the language server reports them as plain keywords. They now get built-in documentation in completion, hover, quick documentation and Go to Declaration (which shows the docs instead of failing), without matching strings, comments, methods or other languages. A refused syntax-based rename now states why the language server couldn't rename first.

Closes #340
Refs #221
2026-10-30 19:46:08 +08:00

78 lines
2.9 KiB
TypeScript

import { afterEach, describe, expect, it } from "vitest";
import { EditorState } from "@codemirror/state";
import { EditorView } from "@codemirror/view";
import { languages } from "@codemirror/language-data";
import { phpConstructAt, phpConstructDocumentation } from "./phpConstructs";
import { lspDoc } from "./lsp";
import { quickDefinition, requestHover } from "./hover";
import { gotoTargets } from "./navigation";
import { closePopup, useLspPopup } from "./popupStore";
const php = await languages.find((l) => l.name === "PHP")!.load();
const views: EditorView[] = [];
afterEach(() => {
closePopup();
for (const view of views.splice(0)) view.destroy();
});
function state(doc: string) {
return EditorState.create({ doc, extensions: [php] });
}
describe("PHP language constructs", () => {
it.each(["empty", "isset", "EMPTY"])("documents %s at either edge of its name", (name) => {
const s = state(`<?php ${name}($value);`);
for (const pos of [6, 8, 6 + name.length]) {
const doc = phpConstructAt(s, pos, "php");
expect(doc).toMatchObject({ from: 6, to: 6 + name.length });
expect(doc?.markdown).toContain("PHP language construct");
expect(doc?.markdown).toContain(`function.${name.toLowerCase()}.php`);
}
});
it.each([
'<?php $empty = "empty($x)";',
"<?php // empty($x)",
"<?php /* empty($x) */",
"<?php $obj->empty();",
"<?php $obj?->empty();",
"<?php Foo::empty();",
"<?php Foo\\empty();",
"<?php function empty() {}",
"<p>empty($x)</p><?php echo 1;",
])("does not hijack other occurrences: %s", (text) => {
const s = state(text);
for (const match of text.matchAll(/empty/g)) expect(phpConstructAt(s, match.index + 2, "php")).toBeNull();
});
it("does not attach PHP docs to other languages or ordinary functions", () => {
expect(phpConstructAt(state("<?php empty($x);"), 8, "javascript")).toBeNull();
expect(phpConstructDocumentation("PHP", "strtoupper")).toBeNull();
expect(phpConstructDocumentation("php", "constructor")).toBeNull();
});
it("serves hover, declaration and quick definition without a running server", async () => {
const view = new EditorView({
state: EditorState.create({
doc: "<?php empty($value);",
selection: { anchor: 8 },
extensions: [php, lspDoc.of({ doc: 1, path: "/p/a.php", language: "php", uiMode: "PHP", readOnly: false })],
}),
parent: document.body,
});
views.push(view);
expect((await requestHover(view, 8))?.contents.value).toContain("empty(mixed $var): bool");
for (const action of [
() => gotoTargets(view, "definition"),
() => gotoTargets(view, "declaration"),
() => quickDefinition(view),
]) {
await action();
const popup = useLspPopup.getState().popup;
expect(popup?.kind).toBe("doc");
if (popup?.kind === "doc") expect(popup.entries[0]?.markup.value).toContain("no source declaration");
closePopup();
}
});
});