Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,26 @@ it("keeps unknown dollar names editable when restoring and pasting a draft", asy
expect(serializeEditorDoc(editor.state.doc).value).toBe(value);
});

it.each(["x\r\n\r\ny", "x\r\ry"])(
"pastes %j with the same blank line as x\\n\\ny",
async (text) => {
await using screen = await mountEditor("");
const { editor } = screen;
await act(async () => {
editor.commands.focus("end");
const clipboardData = new DataTransfer();
clipboardData.setData("text/plain", text);
editor.view.dom.dispatchEvent(
new ClipboardEvent("paste", { clipboardData, cancelable: true }),
);
});
const paragraphs: string[] = [];
editor.state.doc.forEach((node) => paragraphs.push(node.textContent));
expect(paragraphs).toEqual(["x", "", "y"]);
expect(serializeEditorDoc(editor.state.doc).value).toBe("x\n\ny");
},
);

it("keeps the caret inside unknown dollar text across rich and plain remounts", async () => {
const value = "echo $HOME now";
await using screen = await mountEditor(value);
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/components/ComposerPromptEditorTiptap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip";
import { ComposerCodeBlockNodeView } from "./chat/ComposerCodeBlockNodeView";
import { resolveAssistantCitationCommentDismissal } from "./chat/assistantCitationCommentDismissal";
import { composerCodeBlockHighlight } from "./composerCodeBlockHighlight";
import { importPastedComposerText } from "./composerInlineTokenPaste";
import { importPastedComposerText, readPastedPlainText } from "./composerInlineTokenPaste";
import { didComposerSelectionChangeVisibly } from "./composerSelection";
import type { ComposerDraftContextRecords } from "./composerContextPresentation";

Expand Down Expand Up @@ -1367,7 +1367,7 @@ function ComposerPromptEditorTiptapInner(
handlePaste: (view, event) => {
const clipboardData = event.clipboardData;
if (!clipboardData || clipboardData.files.length > 0) return false;
const pastedText = clipboardData.getData("text/plain");
const pastedText = readPastedPlainText(clipboardData);
if (!pastedText) return false;
event.preventDefault();
// Paste verbatim inside fences; markdown parsing would split blocks and create unsupported chips.
Expand Down
6 changes: 6 additions & 0 deletions apps/web/src/components/composerInlineTokenPaste.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,10 @@ describe("importPastedComposerText", () => {
pasted,
);
});

it("imports CRLF and CR clipboard text with LF line breaks", () => {
expect(
importPastedComposerText(clipboard({ "text/plain": "x\r\n\r\ny\rz" }), () => new Map()),
).toBe("x\n\ny\nz");
});
});
7 changes: 6 additions & 1 deletion apps/web/src/components/composerInlineTokenPaste.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ import { readThreadShell } from "~/state/entities";

const threadTitle = (ref: ScopedThreadRef) => readThreadShell(ref)?.title;

// Matches ProseMirror's default clipboard parser, which a custom handlePaste bypasses.
export function readPastedPlainText(clipboardData: Pick<DataTransfer, "getData">): string {
return clipboardData.getData("text/plain").replace(/\r\n?/g, "\n");
}

/** Pasted thread links count as context too, so they become chips like a copied chip does. */
export function readPastedComposerContext(
clipboardData: Pick<DataTransfer, "getData">,
Expand Down Expand Up @@ -65,7 +70,7 @@ export function importPastedComposerText(
fragment: ComposerContextClipboardFragment,
) => ReadonlyMap<string, string>,
): string {
const pastedText = clipboardData.getData("text/plain");
const pastedText = readPastedPlainText(clipboardData);
const fragment = importContextFragment ? readPastedComposerContext(clipboardData) : null;
const rewrittenIds =
fragment && fragment.records.length > 0 ? importContextFragment!(fragment) : null;
Expand Down
Loading