documents: edits read the caret off a CRLF split
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:
1 parent
ce015e7b7b
commit
6b033f3eb0
5 files changed
+52
-11
No files matched your search
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
Reference in new issue
Block a user