fix(clipboard): write CRLF line endings on Windows copies

This commit is contained in:
Thanadon Kongkanun authored and GitHub committed 2026-09-14 01:48:42 +08:00
1 parent 1b6ff2ba54
commit 78f9740366
4 files changed
+68 -6

No files matched your search

+43 -2
View File
@@ -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");
});
});
+18
View File
@@ -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");