Skip to content

Multi-line Editor preserves CRLF carriage returns from Windows clipboard #2877

Description

@yuexiliuli

Multi-line Editor Preserves CRLF Carriage Returns From Windows Clipboard

Summary

gpui-component multi-line EditorState preserves \r from Windows CRLF clipboard text. This can make the first N-1 pasted lines render as blank or disappear visually, while the underlying text is still present.

The final line usually renders normally because it has no trailing \r.

Environment

  • Windows
  • Clipboard source: VS Code
  • gpui = { git = "https://github.com/zed-industries/zed" }
    gpui-component = { git = "https://github.com/longbridge/gpui-component" }
    

Reproduction

  1. Create a multi-line gpui_component::input::Editor.

  2. Paste the following text from VS Code:

    use std::cell::RefCell;
    use std::collections::HashMap;
    use std::ops::Range;
    use std::rc::Rc;
  3. Select and copy the first line.

  4. Move the caret to the end of that first line.

  5. Paste three times with Ctrl+V.

Actual Behavior

  • The first line, and generally all pasted lines except the final logical line, can become visually blank.
  • Typing on those lines also appears blank.
  • Ctrl+A, then Ctrl+C, then pasting into another application shows the text is still present.
  • The copied text reveals that the content was inserted as separate lines.
  • If five lines are initially pasted, the first four can fail while the last line renders normally.
  • Copying through Notepad++ avoids the issue, likely because it converts clipboard line endings to LF.

Expected Behavior

All pasted lines should remain visible and editable, independent of whether the clipboard source uses LF or CRLF.

Root Cause

Windows clipboard content from VS Code uses CRLF:

line1\r\nline2\r\nline3\r\nline4

The multi-line input path in gpui-component preserves \r.

The relevant flow is:

Paste
-> replace_text_in_range_silent
-> replace_text_in_range
-> normalize_input

In crates/base/src/input/base/state.rs, normalize_input currently removes \r and \n only for single-line inputs. Multi-line editors retain \r.

As a result, \r is treated as a normal shaped character at the end of every line except the last. This produces the predictable "first N-1 lines fail, final line works" behavior.

Proposed Fix

Normalize all multi-line input to LF before inserting text:

fn normalize_input<'a>(&self, new_text: &'a str) -> Cow<'a, str> {
    let normalized = if matches!(self.mask_pattern, MaskPattern::Number { .. }) {
        normalize_number_input(new_text)
    } else {
        Cow::Borrowed(new_text)
    };

    if self.is_single_line() && normalized.contains(['\n', '\r']) {
        Cow::Owned(normalized.replace(['\n', '\r'], ""))
    } else if self.is_multi_line() && normalized.contains('\r') {
        Cow::Owned(normalized.replace("\r\n", "\n").replace('\r', "\n"))
    } else {
        normalized
    }
}

This preserves the internal LF-only line model and prevents \r from reaching shaping, layout, cursor hit testing, and display-map code.

Application Workaround

As a temporary workaround, the application can override the component paste action after gpui_component::init(cx):

gpui_component::init(cx);

cx.bind_keys([KeyBinding::new(
    "ctrl-v",
    PasteNormalized,
    Some("Input"),
)]);

The custom action reads clipboard text, converts CRLF/CR to LF, then calls EditorState::replace.

The order is important: registering before gpui_component::init(cx) does not work because component initialization installs its default key bindings afterward.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions