fix(editor): copy editor text with CRLF line endings on Windows
This commit is contained in:
1 parent
417ce09de1
commit
42a1949bdc
5 files changed
+81
No files matched your search
@@ -104,6 +104,7 @@ import { buildSqlShortcutExecutionSql, enabledSqlShortcutActions, resolveSqlShor
|
||||
import { resolveSqlShortcutTableToken } from "@/lib/sql/sqlShortcutTableTarget";
|
||||
import { normalizeShortcutSettings, shortcutToCodeMirrorKey } from "@/lib/editor/shortcutRegistry";
|
||||
import { trimmedSelectionLayer } from "@/lib/editor/codemirrorTrimmedSelectionLayer";
|
||||
import { editorClipboardLineEndingsExtension } from "@/lib/editor/editorClipboardLineEndings";
|
||||
|
||||
import { selectionMatchOccurrences } from "@/lib/editor/codemirrorSelectionMatches";
|
||||
|
||||
@@ -1841,6 +1842,7 @@ const codeMirrorLifecycle = useQueryEditorCodeMirror({
|
||||
}),
|
||||
sqlBlockFoldService,
|
||||
drawSelection(),
|
||||
editorClipboardLineEndingsExtension(EditorView),
|
||||
trimmedSelectionLayer(),
|
||||
selectionMatchOccurrences(),
|
||||
dropCursor(),
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useToast } from "@/composables/useToast";
|
||||
import { useTheme } from "@/composables/useTheme";
|
||||
import { useSettingsStore } from "@/stores/settingsStore";
|
||||
import { loadEditorTheme, editorFontTheme } from "@/lib/editor/editorThemes";
|
||||
import { editorClipboardLineEndingsExtension } from "@/lib/editor/editorClipboardLineEndings";
|
||||
import { createDbxCodeMirrorSqlDialect } from "@/lib/editor/codemirrorSqlDialect";
|
||||
import { copyToClipboard } from "@/lib/common/clipboard";
|
||||
import { formatSqlForDisplay, type SqlFormatDialect } from "@/lib/sql/sqlFormatter";
|
||||
@@ -208,6 +209,7 @@ async function initDdlEditor(content: string) {
|
||||
},
|
||||
}),
|
||||
basicSetup,
|
||||
editorClipboardLineEndingsExtension(EditorView),
|
||||
EditorState.allowMultipleSelections.of(true),
|
||||
langSql.sql({ dialect }),
|
||||
themeExt,
|
||||
|
||||
@@ -7,6 +7,7 @@ import { defaultKeymap, history, historyKeymap } from "@codemirror/commands";
|
||||
import { bracketMatching, foldGutter, foldKeymap } from "@codemirror/language";
|
||||
import { trimmedSelectionLayer } from "@/lib/editor/codemirrorTrimmedSelectionLayer";
|
||||
import { EDITOR_FONT_FAMILY_CSS_VAR, EDITOR_FONT_SIZE_CSS_VAR, cellDetailActiveLineColor, loadEditorTheme, editorFontTheme } from "@/lib/editor/editorThemes";
|
||||
import { editorClipboardLineEndingsExtension } from "@/lib/editor/editorClipboardLineEndings";
|
||||
import { shortcutToCodeMirrorKey } from "@/lib/editor/shortcutRegistry";
|
||||
import { useSettingsStore } from "@/stores/settingsStore";
|
||||
import { CELL_DETAIL_JSON_FORMAT_MAX_LENGTH, isJsonColumnType } from "@/lib/dataGrid/cellDetailPresentation";
|
||||
@@ -217,6 +218,7 @@ export function useCellDetailEditor(options: UseCellDetailEditorOptions): UseCel
|
||||
highlightSpecialChars(),
|
||||
history(),
|
||||
drawSelection(),
|
||||
editorClipboardLineEndingsExtension(EditorView),
|
||||
trimmedSelectionLayer(),
|
||||
dropCursor(),
|
||||
highlightActiveLine(),
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { strict as assert } from "node:assert";
|
||||
import { afterEach, test, vi } from "vitest";
|
||||
import { EditorState } from "@codemirror/state";
|
||||
import { EditorView } from "@codemirror/view";
|
||||
import { editorClipboardLineEndingsExtension } from "@/lib/editor/editorClipboardLineEndings";
|
||||
|
||||
const SQL = "SELECT 1\nFROM t\nWHERE a = 1;";
|
||||
|
||||
/** Run the document through the filters CodeMirror applies to copied text. */
|
||||
function copiedText(state: EditorState, text: string): string {
|
||||
return state.facet(EditorView.clipboardOutputFilter).reduce((value, filter) => filter(value, state), text);
|
||||
}
|
||||
|
||||
function stateWithExtension(): EditorState {
|
||||
return EditorState.create({ doc: SQL, extensions: [editorClipboardLineEndingsExtension(EditorView)] });
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
function stubPlatform(userAgent: string) {
|
||||
vi.stubGlobal("navigator", { userAgent });
|
||||
}
|
||||
|
||||
test("copied editor text uses CRLF on Windows", () => {
|
||||
stubPlatform("Mozilla/5.0 (Windows NT 10.0; Win64; x64)");
|
||||
assert.equal(copiedText(stateWithExtension(), SQL), "SELECT 1\r\nFROM t\r\nWHERE a = 1;");
|
||||
});
|
||||
|
||||
test("the document itself keeps LF", () => {
|
||||
stubPlatform("Mozilla/5.0 (Windows NT 10.0; Win64; x64)");
|
||||
assert.equal(stateWithExtension().doc.toString(), SQL);
|
||||
});
|
||||
|
||||
test("copied editor text keeps LF on macOS", () => {
|
||||
stubPlatform("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)");
|
||||
assert.equal(copiedText(stateWithExtension(), SQL), SQL);
|
||||
});
|
||||
|
||||
test("single-line text is unchanged on Windows", () => {
|
||||
stubPlatform("Mozilla/5.0 (Windows NT 10.0; Win64; x64)");
|
||||
assert.equal(copiedText(stateWithExtension(), "SELECT 1;"), "SELECT 1;");
|
||||
});
|
||||
|
||||
test("a view without the facet contributes no extension", () => {
|
||||
const state = EditorState.create({ doc: SQL, extensions: [editorClipboardLineEndingsExtension({})] });
|
||||
assert.deepEqual(state.facet(EditorView.clipboardOutputFilter), []);
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
import type { Extension } from "@codemirror/state";
|
||||
import { clipboardLineEndings } from "@/lib/common/clipboard";
|
||||
|
||||
type EditorViewWithClipboardFilter = {
|
||||
clipboardOutputFilter?: { of(filter: (text: string) => string): Extension };
|
||||
};
|
||||
|
||||
/**
|
||||
* Give text copied out of a CodeMirror editor the same line endings as every other DBX
|
||||
* copy path.
|
||||
*
|
||||
* `copyToClipboard` rewrites line endings to CRLF on Windows, but CodeMirror serves
|
||||
* Ctrl+C/Ctrl+X itself and writes `text/plain` straight from the document, which always
|
||||
* uses `\n`. Windows native edit controls (the Win32 EDIT control, and the WinForms apps
|
||||
* built on it) treat only CRLF as a line break, so a multi-line statement copied from an
|
||||
* editor arrives there as a single line.
|
||||
*
|
||||
* `EditorView.clipboardOutputFilter` is CodeMirror's own hook for copy/cut/drag text, so
|
||||
* the document keeps using `\n` and only what leaves the editor is rewritten.
|
||||
*/
|
||||
export function editorClipboardLineEndingsExtension(editorView: EditorViewWithClipboardFilter | null | undefined): Extension {
|
||||
const facet = editorView?.clipboardOutputFilter;
|
||||
// The facet arrived in @codemirror/view 6.32; without it the copy path keeps LF.
|
||||
if (!facet) return [];
|
||||
return facet.of((text) => clipboardLineEndings(text));
|
||||
}
|
||||
Reference in new issue
Block a user