Add line ending abstraction (#250)

* Add line ending abstraction

* Make Buffer::set_text use LineIter

* Add ctrl+s for saving to editor
This commit is contained in:
Jeremy Soller 2024-04-30 12:12:25 -06:00 committed by GitHub
parent ff5501d9a3
commit 0cfd9b64ef
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 194 additions and 15 deletions

View file

@ -1,3 +0,0 @@
# SPDX-License-Identifier: MIT OR Apache-2.0
RUST_LOG="cosmic_text=debug,editor_orbclient=debug" cargo run --release --package editor-orbclient -- "$@"

3
editor.sh Executable file
View file

@ -0,0 +1,3 @@
# SPDX-License-Identifier: MIT OR Apache-2.0
RUST_LOG="cosmic_text=debug,editor=debug" cargo run --release --package editor -- "$@"

View file

@ -4,7 +4,7 @@ use cosmic_text::{
Action, Attrs, Buffer, Edit, Family, FontSystem, Metrics, Motion, SwashCache, SyntaxEditor, Action, Attrs, Buffer, Edit, Family, FontSystem, Metrics, Motion, SwashCache, SyntaxEditor,
SyntaxSystem, SyntaxSystem,
}; };
use std::{env, num::NonZeroU32, rc::Rc, slice}; use std::{env, fs, num::NonZeroU32, rc::Rc, slice};
use tiny_skia::{Paint, PixmapMut, Rect, Transform}; use tiny_skia::{Paint, PixmapMut, Rect, Transform};
use winit::{ use winit::{
dpi::PhysicalPosition, dpi::PhysicalPosition,
@ -248,6 +248,17 @@ fn main() {
}); });
} }
} }
"s" => {
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());
}
});
fs::write(&path, &text).unwrap();
log::info!("saved {:?}", path);
}
_ => {} _ => {}
} }
} else { } else {

3
sample/crlf.txt Normal file
View file

@ -0,0 +1,3 @@
These are two lines
in a CRLF file

5
sample/tabs.txt Normal file
View file

@ -0,0 +1,5 @@
Tabs:
One Two Three Four
Two Three Four
Three Four
Four

View file

@ -7,8 +7,8 @@ use unicode_segmentation::UnicodeSegmentation;
use crate::{ use crate::{
Affinity, Attrs, AttrsList, BidiParagraphs, BorrowedWithFontSystem, BufferLine, Color, Cursor, Affinity, Attrs, AttrsList, BidiParagraphs, BorrowedWithFontSystem, BufferLine, Color, Cursor,
FontSystem, LayoutCursor, LayoutGlyph, LayoutLine, Motion, Scroll, ShapeBuffer, ShapeLine, FontSystem, LayoutCursor, LayoutGlyph, LayoutLine, LineEnding, LineIter, Motion, Scroll,
Shaping, Wrap, ShapeBuffer, ShapeLine, Shaping, Wrap,
}; };
/// A line of visible text for rendering /// A line of visible text for rendering
@ -607,7 +607,17 @@ impl Buffer {
attrs: Attrs, attrs: Attrs,
shaping: Shaping, shaping: Shaping,
) { ) {
self.set_rich_text(font_system, [(text, attrs)], attrs, shaping); self.lines.clear();
for (range, ending) in LineIter::new(text) {
self.lines.push(BufferLine::new(
&text[range],
ending,
AttrsList::new(attrs),
shaping,
));
}
self.scroll = Scroll::default();
self.shape_until_scroll(font_system, false);
} }
/// Set text of buffer, using an iterator of styled spans (pairs of text and attributes) /// Set text of buffer, using an iterator of styled spans (pairs of text and attributes)
@ -661,12 +671,15 @@ impl Buffer {
start..end start..end
}); });
let mut maybe_line = lines_iter.next(); let mut maybe_line = lines_iter.next();
//TODO: set this based on information from spans
let line_ending = LineEnding::default();
loop { loop {
let (Some(line_range), Some((attrs, span_range))) = (&maybe_line, &maybe_span) else { let (Some(line_range), Some((attrs, span_range))) = (&maybe_line, &maybe_span) else {
// this is reached only if this text is empty // this is reached only if this text is empty
self.lines.push(BufferLine::new( self.lines.push(BufferLine::new(
String::new(), String::new(),
line_ending,
AttrsList::new(default_attrs), AttrsList::new(default_attrs),
shaping, shaping,
)); ));
@ -701,11 +714,13 @@ impl Buffer {
let prev_attrs_list = let prev_attrs_list =
core::mem::replace(&mut attrs_list, AttrsList::new(default_attrs)); core::mem::replace(&mut attrs_list, AttrsList::new(default_attrs));
let prev_line_string = core::mem::take(&mut line_string); let prev_line_string = core::mem::take(&mut line_string);
let buffer_line = BufferLine::new(prev_line_string, prev_attrs_list, shaping); let buffer_line =
BufferLine::new(prev_line_string, line_ending, prev_attrs_list, shaping);
self.lines.push(buffer_line); self.lines.push(buffer_line);
} else { } else {
// finalize the final line // finalize the final line
let buffer_line = BufferLine::new(line_string, attrs_list, shaping); let buffer_line =
BufferLine::new(line_string, line_ending, attrs_list, shaping);
self.lines.push(buffer_line); self.lines.push(buffer_line);
break; break;
} }

View file

@ -1,13 +1,15 @@
#[cfg(not(feature = "std"))] #[cfg(not(feature = "std"))]
use alloc::{string::String, vec::Vec}; use alloc::{string::String, vec::Vec};
use crate::{Align, AttrsList, FontSystem, LayoutLine, ShapeBuffer, ShapeLine, Shaping, Wrap}; use crate::{
Align, AttrsList, FontSystem, LayoutLine, LineEnding, ShapeBuffer, ShapeLine, Shaping, Wrap,
};
/// A line (or paragraph) of text that is shaped and laid out /// A line (or paragraph) of text that is shaped and laid out
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct BufferLine { pub struct BufferLine {
//TODO: make this not pub(crate)
text: String, text: String,
ending: LineEnding,
attrs_list: AttrsList, attrs_list: AttrsList,
align: Option<Align>, align: Option<Align>,
shape_opt: Option<ShapeLine>, shape_opt: Option<ShapeLine>,
@ -20,9 +22,15 @@ impl BufferLine {
/// Create a new line with the given text and attributes list /// Create a new line with the given text and attributes list
/// Cached shaping and layout can be done using the [`Self::shape`] and /// Cached shaping and layout can be done using the [`Self::shape`] and
/// [`Self::layout`] functions /// [`Self::layout`] functions
pub fn new<T: Into<String>>(text: T, attrs_list: AttrsList, shaping: Shaping) -> Self { pub fn new<T: Into<String>>(
text: T,
ending: LineEnding,
attrs_list: AttrsList,
shaping: Shaping,
) -> Self {
Self { Self {
text: text.into(), text: text.into(),
ending,
attrs_list, attrs_list,
align: None, align: None,
shape_opt: None, shape_opt: None,
@ -41,11 +49,17 @@ impl BufferLine {
/// ///
/// Will reset shape and layout if it differs from current text and attributes list. /// Will reset shape and layout if it differs from current text and attributes list.
/// Returns true if the line was reset /// Returns true if the line was reset
pub fn set_text<T: AsRef<str>>(&mut self, text: T, attrs_list: AttrsList) -> bool { pub fn set_text<T: AsRef<str>>(
&mut self,
text: T,
ending: LineEnding,
attrs_list: AttrsList,
) -> bool {
let text = text.as_ref(); let text = text.as_ref();
if text != self.text || attrs_list != self.attrs_list { if text != self.text || ending != self.ending || attrs_list != self.attrs_list {
self.text.clear(); self.text.clear();
self.text.push_str(text); self.text.push_str(text);
self.ending = ending;
self.attrs_list = attrs_list; self.attrs_list = attrs_list;
self.reset(); self.reset();
true true
@ -59,6 +73,25 @@ impl BufferLine {
self.text self.text
} }
/// Get line ending
pub fn ending(&self) -> LineEnding {
self.ending
}
/// Set line ending
///
/// Will reset shape and layout if it differs from current line ending.
/// Returns true if the line was reset
pub fn set_ending(&mut self, ending: LineEnding) -> bool {
if ending != self.ending {
self.ending = ending;
self.reset_shaping();
true
} else {
false
}
}
/// Get attributes list /// Get attributes list
pub fn attrs_list(&self) -> &AttrsList { pub fn attrs_list(&self) -> &AttrsList {
&self.attrs_list &self.attrs_list
@ -126,7 +159,7 @@ impl BufferLine {
let attrs_list = self.attrs_list.split_off(index); let attrs_list = self.attrs_list.split_off(index);
self.reset(); self.reset();
let mut new = Self::new(text, attrs_list, self.shaping); let mut new = Self::new(text, self.ending, attrs_list, self.shaping);
new.align = self.align; new.align = self.align;
new new
} }

View file

@ -362,8 +362,14 @@ 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
.lines
.last()
.map(|line| line.ending())
.unwrap_or_default();
let line = BufferLine::new( let line = BufferLine::new(
String::new(), String::new(),
ending,
AttrsList::new(attrs_list.as_ref().map_or_else( AttrsList::new(attrs_list.as_ref().map_or_else(
|| { || {
buffer buffer
@ -380,6 +386,7 @@ 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);
@ -392,6 +399,7 @@ 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 // 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 addendum = once("").filter(|_| data.ends_with('\n'));
let mut lines_iter = data.split_inclusive('\n').chain(addendum); let mut lines_iter = data.split_inclusive('\n').chain(addendum);
if let Some(data_line) = lines_iter.next() { if let Some(data_line) = lines_iter.next() {
@ -402,6 +410,7 @@ impl<'buffer> Edit<'buffer> for Editor<'buffer> {
data_line data_line
.strip_suffix(char::is_control) .strip_suffix(char::is_control)
.unwrap_or(data_line), .unwrap_or(data_line),
ending,
these_attrs, these_attrs,
Shaping::Advanced, Shaping::Advanced,
)); ));
@ -414,6 +423,7 @@ impl<'buffer> Edit<'buffer> for Editor<'buffer> {
data_line data_line
.strip_suffix(char::is_control) .strip_suffix(char::is_control)
.unwrap_or(data_line), .unwrap_or(data_line),
ending,
final_attrs.split_off(remaining_split_len), final_attrs.split_off(remaining_split_len),
Shaping::Advanced, Shaping::Advanced,
); );
@ -429,6 +439,7 @@ impl<'buffer> Edit<'buffer> for Editor<'buffer> {
data_line data_line
.strip_suffix(char::is_control) .strip_suffix(char::is_control)
.unwrap_or(data_line), .unwrap_or(data_line),
ending,
final_attrs.split_off(remaining_split_len), final_attrs.split_off(remaining_split_len),
Shaping::Advanced, Shaping::Advanced,
); );

View file

@ -123,6 +123,9 @@ mod font;
pub use self::layout::*; pub use self::layout::*;
mod layout; mod layout;
pub use self::line_ending::*;
mod line_ending;
pub use self::shape::*; pub use self::shape::*;
mod shape; mod shape;

98
src/line_ending.rs Normal file
View file

@ -0,0 +1,98 @@
use core::ops::Range;
/// Line ending
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum LineEnding {
/// Use `\n` for line ending (POSIX-style)
#[default]
Lf,
/// Use `\r\n` for line ending (Windows-style)
CrLf,
/// Use `\r` for line ending (many legacy systems)
Cr,
/// Use `\n\r` for line ending (some legacy systems)
LfCr,
/// No line ending
None,
}
impl LineEnding {
/// Get the line ending as a str
pub fn as_str(&self) -> &'static str {
match self {
Self::Lf => "\n",
Self::CrLf => "\r\n",
Self::Cr => "\r",
Self::LfCr => "\n\r",
Self::None => "",
}
}
}
/// Iterator over lines terminated by [`LineEnding`]
#[derive(Debug)]
pub struct LineIter<'a> {
string: &'a str,
start: usize,
end: usize,
}
impl<'a> LineIter<'a> {
/// Create an iterator of lines in a string slice
pub fn new(string: &'a str) -> Self {
Self {
string,
start: 0,
end: string.len(),
}
}
}
impl<'a> Iterator for LineIter<'a> {
type Item = (Range<usize>, LineEnding);
fn next(&mut self) -> Option<Self::Item> {
let start = self.start;
match self.string[start..self.end].find(&['\r', '\n']) {
Some(i) => {
let end = start + i;
self.start = end;
let after = &self.string[end..];
let ending = if after.starts_with("\r\n") {
LineEnding::CrLf
} else if after.starts_with("\n\r") {
LineEnding::LfCr
} else if after.starts_with("\n") {
LineEnding::Lf
} else if after.starts_with("\r") {
LineEnding::Cr
} else {
//TODO: this should not be possible
LineEnding::None
};
self.start += ending.as_str().len();
Some((start..end, ending))
}
None => {
if self.start < self.end {
self.start = self.end;
Some((start..self.end, LineEnding::None))
} else {
None
}
}
}
}
}
//TODO: DoubleEndedIterator
#[test]
fn test_line_iter() {
let string = "LF\nCRLF\r\nCR\rLFCR\n\rNONE";
let mut iter = LineIter::new(string);
assert_eq!(iter.next(), Some((0..2, LineEnding::Lf)));
assert_eq!(iter.next(), Some((3..7, LineEnding::CrLf)));
assert_eq!(iter.next(), Some((9..11, LineEnding::Cr)));
assert_eq!(iter.next(), Some((12..16, LineEnding::LfCr)));
assert_eq!(iter.next(), Some((18..22, LineEnding::None)));
}