Skip to content

CRLF sources leave a stray \r in <code> and markdown code fences #25

Description

@Gyom03

code.rs:105 and markdown/render.rs:420 both split the source with
split('\n'), so on a CRLF file every row keeps its trailing \r. That row
goes straight into:

crate::text::selectable_text(crate::text::SelectableText::new(
    SharedString::from(line.to_string()),

which means the CR gets painted, selected and copied.

Easy to hit: readFileSync on a CRLF checkout returns CRLF, code?: string is
handed to Rust untouched, and reading a file into <code> is the normal thing
to do with it.

The rest of the code already strips it. from_absolute_spans walks back over
\n and then \r when it works out where each line ends, there's a test called
crlf_line_endings_do_not_leak_into_spans, and diff/mod.rs parses with
.lines(). These two renderers are the odd ones out.

Concretely, on the row // one\r the highlighter reports a span of 0..6 while
the row is 7 bytes, so runs_for_spans falls into its tail branch:

if at < line.len() {
    runs.push(plain(line.len() - at, plain_color));
}

and emits a one-byte run for the CR in the plain text colour.

I fixed it with one helper, next to line_starts:

pub fn source_lines(source: &str) -> impl Iterator<Item = &str> {
    source
        .split('\n')
        .map(|line| line.strip_suffix('\r').unwrap_or(line))
}

and both call sites go through it. I kept split('\n') instead of .lines() on
purpose — .lines() drops the trailing empty row, which would change how many
rows a source ending in a newline paints.

Two tests come with it. The useful one pins both layers to the same source:

let rows: Vec<&str> = source_lines(source).collect();
assert_eq!(document.lines[0][0].range, 0..rows[0].len());

cargo test --lib is green on Windows with them (128 passed). I couldn't do the
end-to-end check — getPaintedText() needs the test renderer, so it needs a
Mac.

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

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions