Files
openidea/src/editor/core/cm/docComment.ts
T
l0ng-ai 67b5b2def7 feat(editor): insert a documentation comment stub on Enter after /**
Enter after a /** that opens a new comment now closes it with the caret on the
summary line, in every language with /* */ comments. Above a PHP function or
method the stub lists its parameters (@param, with the declared type and
variadic ...) and the declared return type (@return; none for void and
constructors), as PhpStorm does. Settings › Editor › General › Smart Keys ›
Enter › Insert documentation comment stub turns it off.
2026-11-06 01:14:11 +08:00

89 lines
3.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Smart Keys › Enter › Insert documentation comment stub: Enter right after a `/**` that opens a
* new comment closes it, the caret on the summary line. In PHP the stub of a function lists its
* parameters (`@param`) and return type (`@return`), as PhpStorm does.
*/
import { EditorSelection } from "@codemirror/state";
import type { Command } from "@codemirror/view";
/** `@param` / `@return` lines for the PHP function declared in `text` (the code below the `/**`). */
export function phpDocTags(text: string): string[] {
const m =
/^\s*(?:#\[[^\]]*\]\s*)*(?:(?:abstract|final|public|protected|private|static)\s+)*function\s+&?\s*(\w+)\s*\(/.exec(
text,
);
if (!m) return [];
let depth = 1;
let i = m[0].length;
for (; i < text.length && depth > 0; i++) {
const c = text[i];
if (c === "(" || c === "[") depth++;
else if (c === ")" || c === "]") depth--;
}
if (depth > 0) return [];
const tags = splitTopLevel(text.slice(m[0].length, i - 1)).flatMap((param) => {
const p = /^(?:#\[[^\]]*\]\s*)*(?:(?:public|protected|private|readonly)\s+)*([^$=.]*?)\s*(\.\.\.)?\s*(\$\w+)/.exec(
param.trim(),
);
if (!p) return [];
// `&$x` passes by reference: the `&` belongs to no type (`Foo&Bar` is an intersection).
const type = p[1]!.replace(/&\s*$/, "").trim().replace(/\s+/g, " ");
const name = (p[2] ?? "") + p[3];
return [type ? `@param ${type} ${name}` : `@param ${name}`];
});
const ret = /^\s*:\s*([?\w\\|&() ]+?)\s*(?:\{|;|$)/.exec(text.slice(i));
const type = ret?.[1]?.trim();
if (type && type !== "void" && m[1] !== "__construct") tags.push(`@return ${type}`);
return tags;
}
function splitTopLevel(params: string): string[] {
const out: string[] = [];
let depth = 0;
let start = 0;
for (let i = 0; i < params.length; i++) {
const c = params[i];
if (c === "(" || c === "[") depth++;
else if (c === ")" || c === "]") depth--;
else if (c === "," && depth === 0) {
out.push(params.slice(start, i));
start = i + 1;
}
}
out.push(params.slice(start));
return out.filter((p) => p.trim());
}
/** The Enter command for documents of `language` (stub tags in PHP). */
export function enterInDocComment(language: string | undefined): Command {
return (view) => {
const { state } = view;
if (state.readOnly || state.selection.ranges.length !== 1) return false;
const r = state.selection.main;
if (!r.empty) return false;
const block = state.languageDataAt<{ block?: { open: string; close: string } }>("commentTokens", r.head)[0]?.block;
if (block?.open !== "/*" || block.close !== "*/") return false;
const line = state.doc.lineAt(r.head);
const before = line.text.slice(0, r.head - line.from);
if (before.trim() !== "/**" || line.text.slice(r.head - line.from).trim()) return false;
// A `/**` that already starts a doc block (the next line continues it) only breaks the line.
const next = line.number < state.doc.lines ? state.doc.line(line.number + 1).text.trimStart() : "";
if (next.startsWith("*")) return false;
const indent = /^[ \t]*/.exec(before)![0];
const br = state.lineBreak;
const below = line.number < state.doc.lines ? state.sliceDoc(state.doc.line(line.number + 1).from) : "";
const tags = language === "php" ? phpDocTags(below.slice(0, 4000)) : [];
const summary = `${br}${indent} * `;
const insert = summary + tags.map((t) => `${br}${indent} * ${t}`).join("") + `${br}${indent} */`;
view.dispatch(
state.update({
changes: { from: r.head, insert },
selection: EditorSelection.cursor(r.head + summary.length),
scrollIntoView: true,
userEvent: "input",
}),
);
return true;
};
}