Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add line ending abstraction #250

Merged
merged 3 commits into from
Apr 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions editor-orbclient.sh

This file was deleted.

3 changes: 3 additions & 0 deletions editor.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# SPDX-License-Identifier: MIT OR Apache-2.0

RUST_LOG="cosmic_text=debug,editor=debug" cargo run --release --package editor -- "$@"
13 changes: 12 additions & 1 deletion examples/editor/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 {
Expand Down
3 changes: 3 additions & 0 deletions sample/crlf.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
These are two lines
in a CRLF file

5 changes: 5 additions & 0 deletions sample/tabs.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Tabs:
One Two Three Four
Two Three Four
Three Four
Four
25 changes: 20 additions & 5 deletions src/buffer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@

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
Expand All @@ -35,7 +35,7 @@
/// and `cursor_end` within this run, or None if the cursor range does not intersect this run.
/// This may return widths of zero if `cursor_start == cursor_end`, if the run is empty, or if the
/// region's left start boundary is the same as the cursor's end boundary or vice versa.
pub fn highlight(&self, cursor_start: Cursor, cursor_end: Cursor) -> Option<(f32, f32)> {

Check warning on line 38 in src/buffer.rs

View workflow job for this annotation

GitHub Actions / clippy

docs for function which may panic missing `# Panics` section

warning: docs for function which may panic missing `# Panics` section --> src/buffer.rs:38:5 | 38 | pub fn highlight(&self, cursor_start: Cursor, cursor_end: Cursor) -> Option<(f32, f32)> { | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | note: first possible panic found here --> src/buffer.rs:60:25 | 60 | let x_end = x_end.expect("end of cursor not found"); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#missing_panics_doc
let mut x_start = None;
let mut x_end = None;
let rtl_factor = if self.rtl { 1. } else { 0. };
Expand Down Expand Up @@ -328,12 +328,12 @@
}

/// Shape lines until cursor, also scrolling to include cursor in view
pub fn shape_until_cursor(
&mut self,
font_system: &mut FontSystem,
cursor: Cursor,
prune: bool,
) {

Check warning on line 336 in src/buffer.rs

View workflow job for this annotation

GitHub Actions / clippy

docs for function which may panic missing `# Panics` section

warning: docs for function which may panic missing `# Panics` section --> src/buffer.rs:331:5 | 331 | / pub fn shape_until_cursor( 332 | | &mut self, 333 | | font_system: &mut FontSystem, 334 | | cursor: Cursor, 335 | | prune: bool, 336 | | ) { | |_____^ | note: first possible panic found here --> src/buffer.rs:339:29 | 339 | let layout_cursor = self | _____________________________^ 340 | | .layout_cursor(font_system, cursor) 341 | | .expect("shape_until_cursor invalid cursor"); | |________________________________________________________^ = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#missing_panics_doc
let old_scroll = self.scroll;

let layout_cursor = self
Expand Down Expand Up @@ -376,7 +376,7 @@
}

/// Shape lines until scroll
pub fn shape_until_scroll(&mut self, font_system: &mut FontSystem, prune: bool) {

Check warning on line 379 in src/buffer.rs

View workflow job for this annotation

GitHub Actions / clippy

docs for function which may panic missing `# Panics` section

warning: docs for function which may panic missing `# Panics` section --> src/buffer.rs:379:5 | 379 | pub fn shape_until_scroll(&mut self, font_system: &mut FontSystem, prune: bool) { | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | note: first possible panic found here --> src/buffer.rs:417:30 | 417 | let layout = self | ______________________________^ 418 | | .line_layout(font_system, line_i) 419 | | .expect("shape_until_scroll invalid line"); | |______________________________________________________________^ = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#missing_panics_doc
let old_scroll = self.scroll;

loop {
Expand Down Expand Up @@ -607,7 +607,17 @@
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)
Expand Down Expand Up @@ -661,12 +671,15 @@
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,
));
Expand Down Expand Up @@ -701,11 +714,13 @@
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;
}
Expand Down
45 changes: 39 additions & 6 deletions src/buffer_line.rs
Original file line number Diff line number Diff line change
@@ -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<Align>,
shape_opt: Option<ShapeLine>,
Expand All @@ -20,9 +22,15 @@
/// 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<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 {
text: text.into(),
ending,
attrs_list,
align: None,
shape_opt: None,
Expand All @@ -41,11 +49,17 @@
///
/// 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<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();
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
Expand All @@ -59,6 +73,25 @@
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
Expand Down Expand Up @@ -126,7 +159,7 @@
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
}
Expand Down Expand Up @@ -154,11 +187,11 @@
}

