Better handling of newlines in editor insert and delete

This commit is contained in:
Jeremy Soller 2025-10-09 12:21:38 -06:00
parent c5deb38cfe
commit 5cc64c77c1
No known key found for this signature in database
GPG key ID: 670FDFB5428E05CA
5 changed files with 117 additions and 42 deletions

2
ci.sh
View file

@ -36,4 +36,4 @@ echo Build with all features
build --all-features
echo Run tests
cargo test
cargo test --all-features

View file

@ -690,7 +690,15 @@ impl Buffer {
shaping,
));
}
if self.lines.is_empty() {
// Ensure there is an ending line with no line ending
if self
.lines
.last()
.map(|line| line.ending())
.unwrap_or(LineEnding::default())
!= LineEnding::None
{
self.lines.push(BufferLine::new(
"",
LineEnding::None,

View file

@ -159,6 +159,9 @@ impl BufferLine {
let len = self.text.len();
self.text.push_str(other.text());
// To preserve line endings, we use the one from the other line
self.ending = other.ending();
if other.attrs_list.defaults() != self.attrs_list.defaults() {
// If default formatting does not match, make a new span for it
self.attrs_list
@ -181,6 +184,8 @@ impl BufferLine {
self.reset();
let mut new = Self::new(text, self.ending, attrs_list, self.shaping);
// To preserve line endings, it moves to the new line
self.ending = LineEnding::None;
new.align = self.align;
new
}
@ -282,7 +287,7 @@ impl BufferLine {
pub(crate) fn empty() -> Self {
Self {
text: String::default(),
ending: LineEnding::default(),
ending: LineEnding::None,
attrs_list: AttrsList::new(&Attrs::new()),
align: None,
shape_opt: Cached::Empty,

View file

@ -9,14 +9,13 @@ use alloc::{
#[cfg(feature = "swash")]
use std::cmp;
use core::iter::once;
use unicode_segmentation::UnicodeSegmentation;
#[cfg(feature = "swash")]
use crate::Color;
use crate::{
Action, Attrs, AttrsList, BorrowedWithFontSystem, BufferLine, BufferRef, Change, ChangeItem,
Cursor, Edit, FontSystem, LayoutRun, Selection, Shaping,
Cursor, Edit, FontSystem, LayoutRun, LineEnding, LineIter, Selection, Shaping,
};
/// A wrapper of [`Buffer`] for easy editing
@ -300,7 +299,7 @@ impl<'buffer> Edit<'buffer> for Editor<'buffer> {
// Remove end line
let removed = buffer.lines.remove(end.line);
change_lines.insert(0, removed.text().to_string());
change_lines.insert(0, removed);
Some(after)
} else {
@ -310,37 +309,51 @@ impl<'buffer> Edit<'buffer> for Editor<'buffer> {
// Delete interior lines (in reverse for safety)
for line_i in (start.line + 1..end.line).rev() {
let removed = buffer.lines.remove(line_i);
change_lines.insert(0, removed.text().to_string());
change_lines.insert(0, removed);
}
// Delete the selection from the first line
{
let line = &mut buffer.lines[start.line];
// Get part after selection if start line is also end line
let after_opt = if start.line == end.line {
Some(buffer.lines[start.line].split_off(end.index))
Some(line.split_off(end.index))
} else {
None
};
// Delete selected part of line
let removed = buffer.lines[start.line].split_off(start.index);
change_lines.insert(0, removed.text().to_string());
let removed = line.split_off(start.index);
change_lines.insert(0, removed);
// Re-add part of line after selection
if let Some(after) = after_opt {
buffer.lines[start.line].append(&after);
line.append(&after);
}
// Re-add valid parts of end line
if let Some(end_line) = end_line_opt {
buffer.lines[start.line].append(&end_line);
if let Some(mut end_line) = end_line_opt {
// Preserve line ending of original line
end_line.set_ending(line.ending());
line.append(&end_line);
}
}
let mut text = String::new();
let mut last_ending: Option<LineEnding> = None;
for line in change_lines {
if let Some(ending) = last_ending {
text.push_str(ending.as_str());
}
text.push_str(line.text());
last_ending = Some(line.ending());
}
ChangeItem {
start,
end,
text: change_lines.join("\n"),
text,
insert: false,
}
});
@ -367,14 +380,18 @@ impl<'buffer> Edit<'buffer> for Editor<'buffer> {
// Ensure there are enough lines in the buffer to handle this cursor
while cursor.line >= buffer.lines.len() {
let ending = buffer
.lines
.last()
.map(super::super::buffer_line::BufferLine::ending)
.unwrap_or_default();
// Get last line ending
let mut last_ending = LineEnding::None;
if let Some(last_line) = buffer.lines.last_mut() {
last_ending = last_line.ending();
// Ensure a valid line ending is always set on interior lines
if last_ending == LineEnding::None {
last_line.set_ending(LineEnding::default());
}
}
let line = BufferLine::new(
String::new(),
ending,
last_ending,
AttrsList::new(&attrs_list.as_ref().map_or_else(
|| {
buffer
@ -391,7 +408,6 @@ impl<'buffer> Edit<'buffer> for Editor<'buffer> {
let line: &mut BufferLine = &mut buffer.lines[cursor.line];
let insert_line = cursor.line + 1;
let ending = line.ending();
// Collect text after insertion as a line
let after: BufferLine = line.split_off(cursor.index);
@ -403,31 +419,32 @@ impl<'buffer> Edit<'buffer> for Editor<'buffer> {
});
// Append the inserted text, line by line
// we want to see a blank entry if the string ends with a newline
//TODO: adjust this to get line ending from data?
let addendum = once("").filter(|_| data.ends_with('\n'));
let mut lines_iter = data.split_inclusive('\n').chain(addendum);
if let Some(data_line) = lines_iter.next() {
let mut lines: Vec<_> = LineIter::new(&data).collect();
// Ensure there is always an ending line with no line ending
if lines.last().map(|line| line.1).unwrap_or(LineEnding::None) != LineEnding::None {
lines.push((Default::default(), LineEnding::None));
}
let mut lines_iter = lines.into_iter();
// Add first line
if let Some((range, ending)) = lines_iter.next() {
let data_line = &data[range];
let mut these_attrs = final_attrs.split_off(data_line.len());
remaining_split_len -= data_line.len();
remaining_split_len -= data_line.len() + ending.as_str().len();
core::mem::swap(&mut these_attrs, &mut final_attrs);
line.append(&BufferLine::new(
data_line
.strip_suffix(char::is_control)
.unwrap_or(data_line),
data_line,
ending,
these_attrs,
Shaping::Advanced,
));
} else {
panic!("str::lines() did not yield any elements");
}
if let Some(data_line) = lines_iter.next_back() {
remaining_split_len -= data_line.len();
// Add last line
if let Some((range, ending)) = lines_iter.next_back() {
let data_line = &data[range];
remaining_split_len -= data_line.len() + ending.as_str().len();
let mut tmp = BufferLine::new(
data_line
.strip_suffix(char::is_control)
.unwrap_or(data_line),
data_line,
ending,
final_attrs.split_off(remaining_split_len),
Shaping::Advanced,
@ -438,12 +455,12 @@ impl<'buffer> Edit<'buffer> for Editor<'buffer> {
} else {
line.append(&after);
}
for data_line in lines_iter.rev() {
remaining_split_len -= data_line.len();
// Add middle lines
for (range, ending) in lines_iter.rev() {
let data_line = &data[range];
remaining_split_len -= data_line.len() + ending.as_str().len();
let tmp = BufferLine::new(
data_line
.strip_suffix(char::is_control)
.unwrap_or(data_line),
data_line,
ending,
final_attrs.split_off(remaining_split_len),
Shaping::Advanced,

View file

@ -24,6 +24,51 @@ fn editor() -> ViEditor<'static, 'static> {
ViEditor::new(editor)
}
fn editor_text(editor: &ViEditor<'static, 'static>) -> String {
let mut text = String::new();
editor.with_buffer(|buffer| {
for line in buffer.lines.iter() {
text.push_str(line.text());
text.push_str(line.ending().as_str());
}
});
text
}
#[test]
fn editor_line_endings_preserved() {
let mut editor = editor();
assert_eq!(editor_text(&editor), "");
let start = Cursor::new(0, 0);
for &text in &[
"No newlines",
"One Newline\n",
"Two\nNewlines\n",
"LF\nCRLF\r\nCR\rLFCR\n\rNONE",
] {
editor.start_change();
let end = editor.insert_at(start, text, None);
editor.finish_change();
assert_eq!(editor_text(&editor), text);
editor.start_change();
editor.delete_range(start, end);
editor.finish_change();
assert_eq!(editor_text(&editor), "");
editor.start_change();
editor.undo();
editor.finish_change();
assert_eq!(editor_text(&editor), text);
editor.start_change();
editor.redo();
editor.finish_change();
assert_eq!(editor_text(&editor), "");
}
}
// Tests that inserting into an empty editor correctly sets the editor as modified.
#[test]
fn insert_in_empty_editor_sets_changed() {