mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-06 08:07:49 +00:00
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.
This commit is contained in:
@@ -252,6 +252,8 @@ pub struct SmartKeysSettings {
|
||||
/// Reformat on paste: `none` | `indentBlock` | `indentEachLine`.
|
||||
pub reformat_on_paste: String,
|
||||
pub smart_indent: bool,
|
||||
/// Enter › Insert documentation comment stub: Enter after `/**` closes the comment.
|
||||
pub insert_doc_stub: bool,
|
||||
/// Backspace › Unindent: `autoindent` (proper indent position) | `nearest` indent position | `none`.
|
||||
pub backspace_unindent: String,
|
||||
}
|
||||
@@ -267,6 +269,7 @@ impl Default for SmartKeysSettings {
|
||||
tab_out_of_pair: true,
|
||||
reformat_on_paste: "indentEachLine".into(),
|
||||
smart_indent: true,
|
||||
insert_doc_stub: true,
|
||||
backspace_unindent: "autoindent".into(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6870,6 +6870,8 @@ export type SmartKeysSettings = {
|
||||
/** Reformat on paste: `none` | `indentBlock` | `indentEachLine`. */
|
||||
reformatOnPaste?: string,
|
||||
smartIndent?: boolean,
|
||||
/** Enter › Insert documentation comment stub: Enter after `/**` closes the comment. */
|
||||
insertDocStub?: boolean,
|
||||
/** Backspace › Unindent: `autoindent` (proper indent position) | `nearest` indent position | `none`. */
|
||||
backspaceUnindent?: string,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { EditorSelection, EditorState } from "@codemirror/state";
|
||||
import { EditorView } from "@codemirror/view";
|
||||
import { undo, history } from "@codemirror/commands";
|
||||
import { enterInDocComment, phpDocTags } from "./docComment";
|
||||
|
||||
const views: EditorView[] = [];
|
||||
afterEach(() => views.splice(0).forEach((v) => v.destroy()));
|
||||
|
||||
const slashStar = EditorState.languageData.of(() => [
|
||||
{ commentTokens: { line: "//", block: { open: "/*", close: "*/" } } },
|
||||
]);
|
||||
const hash = EditorState.languageData.of(() => [{ commentTokens: { line: "#" } }]);
|
||||
|
||||
/** `|` marks the caret. */
|
||||
function enter(text: string, language = "php", tokens = slashStar) {
|
||||
const at = text.indexOf("|");
|
||||
const view = new EditorView({
|
||||
state: EditorState.create({
|
||||
doc: text.replace("|", ""),
|
||||
selection: EditorSelection.cursor(at),
|
||||
extensions: [tokens, history()],
|
||||
}),
|
||||
});
|
||||
views.push(view);
|
||||
const handled = enterInDocComment(language)(view);
|
||||
const s = view.state;
|
||||
const caret = s.selection.main.head;
|
||||
return { handled, view, text: s.doc.sliceString(0, caret) + "|" + s.doc.sliceString(caret) };
|
||||
}
|
||||
|
||||
describe("Enter after /**", () => {
|
||||
it("lists a PHP function's parameters and return type, caret on the summary line", () => {
|
||||
const r = enter("/**|\nfunction f(int $a, $b): string {}\n/**\n * old\n */\nfunction g() {}");
|
||||
expect(r.handled).toBe(true);
|
||||
expect(r.text).toBe(
|
||||
"/**\n * |\n * @param int $a\n * @param $b\n * @return string\n */\nfunction f(int $a, $b): string {}\n/**\n * old\n */\nfunction g() {}",
|
||||
);
|
||||
undo(r.view);
|
||||
expect(r.view.state.doc.toString()).toBe(
|
||||
"/**\nfunction f(int $a, $b): string {}\n/**\n * old\n */\nfunction g() {}",
|
||||
);
|
||||
});
|
||||
|
||||
it("uses the indentation of the /** line inside a class", () => {
|
||||
expect(
|
||||
enter("class A {\n /**|\n public static function make(?array $o = [], string ...$rest): ?static {}\n}")
|
||||
.text,
|
||||
).toBe(
|
||||
"class A {\n /**\n * |\n * @param ?array $o\n * @param string ...$rest\n * @return ?static\n */\n public static function make(?array $o = [], string ...$rest): ?static {}\n}",
|
||||
);
|
||||
});
|
||||
|
||||
it("gives constructors and void methods no @return; promoted parameters keep their type", () => {
|
||||
expect(phpDocTags("public function __construct(private int $a, #[Attr] readonly Foo&Bar $b) {}")).toEqual([
|
||||
"@param int $a",
|
||||
"@param Foo&Bar $b",
|
||||
]);
|
||||
expect(phpDocTags("function run(array &$x = array(1, 2), &$y): void {}")).toEqual(["@param array $x", "@param $y"]);
|
||||
expect(phpDocTags("abstract protected function find(int|string $id): ?Model;")).toEqual([
|
||||
"@param int|string $id",
|
||||
"@return ?Model",
|
||||
]);
|
||||
});
|
||||
|
||||
it("closes the comment with an empty summary above anything else, and in other /* */ languages", () => {
|
||||
expect(enter("/**|\nclass A {}").text).toBe("/**\n * |\n */\nclass A {}");
|
||||
expect(enter(" /**|\n\nconst x = 1;", "javascript").text).toBe(" /**\n * |\n */\n\nconst x = 1;");
|
||||
});
|
||||
|
||||
it("only breaks the line in an existing doc block, after other text, or without /* */ comments", () => {
|
||||
expect(enter("/**|\n * old\n */\nfunction f() {}").handled).toBe(false);
|
||||
expect(enter("$a = 1; /**|").handled).toBe(false);
|
||||
expect(enter("/** x|").handled).toBe(false);
|
||||
expect(enter("/**|", "python", hash).handled).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* 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;
|
||||
};
|
||||
}
|
||||
@@ -48,6 +48,7 @@ import {
|
||||
selectHumpRight,
|
||||
} from "./editing";
|
||||
import { enterInLineComment, lineCommentStyle, type LineCommentStyle } from "./comments";
|
||||
import { enterInDocComment } from "./docComment";
|
||||
import { skipClosingQuote, smartBackspace, smartBackspaceApplies, tabOut } from "./smartKeys";
|
||||
|
||||
// --- soft wraps ---------------------------------------------------------------
|
||||
@@ -571,6 +572,7 @@ export function prefsExtension(o: PrefsOptions): Extension {
|
||||
{ key: "Home", run: cursorLineStart, shift: selectLineStart, preventDefault: true },
|
||||
{ mac: "Mod-ArrowLeft", run: cursorLineStart, shift: selectLineStart, preventDefault: true },
|
||||
);
|
||||
if (!o.readOnly && k.insertDocStub) keys.push({ key: "Enter", run: enterInDocComment(o.language) });
|
||||
if (!o.readOnly) keys.push({ key: "Enter", run: enterInLineComment });
|
||||
if (!k.smartIndent) keys.push({ key: "Enter", run: insertNewlineKeepIndent });
|
||||
if (k.backspaceUnindent === "none") keys.push({ key: "Backspace", run: deleteCharBackwardStrict });
|
||||
|
||||
@@ -677,7 +677,10 @@ const SMART_KEYS = editorPage({
|
||||
},
|
||||
{
|
||||
title: "Enter",
|
||||
items: [{ t: "check", key: `${P}.smartKeys.smartIndent`, label: "Smart indent" }],
|
||||
items: [
|
||||
{ t: "check", key: `${P}.smartKeys.smartIndent`, label: "Smart indent" },
|
||||
{ t: "check", key: `${P}.smartKeys.insertDocStub`, label: "Insert documentation comment stub" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Backspace",
|
||||
|
||||
@@ -152,6 +152,7 @@ export const EDITOR_EXTRA_DEFAULTS = {
|
||||
tabOutOfPair: true,
|
||||
reformatOnPaste: "indentEachLine",
|
||||
smartIndent: true,
|
||||
insertDocStub: true,
|
||||
backspaceUnindent: "autoindent",
|
||||
},
|
||||
codeEditing: {
|
||||
|
||||
@@ -2175,6 +2175,7 @@
|
||||
"Smart Step Into": "智能步入",
|
||||
"Smart Step Into failed": "智能步入失败",
|
||||
"Smart indent": "智能缩进",
|
||||
"Insert documentation comment stub": "插入文档注释存根",
|
||||
"Soft": "软",
|
||||
"Soft Wraps": "软换行",
|
||||
"Soft-Wrap": "软换行",
|
||||
|
||||
Reference in New Issue
Block a user