/// Shape a line using a pre-existing shape buffer, will cache results
pub fn shape_in_buffer(
&mut self,
scratch: &mut ShapeBuffer,
font_system: &mut FontSystem,
) -> &ShapeLine {

Check warning on line 194 in src/buffer_line.rs

View workflow job for this annotation

GitHub Actions / clippy

docs for function which may panic missing `# Panics` section

warning: docs for function which may panic missing `# Panics` section --> src/buffer_line.rs:190:5 | 190 | / pub fn shape_in_buffer( 191 | | &mut self, 192 | | scratch: &mut ShapeBuffer, 193 | | font_system: &mut FontSystem, 194 | | ) -> &ShapeLine { | |___________________^ | note: first possible panic found here --> src/buffer_line.rs:205:9 | 205 | self.shape_opt.as_ref().expect("shape not found") | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#missing_panics_doc
if self.shape_opt.is_none() {
self.shape_opt = Some(ShapeLine::new_in_buffer(
scratch,
Expand Down Expand Up @@ -197,15 +230,15 @@
}

/// Layout a line using a pre-existing shape buffer, will cache results
pub fn layout_in_buffer(
&mut self,
scratch: &mut ShapeBuffer,
font_system: &mut FontSystem,
font_size: f32,
width: f32,
wrap: Wrap,
match_mono_width: Option<f32>,
) -> &[LayoutLine] {

Check warning on line 241 in src/buffer_line.rs

View workflow job for this annotation

GitHub Actions / clippy

docs for function which may panic missing `# Panics` section

warning: docs for function which may panic missing `# Panics` section --> src/buffer_line.rs:233:5 | 233 | / pub fn layout_in_buffer( 234 | | &mut self, 235 | | scratch: &mut ShapeBuffer, 236 | | font_system: &mut FontSystem, ... | 240 | | match_mono_width: Option<f32>, 241 | | ) -> &[LayoutLine] { | |______________________^ | note: first possible panic found here --> src/buffer_line.rs:257:9 | 257 | self.layout_opt.as_ref().expect("layout not found") | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#missing_panics_doc
if self.layout_opt.is_none() {
let align = self.align;
let shape = self.shape_in_buffer(scratch, font_system);
Expand Down
11 changes: 11 additions & 0 deletions src/edit/editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -362,8 +362,14 @@

// 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
Expand All @@ -380,6 +386,7 @@

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);
Expand All @@ -392,6 +399,7 @@

// 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() {
Expand All @@ -402,6 +410,7 @@
data_line
.strip_suffix(char::is_control)
.unwrap_or(data_line),
ending,
these_attrs,
Shaping::Advanced,
));
Expand All @@ -414,6 +423,7 @@
data_line
.strip_suffix(char::is_control)
.unwrap_or(data_line),
ending,
final_attrs.split_off(remaining_split_len),
Shaping::Advanced,
);
Expand All @@ -429,6 +439,7 @@
data_line
.strip_suffix(char::is_control)
.unwrap_or(data_line),
ending,
final_attrs.split_off(remaining_split_len),
Shaping::Advanced,
);
Expand Down Expand Up @@ -507,17 +518,17 @@

fn apply_change(&mut self, change: &Change) -> bool {
// Cannot apply changes if there is a pending change
match self.change.take() {
Some(pending) => {
if !pending.items.is_empty() {
//TODO: is this a good idea?
log::warn!("pending change caused apply_change to be ignored!");
self.change = Some(pending);
return false;
}
}
None => {}
}

Check warning on line 531 in src/edit/editor.rs

View workflow job for this annotation

GitHub Actions / clippy

you seem to be trying to use `match` for destructuring a single pattern. Consider using `if let`

warning: you seem to be trying to use `match` for destructuring a single pattern. Consider using `if let` --> src/edit/editor.rs:521:9 | 521 | / match self.change.take() { 522 | | Some(pending) => { 523 | | if !pending.items.is_empty() { 524 | | //TODO: is this a good idea? ... | 530 | | None => {} 531 | | } | |_________^ | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#single_match = note: `#[warn(clippy::single_match)]` on by default help: try | 521 ~ if let Some(pending) = self.change.take() { 522 + if !pending.items.is_empty() { 523 + //TODO: is this a good idea? 524 + log::warn!("pending change caused apply_change to be ignored!"); 525 + self.change = Some(pending); 526 + return false; 527 + } 528 + } |

for item in change.items.iter() {
//TODO: edit cursor if needed?
Expand Down
3 changes: 3 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
98 changes: 98 additions & 0 deletions src/line_ending.rs
Original file line number Diff line number Diff line change
@@ -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']) {

Check warning on line 55 in src/line_ending.rs

View workflow job for this annotation

GitHub Actions / clippy

the borrowed expression implements the required traits

warning: the borrowed expression implements the required traits --> src/line_ending.rs:55:49 | 55 | match self.string[start..self.end].find(&['\r', '\n']) { | ^^^^^^^^^^^^^ help: change this to: `['\r', '\n']` | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#needless_borrows_for_generic_args = note: `#[warn(clippy::needless_borrows_for_generic_args)]` on by default
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") {

Check warning on line 64 in src/line_ending.rs

View workflow job for this annotation

GitHub Actions / clippy

single-character string constant used as pattern

warning: single-character string constant used as pattern --> src/line_ending.rs:64:45 | 64 | } else if after.starts_with("\n") { | ^^^^ help: consider using a `char`: `'\n'` | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#single_char_pattern = note: `#[warn(clippy::single_char_pattern)]` on by default
LineEnding::Lf
} else if after.starts_with("\r") {

Check warning on line 66 in src/line_ending.rs

View workflow job for this annotation

GitHub Actions / clippy

single-character string constant used as pattern

warning: single-character string constant used as pattern --> src/line_ending.rs:66:45 | 66 | } else if after.starts_with("\r") { | ^^^^ help: consider using a `char`: `'\r'` | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#single_char_pattern
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)));
}
Loading