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:
parent
ff5501d9a3
commit
0cfd9b64ef
10 changed files with 194 additions and 15 deletions
|
|
@ -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
3
editor.sh
Executable 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 -- "$@"
|
||||||
|
|
@ -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
3
sample/crlf.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
These are two lines
|
||||||
|
in a CRLF file
|
||||||
|
|
||||||
5
sample/tabs.txt
Normal file
5
sample/tabs.txt
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
Tabs:
|
||||||
|
One Two Three Four
|
||||||
|
Two Three Four
|
||||||
|
Three Four
|
||||||
|
Four
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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
98
src/line_ending.rs
Normal 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)));
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue