fix(editor): 右键粘贴含 CRLF 的剪贴板文本不再报选区越界 (#9576)
SQL 编辑器里选中一百多行后,右键“粘贴”从 Excel 复制的表格数据会报 “读取剪贴板失败:Selection points outside of document”,文档内容不变; 先删除文本再粘贴则正常。 右键粘贴按剪贴板原文长度计算光标位置,而 CodeMirror 会把插入文本中的 \r\n、\r 归一化为 \n,实际写入的文本比原文短,光标落到文档末尾之后, dispatch 抛出 RangeError: Selection points outside of document。 - 新增 queryEditorClipboardPasteChange:先归一行尾再按实际插入长度计算光标 - QueryEditor 右键粘贴改用它,选区长度不再影响粘贴是否成功 Co-authored-by: zipg <4047349+zipg@users.noreply.github.com>
This commit is contained in:
1 parent
58fd050a28
commit
ed1881e323
5 files changed
+70
-2
No files matched your search
@@ -182,6 +182,7 @@ import { createDbxCodeMirrorSqlDialect, type CodeMirrorSqlDialectName } from "@/
|
||||
import { sqlSemanticTableNameSpansForSyntaxTree } from "@/lib/editor/codemirrorSqlSemanticHighlight";
|
||||
import { startsQueryEditorRectangularSelection, startsQueryEditorSelectionDrag, usesQueryEditorObjectNavigationModifier } from "@/lib/editor/queryEditorPointerSelection";
|
||||
import { LARGE_PASTE_HISTORY_USER_EVENT, normalizeQueryEditorPasteText, recoverableNativePasteSuffix, shouldRecoverLargeTauriPaste } from "@/lib/editor/queryEditorLargePaste";
|
||||
import { queryEditorClipboardPasteChange } from "@/lib/editor/queryEditorClipboardPaste";
|
||||
import { computePasteCaretResyncTarget } from "@/lib/editor/queryEditorPasteCaretResync";
|
||||
import { queryEditorCommentTokens, queryEditorLineCommentToken, queryEditorWordLanguageData } from "@/lib/editor/queryEditorLineComment";
|
||||
import { createShellLineCommentHighlight } from "@/lib/editor/codemirrorShellLineCommentHighlight";
|
||||
@@ -1910,8 +1911,7 @@ async function pasteClipboardSqlFromContextMenu() {
|
||||
const selection = currentView.state.selection.main;
|
||||
// 粘贴:替换选中内容或在光标处插入
|
||||
currentView.dispatch({
|
||||
changes: { from: selection.from, to: selection.to, insert: text },
|
||||
selection: { anchor: selection.from + text.length, head: selection.from + text.length },
|
||||
...queryEditorClipboardPasteChange(text, selection.from, selection.to),
|
||||
scrollIntoView: true,
|
||||
userEvent: "input.paste",
|
||||
});
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { strict as assert } from "node:assert";
|
||||
import { test } from "vitest";
|
||||
import { EditorState } from "@codemirror/state";
|
||||
import { queryEditorClipboardPasteChange } from "@/lib/editor/queryEditorClipboardPaste";
|
||||
|
||||
const EXCEL_ROWS = "1\tA\r\n2\tB\r\n3\tC";
|
||||
|
||||
test("caret lands at the end of the normalized paste text", () => {
|
||||
const change = queryEditorClipboardPasteChange(EXCEL_ROWS, 10, 20);
|
||||
assert.equal(change.changes.insert, "1\tA\n2\tB\n3\tC");
|
||||
assert.equal(change.changes.from, 10);
|
||||
assert.equal(change.changes.to, 20);
|
||||
assert.deepEqual(change.selection, { anchor: 10 + change.changes.insert.length, head: 10 + change.changes.insert.length });
|
||||
});
|
||||
|
||||
test("pasting CRLF text over a selection that reaches the end of the document keeps the caret in range", () => {
|
||||
const doc = "SELECT 1;\n".repeat(120);
|
||||
const state = EditorState.create({ doc });
|
||||
const transaction = state.update({
|
||||
...queryEditorClipboardPasteChange(EXCEL_ROWS, 0, state.doc.length),
|
||||
scrollIntoView: true,
|
||||
userEvent: "input.paste",
|
||||
});
|
||||
assert.equal(transaction.state.doc.toString(), "1\tA\n2\tB\n3\tC");
|
||||
assert.equal(transaction.state.selection.main.anchor, transaction.state.doc.length);
|
||||
});
|
||||
|
||||
test("raw clipboard length would overshoot the document (the reported RangeError)", () => {
|
||||
const doc = "SELECT 1;\n".repeat(120);
|
||||
const state = EditorState.create({ doc });
|
||||
assert.throws(
|
||||
() =>
|
||||
state.update({
|
||||
changes: { from: 0, to: state.doc.length, insert: EXCEL_ROWS },
|
||||
selection: { anchor: EXCEL_ROWS.length },
|
||||
userEvent: "input.paste",
|
||||
}),
|
||||
/Selection points outside of document/,
|
||||
);
|
||||
});
|
||||
|
||||
test("LF clipboard text keeps the same caret as before", () => {
|
||||
const change = queryEditorClipboardPasteChange("a\nb", 3, 3);
|
||||
assert.deepEqual(change.changes, { from: 3, to: 3, insert: "a\nb" });
|
||||
assert.deepEqual(change.selection, { anchor: 6, head: 6 });
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
import { normalizeQueryEditorPasteText } from "./queryEditorLargePaste";
|
||||
|
||||
export interface QueryEditorPasteChange {
|
||||
changes: { from: number; to: number; insert: string };
|
||||
selection: { anchor: number; head: number };
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the "replace [from, to) with clipboard text" transaction for the query editor.
|
||||
*
|
||||
* CodeMirror normalizes the line endings of inserted text (`\r\n` and `\r` become `\n`), so a
|
||||
* caret derived from the raw clipboard length overshoots the new document whenever the clipboard
|
||||
* holds CRLF text (Excel rows, Windows editors) and `dispatch` throws
|
||||
* `RangeError: Selection points outside of document` -- it only survives while the replaced
|
||||
* selection leaves at least that many characters behind it. Deriving the caret from the text
|
||||
* that actually lands in the document keeps the paste working for any selection.
|
||||
*/
|
||||
export function queryEditorClipboardPasteChange(rawText: string, from: number, to: number): QueryEditorPasteChange {
|
||||
const insert = normalizeQueryEditorPasteText(rawText);
|
||||
const caret = from + insert.length;
|
||||
return { changes: { from, to, insert }, selection: { anchor: caret, head: caret } };
|
||||
}
|
||||
Reference in new issue
Block a user