documents: edits read the caret off a CRLF split
CI / platforms (windows-latest) (push) Failing after 10s
CI / site (push) Successful in 22s
CI / crate (push) Failing after 11m47s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s

The editor splits lines on LF only, so End can leave the caret
between CR and LF. The edit runner and table insert now settle the
selection before the CR; the line helpers assert they never see a
split instead of guessing.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-10-14 15:56:17 -04:00
1 parent ce015e7b7b
commit 6b033f3eb0
5 files changed
+52 -11

No files matched your search

+10 -3
View File
@@ -6,7 +6,7 @@ use gpui::{AppContext as _, Context};
use super::{
DocumentEvent, LARGEST_READ, MarkdownDocument,
block::{self, BlockCommand},
block::{self, BlockCommand, settled},
format::{Change, EditRefusal, InlineFormat, toggle},
tables::{self, TableEdit},
};
@@ -109,7 +109,8 @@ impl MarkdownDocument {
fn snapshot(&self, cx: &Context<Self>) -> Snapshot {
let text = self.editor.read(cx).text();
let Some(open) = &self.open else {
return (text.to_string(), 0, self.editor.read(cx).primary().range());
let picked = self.editor.read(cx).primary().range();
return settle((text.to_string(), 0, picked));
};
let input = open.input.read(cx);
let (before, after) = match open.append {
@@ -122,7 +123,7 @@ impl MarkdownDocument {
let picked = input.selection();
let offset = before.len();
let full = format!("{before}{}{after}", input.text());
(full, offset, offset + picked.start..offset + picked.end)
settle((full, offset, offset + picked.start..offset + picked.end))
}
/// Writes `change`: into the open block while it stays inside.
@@ -179,3 +180,9 @@ impl MarkdownDocument {
Ok(())
}
}
/// The editor splits lines on LF only; reads start off a CRLF.
fn settle((full, offset, picked): Snapshot) -> Snapshot {
let picked = settled(&full, picked.start)..settled(&full, picked.end);
(full, offset, picked)
}
+9 -5
View File
@@ -251,19 +251,23 @@ fn path<'a>(text: &str, root: &'a Node, at: usize) -> Vec<&'a Node> {
path
}
/// Whether `at` falls between a CR and its LF.
fn splits(text: &str, at: usize) -> bool {
text[..at].ends_with('\r') && text[at..].starts_with('\n')
}
/// `at`, before the CR when it splits a CRLF.
fn settled(text: &str, at: usize) -> usize {
let split = text[..at].ends_with('\r') && text[at..].starts_with('\n');
if split { at - 1 } else { at }
pub(super) fn settled(text: &str, at: usize) -> usize {
if splits(text, at) { at - 1 } else { at }
}
pub(super) fn line_start(text: &str, at: usize) -> usize {
let at = settled(text, at);
debug_assert!(!splits(text, at), "line_start inside a CRLF");
text[..at].rfind(['\n', '\r']).map_or(0, |at| at + 1)
}
pub(super) fn line_end(text: &str, at: usize) -> usize {
let at = settled(text, at);
debug_assert!(!splits(text, at), "line_end inside a CRLF");
at + text[at..].find(['\n', '\r']).unwrap_or(text.len() - at)
}
+2 -2
View File
@@ -6,7 +6,7 @@ use unicode_width::UnicodeWidthStr;
use super::{
MarkdownDocument,
block::{eol, line_end, line_start, next_line, prev_line},
block::{eol, line_end, line_start, next_line, prev_line, settled},
format::{Change, EditRefusal},
};
use crate::documents::{
@@ -264,7 +264,7 @@ impl MarkdownDocument {
}
self.commit(cx);
let text = self.editor.read(cx).text().to_string();
let at = self.editor.read(cx).primary().range().start;
let at = settled(&text, self.editor.read(cx).primary().range().start);
let (start, end, eol) = (line_start(&text, at), line_end(&text, at), eol(&text));
// A blank line gives way; a written one keeps.
let blank = text[start..end].trim().is_empty();
+30
View File
@@ -355,3 +355,33 @@ fn tidy_in_an_open_table_keeps_it_open(cx: &mut TestAppContext) {
"| A | B |\n| ---- | ---- |\n| 1 | 2 |"
);
}
const CRLF: &str = "Intro\r\nNext\r\n";
/// A code block in CRLF text after `keys`.
fn code_block_after(keys: &str, cx: &mut TestAppContext) -> String {
let (document, cx) = open(CRLF, MarkdownMode::Source, cx);
let editor = document.read_with(cx, |document, _| document.editor().clone());
let focus = editor.read_with(cx, gpui::Focusable::focus_handle);
cx.update(|window, cx| window.focus(&focus, cx));
cx.simulate_keystrokes(keys);
document.update(cx, |document, cx| {
document
.block(crate::documents::BlockCommand::CodeBlock, cx)
.expect("taken")
});
cx.run_until_parked();
text(&document, cx)
}
const FENCE_AFTER_INTRO: &str = "Intro\r\n\r\n```\r\n\r\n```\r\n\r\nNext\r\n";
#[gpui::test]
fn a_block_after_end_in_crlf_text_reads_the_line(cx: &mut TestAppContext) {
assert_eq!(code_block_after("end", cx), FENCE_AFTER_INTRO);
}
#[gpui::test]
fn a_selection_from_inside_a_crlf_reads_the_line(cx: &mut TestAppContext) {
assert_eq!(code_block_after("end shift-right", cx), FENCE_AFTER_INTRO);
}