diff --git a/ci.sh b/ci.sh index 1ebce39..02a2894 100755 --- a/ci.sh +++ b/ci.sh @@ -36,4 +36,4 @@ echo Build with all features build --all-features echo Run tests -cargo test +cargo test --all-features diff --git a/src/buffer.rs b/src/buffer.rs index 312707f..2ded307 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -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, diff --git a/src/buffer_line.rs b/src/buffer_line.rs index cec5077..d8689fd 100644 --- a/src/buffer_line.rs +++ b/src/buffer_line.rs @@ -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, diff --git a/src/edit/editor.rs b/src/edit/editor.rs index f0f5152..f86fd4d 100644 --- a/src/edit/editor.rs +++ b/src/edit/editor.rs @@ -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 = 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, diff --git a/tests/editor_modified_state.rs b/tests/editor_modified_state.rs index 01ac008..b3f9e07 100644 --- a/tests/editor_modified_state.rs +++ b/tests/editor_modified_state.rs @@ -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() {