Better handling of newlines in editor insert and delete
This commit is contained in:
parent
c5deb38cfe
commit
5cc64c77c1
5 changed files with 117 additions and 42 deletions
2
ci.sh
2
ci.sh
|
|
@ -36,4 +36,4 @@ echo Build with all features
|
||||||
build --all-features
|
build --all-features
|
||||||
|
|
||||||
echo Run tests
|
echo Run tests
|
||||||
cargo test
|
cargo test --all-features
|
||||||
|
|
|
||||||
|
|
@ -690,7 +690,15 @@ impl Buffer {
|
||||||
shaping,
|
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(
|
self.lines.push(BufferLine::new(
|
||||||
"",
|
"",
|
||||||
LineEnding::None,
|
LineEnding::None,
|
||||||
|
|
|
||||||
|
|
@ -159,6 +159,9 @@ impl BufferLine {
|
||||||
let len = self.text.len();
|
let len = self.text.len();
|
||||||
self.text.push_str(other.text());
|
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 other.attrs_list.defaults() != self.attrs_list.defaults() {
|
||||||
// If default formatting does not match, make a new span for it
|
// If default formatting does not match, make a new span for it
|
||||||
self.attrs_list
|
self.attrs_list
|
||||||
|
|
@ -181,6 +184,8 @@ impl BufferLine {
|
||||||
self.reset();
|
self.reset();
|
||||||
|
|
||||||
let mut new = Self::new(text, self.ending, attrs_list, self.shaping);
|
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.align = self.align;
|
||||||
new
|
new
|
||||||
}
|
}
|
||||||
|
|
@ -282,7 +287,7 @@ impl BufferLine {
|
||||||
pub(crate) fn empty() -> Self {
|
pub(crate) fn empty() -> Self {
|
||||||
Self {
|
Self {
|
||||||
text: String::default(),
|
text: String::default(),
|
||||||
ending: LineEnding::default(),
|
ending: LineEnding::None,
|
||||||
attrs_list: AttrsList::new(&Attrs::new()),
|
attrs_list: AttrsList::new(&Attrs::new()),
|
||||||
align: None,
|
align: None,
|
||||||
shape_opt: Cached::Empty,
|
shape_opt: Cached::Empty,
|
||||||
|
|
|
||||||
|
|
@ -9,14 +9,13 @@ use alloc::{
|
||||||
#[cfg(feature = "swash")]
|
#[cfg(feature = "swash")]
|
||||||
use std::cmp;
|
use std::cmp;
|
||||||
|
|
||||||
use core::iter::once;
|
|
||||||
use unicode_segmentation::UnicodeSegmentation;
|
use unicode_segmentation::UnicodeSegmentation;
|
||||||
|
|
||||||
#[cfg(feature = "swash")]
|
#[cfg(feature = "swash")]
|
||||||
use crate::Color;
|
use crate::Color;
|
||||||
use crate::{
|
use crate::{
|
||||||
Action, Attrs, AttrsList, BorrowedWithFontSystem, BufferLine, BufferRef, Change, ChangeItem,
|
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
|
/// A wrapper of [`Buffer`] for easy editing
|
||||||
|
|
@ -300,7 +299,7 @@ impl<'buffer> Edit<'buffer> for Editor<'buffer> {
|
||||||
|
|
||||||
// Remove end line
|
// Remove end line
|
||||||
let removed = buffer.lines.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)
|
Some(after)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -310,37 +309,51 @@ impl<'buffer> Edit<'buffer> for Editor<'buffer> {
|
||||||
// Delete interior lines (in reverse for safety)
|
// Delete interior lines (in reverse for safety)
|
||||||
for line_i in (start.line + 1..end.line).rev() {
|
for line_i in (start.line + 1..end.line).rev() {
|
||||||
let removed = buffer.lines.remove(line_i);
|
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
|
// 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
|
// Get part after selection if start line is also end line
|
||||||
let after_opt = if start.line == 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 {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
// Delete selected part of line
|
// Delete selected part of line
|
||||||
let removed = buffer.lines[start.line].split_off(start.index);
|
let removed = line.split_off(start.index);
|
||||||
change_lines.insert(0, removed.text().to_string());
|
change_lines.insert(0, removed);
|
||||||
|
|
||||||
// Re-add part of line after selection
|
// Re-add part of line after selection
|
||||||
if let Some(after) = after_opt {
|
if let Some(after) = after_opt {
|
||||||
buffer.lines[start.line].append(&after);
|
line.append(&after);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Re-add valid parts of end line
|
// Re-add valid parts of end line
|
||||||
if let Some(end_line) = end_line_opt {
|
if let Some(mut end_line) = end_line_opt {
|
||||||
buffer.lines[start.line].append(&end_line);
|
// 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 {
|
ChangeItem {
|
||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
text: change_lines.join("\n"),
|
text,
|
||||||
insert: false,
|
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
|
// Ensure there are enough lines in the buffer to handle this cursor
|
||||||
while cursor.line >= buffer.lines.len() {
|
while cursor.line >= buffer.lines.len() {
|
||||||
let ending = buffer
|
// Get last line ending
|
||||||
.lines
|
let mut last_ending = LineEnding::None;
|
||||||
.last()
|
if let Some(last_line) = buffer.lines.last_mut() {
|
||||||
.map(super::super::buffer_line::BufferLine::ending)
|
last_ending = last_line.ending();
|
||||||
.unwrap_or_default();
|
// 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(
|
let line = BufferLine::new(
|
||||||
String::new(),
|
String::new(),
|
||||||
ending,
|
last_ending,
|
||||||
AttrsList::new(&attrs_list.as_ref().map_or_else(
|
AttrsList::new(&attrs_list.as_ref().map_or_else(
|
||||||
|| {
|
|| {
|
||||||
buffer
|
buffer
|
||||||
|
|
@ -391,7 +408,6 @@ impl<'buffer> Edit<'buffer> for Editor<'buffer> {
|
||||||
|
|
||||||
let line: &mut BufferLine = &mut buffer.lines[cursor.line];
|
let line: &mut BufferLine = &mut buffer.lines[cursor.line];
|
||||||
let insert_line = cursor.line + 1;
|
let insert_line = cursor.line + 1;
|
||||||
let ending = line.ending();
|
|
||||||
|
|
||||||
// Collect text after insertion as a line
|
// Collect text after insertion as a line
|
||||||
let after: BufferLine = line.split_off(cursor.index);
|
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
|
// Append the inserted text, line by line
|
||||||
// we want to see a blank entry if the string ends with a newline
|
let mut lines: Vec<_> = LineIter::new(&data).collect();
|
||||||
//TODO: adjust this to get line ending from data?
|
// Ensure there is always an ending line with no line ending
|
||||||
let addendum = once("").filter(|_| data.ends_with('\n'));
|
if lines.last().map(|line| line.1).unwrap_or(LineEnding::None) != LineEnding::None {
|
||||||
let mut lines_iter = data.split_inclusive('\n').chain(addendum);
|
lines.push((Default::default(), LineEnding::None));
|
||||||
if let Some(data_line) = lines_iter.next() {
|
}
|
||||||
|
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());
|
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);
|
core::mem::swap(&mut these_attrs, &mut final_attrs);
|
||||||
line.append(&BufferLine::new(
|
line.append(&BufferLine::new(
|
||||||
data_line
|
data_line,
|
||||||
.strip_suffix(char::is_control)
|
|
||||||
.unwrap_or(data_line),
|
|
||||||
ending,
|
ending,
|
||||||
these_attrs,
|
these_attrs,
|
||||||
Shaping::Advanced,
|
Shaping::Advanced,
|
||||||
));
|
));
|
||||||
} else {
|
|
||||||
panic!("str::lines() did not yield any elements");
|
|
||||||
}
|
}
|
||||||
if let Some(data_line) = lines_iter.next_back() {
|
// Add last line
|
||||||
remaining_split_len -= data_line.len();
|
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(
|
let mut tmp = BufferLine::new(
|
||||||
data_line
|
data_line,
|
||||||
.strip_suffix(char::is_control)
|
|
||||||
.unwrap_or(data_line),
|
|
||||||
ending,
|
ending,
|
||||||
final_attrs.split_off(remaining_split_len),
|
final_attrs.split_off(remaining_split_len),
|
||||||
Shaping::Advanced,
|
Shaping::Advanced,
|
||||||
|
|
@ -438,12 +455,12 @@ impl<'buffer> Edit<'buffer> for Editor<'buffer> {
|
||||||
} else {
|
} else {
|
||||||
line.append(&after);
|
line.append(&after);
|
||||||
}
|
}
|
||||||
for data_line in lines_iter.rev() {
|
// Add middle lines
|
||||||
remaining_split_len -= data_line.len();
|
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(
|
let tmp = BufferLine::new(
|
||||||
data_line
|
data_line,
|
||||||
.strip_suffix(char::is_control)
|
|
||||||
.unwrap_or(data_line),
|
|
||||||
ending,
|
ending,
|
||||||
final_attrs.split_off(remaining_split_len),
|
final_attrs.split_off(remaining_split_len),
|
||||||
Shaping::Advanced,
|
Shaping::Advanced,
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,51 @@ fn editor() -> ViEditor<'static, 'static> {
|
||||||
ViEditor::new(editor)
|
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.
|
// Tests that inserting into an empty editor correctly sets the editor as modified.
|
||||||
#[test]
|
#[test]
|
||||||
fn insert_in_empty_editor_sets_changed() {
|
fn insert_in_empty_editor_sets_changed() {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue