mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-08 08:52:12 +00:00
fix(editor): cover edge cases of multiline paste with multiple cursors (#1853)
dispatchPaste spread the clipboard only when the line count equaled the caret count and pasted the whole block at every caret otherwise. Follow IntelliJ's ClipboardTextPerCaretSplitter instead: caret i gets line i, extra carets get an empty insert, lines beyond the caret count are dropped, and a single line repeats at every caret unchanged (a lone trailing newline stays a single line). Closes #1828 Signed-off-by: Inf1mum <[email protected]> Co-authored-by: Inf1mum <[email protected]>
This commit is contained in:
1 parent
c08657ba17
commit
0ac3f82dfb
2 files changed
+58
-7
No files matched your search
@@ -256,13 +256,45 @@ describe("multi-caret paste", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("pastes the whole text at every caret when the counts differ", () => {
|
||||
it("drops the extra lines when there are more lines than carets (IDEA parity)", () => {
|
||||
// IntelliJ's ClipboardTextPerCaretSplitter gives caret i line i and simply
|
||||
// ignores the lines beyond the caret count: 2 carets + "x\ny\nz" → "x", "y".
|
||||
const v = view("a b", [
|
||||
[1, 1],
|
||||
[3, 3],
|
||||
]);
|
||||
pasteEditorText(v, "x\ny\nz");
|
||||
expect(v.state.doc.toString()).toBe("ax\ny\nz bx\ny\nz");
|
||||
expect(v.state.doc.toString()).toBe("ax by");
|
||||
});
|
||||
|
||||
it("pastes empty lines into the extra carets when there are more carets than lines", () => {
|
||||
const v = view("a b c", [
|
||||
[1, 1],
|
||||
[3, 3],
|
||||
[5, 5],
|
||||
]);
|
||||
pasteEditorText(v, "x\ny");
|
||||
expect(v.state.doc.toString()).toBe("ax by c");
|
||||
});
|
||||
|
||||
it("repeats a single line at every caret", () => {
|
||||
const v = view("a b", [
|
||||
[1, 1],
|
||||
[3, 3],
|
||||
]);
|
||||
pasteEditorText(v, "xyz");
|
||||
expect(v.state.doc.toString()).toBe("axyz bxyz");
|
||||
});
|
||||
|
||||
it("treats a single copied line with a trailing newline as one line (IDEA parity)", () => {
|
||||
// IntelliJ's splitter ignores the trailing newline of a one-line clipboard,
|
||||
// so "x\n" at two carets repeats "x" instead of spreading "x" and "".
|
||||
const v = view("a b", [
|
||||
[1, 1],
|
||||
[3, 3],
|
||||
]);
|
||||
pasteEditorText(v, "x\n");
|
||||
expect(v.state.doc.toString()).toBe("ax bx");
|
||||
});
|
||||
|
||||
it("pastes the whole text at a single caret", () => {
|
||||
|
||||
@@ -15,16 +15,19 @@ function clipboardFileFormatter(view: EditorView): ((paths: string[]) => string)
|
||||
|
||||
/**
|
||||
* The dispatch shared by every paste entry point. With several carets and a
|
||||
* clipboard of exactly one line per caret, each caret gets its own line
|
||||
* (JetBrains behavior): `"a\nb"` at two carets inserts `a` then `b`, not the
|
||||
* block twice. Any other count pastes the full text at every caret.
|
||||
* multi-line clipboard, the lines spread across the carets the way IntelliJ
|
||||
* does (its ClipboardTextPerCaretSplitter): caret *i* gets line *i*, missing
|
||||
* lines become empty inserts, and lines beyond the caret count are dropped —
|
||||
* `"a\nb"` at two carets inserts `a` then `b`, not the block twice, while a
|
||||
* single line (a trailing newline doesn't make it multi-line) repeats at every
|
||||
* caret without the newline.
|
||||
* `userEvent` is `"input.paste"` unless the caller passes another (plain-text
|
||||
* paste opts out of Reformat on Paste through `"input.pastePlain"`).
|
||||
*/
|
||||
export function dispatchPaste(view: EditorView, text: string, userEvent = "input.paste") {
|
||||
const ranges = view.state.selection.ranges;
|
||||
const lines = text.replace(/\r\n?/g, "\n").split("\n");
|
||||
if (ranges.length > 1 && lines.length === ranges.length) {
|
||||
const lines = pasteLinesPerCaret(text, ranges.length);
|
||||
if (lines) {
|
||||
let i = 0;
|
||||
const spec = view.state.changeByRange((range) => {
|
||||
const insert = lines[i++]!;
|
||||
@@ -39,6 +42,22 @@ export function dispatchPaste(view: EditorView, text: string, userEvent = "input
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The per-caret inserts for `text` at `caretCount` carets, mirroring IntelliJ's
|
||||
* ClipboardTextPerCaretSplitter: line *i* for caret *i*, `""` for the carets
|
||||
* left over when there are more carets than lines, and the lines beyond the
|
||||
* caret count dropped. A clipboard of a single line — a lone trailing newline
|
||||
* is an artifact of copying, not an extra empty line — repeats at every caret
|
||||
* without its newline. `undefined` keeps the single-caret fallback.
|
||||
*/
|
||||
function pasteLinesPerCaret(text: string, caretCount: number): string[] | undefined {
|
||||
if (caretCount <= 1) return undefined;
|
||||
const lines = text.replace(/\r\n?/g, "\n").split("\n");
|
||||
if (lines.length === 2 && lines[1] === "") lines.pop();
|
||||
if (lines.length === 1) return Array(caretCount).fill(lines[0]!);
|
||||
return Array.from({ length: caretCount }, (_, i) => lines[i] ?? "");
|
||||
}
|
||||
|
||||
export function pasteEditorText(view: EditorView, text: string) {
|
||||
if (!text || !view.dom.isConnected || view.state.readOnly) return;
|
||||
dispatchPaste(view, text);
|
||||
|
||||
Reference in new issue
Block a user