fix(clipboard): write CRLF line endings on Windows copies
This commit is contained in:
1 parent
1b6ff2ba54
commit
78f9740366
4 files changed
+68
-6
No files matched your search
@@ -48,7 +48,7 @@ import {
|
||||
import { createNacosNamespaceRequestGuard, subscribeNacosNamespacesChanged, type NacosNamespacesChangedDetail } from "@/lib/nacos/nacosNamespaceCache";
|
||||
import { nacosInstanceMatchesPatch, nacosInstanceRefIdentity, nacosIpAddressIsValid, nacosServiceDetailMatches } from "@/lib/nacos/nacosServiceManagement";
|
||||
import { loadReadableNacosNamespaces, nacosNamespaceIdentity } from "@/lib/nacos/nacosNamespaceVisibility";
|
||||
import { copyToClipboard, readTextFromClipboard } from "@/lib/common/clipboard";
|
||||
import { clipboardLineEndings, copyToClipboard, readTextFromClipboard } from "@/lib/common/clipboard";
|
||||
import { trimmedSelectionLayer } from "@/lib/editor/codemirrorTrimmedSelectionLayer";
|
||||
import { safeLocalStorageGet, safeLocalStorageSet } from "@/lib/backend/safeStorage";
|
||||
import { editorFontTheme, loadEditorTheme } from "@/lib/editor/editorThemes";
|
||||
@@ -1188,7 +1188,8 @@ async function copyConfigIdentity() {
|
||||
await copyToClipboard(text);
|
||||
try {
|
||||
const copiedText = await readTextFromClipboard();
|
||||
if (copiedText !== text) {
|
||||
// The clipboard holds the platform-normalized text (CRLF on Windows).
|
||||
if (copiedText !== text && copiedText !== clipboardLineEndings(text)) {
|
||||
throw new Error(t("nacos.copyVerifyFailed"));
|
||||
}
|
||||
} catch (verifyError) {
|
||||
|
||||
@@ -11,7 +11,7 @@ import { useExportTracker } from "@/composables/useExportTracker";
|
||||
import { clipboardCellValue, type CellValue } from "@/lib/dataGrid/cellValue";
|
||||
import { binaryCellClipboardText } from "@/lib/dataGrid/binaryCellDownload";
|
||||
import { tryStartExclusiveActivation, type ActionActivationGuard } from "@/lib/connection/actionActivation";
|
||||
import { copyToClipboard } from "@/lib/common/clipboard";
|
||||
import { clipboardLineEndings, copyToClipboard } from "@/lib/common/clipboard";
|
||||
import { clearDataGridClipboardCopy, rememberDataGridClipboardCopy } from "@/lib/dataGrid/dataGridClipboard";
|
||||
import { buildDataGridCopyInsertStatement, type DataGridCopyInsertMode, type DataGridTableMeta } from "@/lib/dataGrid/dataGridSql";
|
||||
import { formatSqlInsert, formatTsv } from "@/lib/export/exportFormats";
|
||||
@@ -236,7 +236,9 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
|
||||
clearDataGridClipboardCopy();
|
||||
try {
|
||||
await copyToClipboard(text);
|
||||
if (copiedRows) rememberDataGridClipboardCopy(text, copiedRows, copiedHeader);
|
||||
// Remember the text as it now sits on the clipboard, so a paste back into
|
||||
// the grid still matches and keeps its null-cell metadata.
|
||||
if (copiedRows) rememberDataGridClipboardCopy(clipboardLineEndings(text), copiedRows, copiedHeader);
|
||||
toast(t("grid.copied"));
|
||||
return true;
|
||||
} catch (e: any) {
|
||||
|
||||
+43
-2
@@ -1,6 +1,10 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { copyToClipboard, type ClipboardEnvironment } from "@/lib/common/clipboard";
|
||||
import { clipboardLineEndings, copyToClipboard, type ClipboardEnvironment } from "@/lib/common/clipboard";
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
function legacyClipboardEnvironment(activeDialog?: { appendChild: ReturnType<typeof vi.fn>; removeChild: ReturnType<typeof vi.fn> }) {
|
||||
const textarea = {
|
||||
@@ -45,4 +49,41 @@ describe("copyToClipboard", () => {
|
||||
expect(body.appendChild).toHaveBeenCalledWith(textarea);
|
||||
expect(body.removeChild).toHaveBeenCalledWith(textarea);
|
||||
});
|
||||
|
||||
it("writes CRLF line endings on Windows", async () => {
|
||||
vi.spyOn(navigator, "userAgent", "get").mockReturnValue("Mozilla/5.0 (Windows NT 10.0; Win64; x64)");
|
||||
const writeText = vi.fn();
|
||||
|
||||
await copyToClipboard("SELECT 1\nFROM t\nWHERE x = 1", { navigator: { clipboard: { writeText } } });
|
||||
|
||||
expect(writeText).toHaveBeenCalledWith("SELECT 1\r\nFROM t\r\nWHERE x = 1");
|
||||
});
|
||||
|
||||
it("keeps LF line endings off Windows", async () => {
|
||||
vi.spyOn(navigator, "userAgent", "get").mockReturnValue("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)");
|
||||
const writeText = vi.fn();
|
||||
|
||||
await copyToClipboard("SELECT 1\nFROM t", { navigator: { clipboard: { writeText } } });
|
||||
|
||||
expect(writeText).toHaveBeenCalledWith("SELECT 1\nFROM t");
|
||||
});
|
||||
});
|
||||
|
||||
describe("clipboardLineEndings", () => {
|
||||
it("converts bare LF and CR to CRLF on Windows", () => {
|
||||
expect(clipboardLineEndings("a\nb\rc", "windows")).toBe("a\r\nb\r\nc");
|
||||
});
|
||||
|
||||
it("leaves text that already uses CRLF untouched", () => {
|
||||
expect(clipboardLineEndings("a\r\nb\r\n", "windows")).toBe("a\r\nb\r\n");
|
||||
});
|
||||
|
||||
it("leaves single-line text untouched on Windows", () => {
|
||||
expect(clipboardLineEndings("SELECT 1", "windows")).toBe("SELECT 1");
|
||||
});
|
||||
|
||||
it("returns the text unchanged on other platforms", () => {
|
||||
expect(clipboardLineEndings("a\nb", "macos")).toBe("a\nb");
|
||||
expect(clipboardLineEndings("a\nb", "linux")).toBe("a\nb");
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
import { isTauriRuntime } from "@/lib/backend/tauriRuntime";
|
||||
import { getPlatform, type Platform } from "@/lib/backend/platform";
|
||||
|
||||
interface ClipboardApi {
|
||||
readText?: () => Promise<string> | string;
|
||||
@@ -42,6 +43,21 @@ export interface ClipboardEnvironment {
|
||||
document?: ClipboardDocument;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rewrites the line endings of text destined for the system clipboard.
|
||||
*
|
||||
* Windows native edit controls (the Win32 EDIT control, and applications built
|
||||
* on it such as PowerBuilder) only treat CRLF as a line break, so text copied
|
||||
* with bare LF arrives as a single line. Other platforms keep LF.
|
||||
*
|
||||
* Existing CRLF pairs are matched before bare CR and LF so that already
|
||||
* normalized text is left byte-identical.
|
||||
*/
|
||||
export function clipboardLineEndings(text: string, platform: Platform = getPlatform()): string {
|
||||
if (platform !== "windows") return text;
|
||||
return text.replace(/\r\n|\r|\n/g, "\r\n");
|
||||
}
|
||||
|
||||
export interface ClipboardShortcutEvent {
|
||||
key: string;
|
||||
metaKey?: boolean;
|
||||
@@ -136,6 +152,8 @@ export async function readTextFromClipboard(env: ClipboardEnvironment = globalTh
|
||||
}
|
||||
|
||||
export async function copyToClipboard(text: string, env: ClipboardEnvironment = globalThis as unknown as ClipboardEnvironment): Promise<void> {
|
||||
text = clipboardLineEndings(text);
|
||||
|
||||
if (isTauriRuntime(env as unknown as Record<string, unknown>)) {
|
||||
try {
|
||||
const { writeText } = await import("@tauri-apps/plugin-clipboard-manager");
|
||||
|
||||
Reference in new issue
Block a user