diff --git a/editor-orbclient.sh b/editor-orbclient.sh deleted file mode 100755 index 1c4244cae0..0000000000 --- a/editor-orbclient.sh +++ /dev/null @@ -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 -- "$@" diff --git a/editor.sh b/editor.sh new file mode 100755 index 0000000000..dbde11049b --- /dev/null +++ b/editor.sh @@ -0,0 +1,3 @@ +# SPDX-License-Identifier: MIT OR Apache-2.0 + +RUST_LOG="cosmic_text=debug,editor=debug" cargo run --release --package editor -- "$@" diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs index 857d8677c0..6afeec127c 100644 --- a/examples/editor/src/main.rs +++ b/examples/editor/src/main.rs @@ -4,7 +4,7 @@ use cosmic_text::{ Action, Attrs, Buffer, Edit, Family, FontSystem, Metrics, Motion, SwashCache, SyntaxEditor, 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 winit::{ 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 { diff --git a/sample/crlf.txt b/sample/crlf.txt new file mode 100644 index 0000000000..8b790a54ba --- /dev/null +++ b/sample/crlf.txt @@ -0,0 +1,3 @@ +These are two lines +in a CRLF file + diff --git a/sample/tabs.txt b/sample/tabs.txt new file mode 100644 index 0000000000..45d81ccf5b --- /dev/null +++ b/sample/tabs.txt @@ -0,0 +1,5 @@ +Tabs: + One Two Three Four + Two Three Four + Three Four + Four diff --git a/src/buffer.rs b/src/buffer.rs index facc1500eb..608927e1d0 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -7,8 +7,8 @@ use unicode_segmentation::UnicodeSegmentation; use crate::{ Affinity, Attrs, AttrsList, BidiParagraphs, BorrowedWithFontSystem, BufferLine, Color, Cursor, - FontSystem, LayoutCursor, LayoutGlyph, LayoutLine, Motion, Scroll, ShapeBuffer, ShapeLine, - Shaping, Wrap, + FontSystem, LayoutCursor, LayoutGlyph, LayoutLine, LineEnding, LineIter, Motion, Scroll, + ShapeBuffer, ShapeLine, Shaping, Wrap, }; /// A line of visible text for rendering @@ -607,7 +607,17 @@ impl Buffer { attrs: Attrs, 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) @@ -661,12 +671,15 @@ impl Buffer { start..end }); let mut maybe_line = lines_iter.next(); + //TODO: set this based on information from spans + let line_ending = LineEnding::default(); loop { let (Some(line_range), Some((attrs, span_range))) = (&maybe_line, &maybe_span) else { // this is reached only if this text is empty self.lines.push(BufferLine::new( String::new(), + line_ending, AttrsList::new(default_attrs), shaping, )); @@ -701,11 +714,13 @@ impl Buffer { let prev_attrs_list = core::mem::replace(&mut attrs_list, AttrsList::new(default_attrs)); 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); } else { // 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); break; } diff --git a/src/buffer_line.rs b/src/buffer_line.rs index 4fc53d44e9..d5a432d9d6 100644 --- a/src/buffer_line.rs +++ b/src/buffer_line.rs @@ -1,13 +1,15 @@ #[cfg(not(feature = "std"))] 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 #[derive(Clone, Debug)] pub struct BufferLine { - //TODO: make this not pub(crate) text: String, + ending: LineEnding, attrs_list: AttrsList, align: Option, shape_opt: Option, @@ -20,9 +22,15 @@ impl BufferLine { /// Create a new line with the given text and attributes list /// Cached shaping and layout can be done using the [`Self::shape`] and /// [`Self::layout`] functions - pub fn new>(text: T, attrs_list: AttrsList, shaping: Shaping) -> Self { + pub fn new>( + text: T, + ending: LineEnding, + attrs_list: AttrsList, + shaping: Shaping, + ) -> Self { Self { text: text.into(), + ending, attrs_list, align: None, shape_opt: None, @@ -41,11 +49,17 @@ impl BufferLine { /// /// Will reset shape and layout if it differs from current text and attributes list. /// Returns true if the line was reset - pub fn set_text>(&mut self, text: T, attrs_list: AttrsList) -> bool { + pub fn set_text>( + &mut self, + text: T, + ending: LineEnding, + attrs_list: AttrsList, + ) -> bool { 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.push_str(text); + self.ending = ending; self.attrs_list = attrs_list; self.reset(); true @@ -59,6 +73,25 @@ impl BufferLine { 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 pub fn attrs_list(&self) -> &AttrsList { &self.attrs_list @@ -126,7 +159,7 @@ impl BufferLine { let attrs_list = self.attrs_list.split_off(index); 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 } diff --git a/src/edit/editor.rs b/src/edit/editor.rs index 03c63e83de..cf7690159b 100644 --- a/src/edit/editor.rs +++ b/src/edit/editor.rs @@ -362,8 +362,14 @@ 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(|line| line.ending()) + .unwrap_or_default(); let line = BufferLine::new( String::new(), + ending, AttrsList::new(attrs_list.as_ref().map_or_else( || { buffer @@ -380,6 +386,7 @@ 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); @@ -392,6 +399,7 @@ 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() { @@ -402,6 +410,7 @@ impl<'buffer> Edit<'buffer> for Editor<'buffer> { data_line .strip_suffix(char::is_control) .unwrap_or(data_line), + ending, these_attrs, Shaping::Advanced, )); @@ -414,6 +423,7 @@ impl<'buffer> Edit<'buffer> for Editor<'buffer> { data_line .strip_suffix(char::is_control) .unwrap_or(data_line), + ending, final_attrs.split_off(remaining_split_len), Shaping::Advanced, ); @@ -429,6 +439,7 @@ impl<'buffer> Edit<'buffer> for Editor<'buffer> { data_line .strip_suffix(char::is_control) .unwrap_or(data_line), + ending, final_attrs.split_off(remaining_split_len), Shaping::Advanced, ); diff --git a/src/lib.rs b/src/lib.rs index baf6e8a411..b34f6a5ebe 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -123,6 +123,9 @@ mod font; pub use self::layout::*; mod layout; +pub use self::line_ending::*; +mod line_ending; + pub use self::shape::*; mod shape; diff --git a/src/line_ending.rs b/src/line_ending.rs new file mode 100644 index 0000000000..7c6de2ee67 --- /dev/null +++ b/src/line_ending.rs @@ -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, LineEnding); + fn next(&mut self) -> Option { + 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))); +}