Introduce LineEnding to editor and fix inconsistencies
This commit is contained in:
parent
00a048677f
commit
87165ccd29
5 changed files with 93 additions and 60 deletions
|
|
@ -137,7 +137,7 @@ impl text::Editor for () {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn line(&self, _index: usize) -> Option<&str> {
|
fn line(&self, _index: usize) -> Option<text::editor::Line<'_>> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ use crate::text::highlighter::{self, Highlighter};
|
||||||
use crate::text::{LineHeight, Wrapping};
|
use crate::text::{LineHeight, Wrapping};
|
||||||
use crate::{Pixels, Point, Rectangle, Size};
|
use crate::{Pixels, Point, Rectangle, Size};
|
||||||
|
|
||||||
|
use std::borrow::Cow;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
/// A component that can be used by widgets to edit multi-line text.
|
/// A component that can be used by widgets to edit multi-line text.
|
||||||
|
|
@ -28,7 +29,7 @@ pub trait Editor: Sized + Default {
|
||||||
fn selection(&self) -> Option<String>;
|
fn selection(&self) -> Option<String>;
|
||||||
|
|
||||||
/// Returns the text of the given line in the [`Editor`], if it exists.
|
/// Returns the text of the given line in the [`Editor`], if it exists.
|
||||||
fn line(&self, index: usize) -> Option<&str>;
|
fn line(&self, index: usize) -> Option<Line<'_>>;
|
||||||
|
|
||||||
/// Returns the amount of lines in the [`Editor`].
|
/// Returns the amount of lines in the [`Editor`].
|
||||||
fn line_count(&self) -> usize;
|
fn line_count(&self) -> usize;
|
||||||
|
|
@ -189,3 +190,41 @@ pub enum Cursor {
|
||||||
/// Cursor selecting a range of text
|
/// Cursor selecting a range of text
|
||||||
Selection(Vec<Rectangle>),
|
Selection(Vec<Rectangle>),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A line of an [`Editor`].
|
||||||
|
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
||||||
|
pub struct Line<'a> {
|
||||||
|
/// The raw text of the [`Line`].
|
||||||
|
pub text: Cow<'a, str>,
|
||||||
|
/// The line ending of the [`Line`].
|
||||||
|
pub ending: LineEnding,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The line ending of a [`Line`].
|
||||||
|
#[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 {
|
||||||
|
/// Gets the string representation of the [`LineEnding`].
|
||||||
|
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 => "",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -117,8 +117,16 @@ impl Editor {
|
||||||
} else {
|
} else {
|
||||||
self.is_loading = true;
|
self.is_loading = true;
|
||||||
|
|
||||||
|
let mut text = self.content.text();
|
||||||
|
|
||||||
|
if let Some(ending) = self.content.line_ending() {
|
||||||
|
if !text.ends_with(ending.as_str()) {
|
||||||
|
text.push_str(ending.as_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Task::perform(
|
Task::perform(
|
||||||
save_file(self.file.clone(), self.content.text()),
|
save_file(self.file.clone(), text),
|
||||||
Message::FileSaved,
|
Message::FileSaved,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ use crate::text;
|
||||||
|
|
||||||
use cosmic_text::Edit as _;
|
use cosmic_text::Edit as _;
|
||||||
|
|
||||||
|
use std::borrow::Cow;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::sync::{self, Arc};
|
use std::sync::{self, Arc};
|
||||||
|
|
||||||
|
|
@ -89,11 +90,17 @@ impl editor::Editor for Editor {
|
||||||
|| (buffer.lines.len() == 1 && buffer.lines[0].text().is_empty())
|
|| (buffer.lines.len() == 1 && buffer.lines[0].text().is_empty())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn line(&self, index: usize) -> Option<&str> {
|
fn line(&self, index: usize) -> Option<editor::Line<'_>> {
|
||||||
self.buffer()
|
self.buffer().lines.get(index).map(|line| editor::Line {
|
||||||
.lines
|
text: Cow::Borrowed(line.text()),
|
||||||
.get(index)
|
ending: match line.ending() {
|
||||||
.map(cosmic_text::BufferLine::text)
|
cosmic_text::LineEnding::Lf => editor::LineEnding::Lf,
|
||||||
|
cosmic_text::LineEnding::CrLf => editor::LineEnding::CrLf,
|
||||||
|
cosmic_text::LineEnding::Cr => editor::LineEnding::Cr,
|
||||||
|
cosmic_text::LineEnding::LfCr => editor::LineEnding::LfCr,
|
||||||
|
cosmic_text::LineEnding::None => editor::LineEnding::None,
|
||||||
|
},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn line_count(&self) -> usize {
|
fn line_count(&self) -> usize {
|
||||||
|
|
|
||||||
|
|
@ -50,12 +50,13 @@ use crate::core::{
|
||||||
Rectangle, Shell, Size, SmolStr, Theme, Vector,
|
Rectangle, Shell, Size, SmolStr, Theme, Vector,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use std::borrow::Cow;
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::ops::DerefMut;
|
use std::ops::DerefMut;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
pub use text::editor::{Action, Edit, Motion};
|
pub use text::editor::{Action, Edit, Line, LineEnding, Motion};
|
||||||
|
|
||||||
/// A multi-line text input.
|
/// A multi-line text input.
|
||||||
///
|
///
|
||||||
|
|
@ -349,69 +350,47 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the text of the line at the given index, if it exists.
|
/// Returns the text of the line at the given index, if it exists.
|
||||||
pub fn line(
|
pub fn line(&self, index: usize) -> Option<Line<'_>> {
|
||||||
&self,
|
let internal = self.0.borrow();
|
||||||
index: usize,
|
let line = internal.editor.line(index)?;
|
||||||
) -> Option<impl std::ops::Deref<Target = str> + '_> {
|
|
||||||
std::cell::Ref::filter_map(self.0.borrow(), |internal| {
|
Some(Line {
|
||||||
internal.editor.line(index)
|
text: Cow::Owned(line.text.into_owned()),
|
||||||
|
ending: line.ending,
|
||||||
})
|
})
|
||||||
.ok()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns an iterator of the text of the lines in the [`Content`].
|
/// Returns an iterator of the text of the lines in the [`Content`].
|
||||||
pub fn lines(
|
pub fn lines(&self) -> impl Iterator<Item = Line<'_>> {
|
||||||
&self,
|
(0..)
|
||||||
) -> impl Iterator<Item = impl std::ops::Deref<Target = str> + '_> {
|
.map(|i| self.line(i))
|
||||||
struct Lines<'a, Renderer: text::Renderer> {
|
.take_while(Option::is_some)
|
||||||
internal: std::cell::Ref<'a, Internal<Renderer>>,
|
.flatten()
|
||||||
current: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, Renderer: text::Renderer> Iterator for Lines<'a, Renderer> {
|
|
||||||
type Item = std::cell::Ref<'a, str>;
|
|
||||||
|
|
||||||
fn next(&mut self) -> Option<Self::Item> {
|
|
||||||
let line = std::cell::Ref::filter_map(
|
|
||||||
std::cell::Ref::clone(&self.internal),
|
|
||||||
|internal| internal.editor.line(self.current),
|
|
||||||
)
|
|
||||||
.ok()?;
|
|
||||||
|
|
||||||
self.current += 1;
|
|
||||||
|
|
||||||
Some(line)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Lines {
|
|
||||||
internal: self.0.borrow(),
|
|
||||||
current: 0,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the text of the [`Content`].
|
/// Returns the text of the [`Content`].
|
||||||
///
|
|
||||||
/// Lines are joined with `'\n'`.
|
|
||||||
pub fn text(&self) -> String {
|
pub fn text(&self) -> String {
|
||||||
let mut text = self.lines().enumerate().fold(
|
let mut contents = String::new();
|
||||||
String::new(),
|
let mut lines = self.lines().peekable();
|
||||||
|mut contents, (i, line)| {
|
|
||||||
if i > 0 {
|
|
||||||
contents.push('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
contents.push_str(&line);
|
while let Some(line) = lines.next() {
|
||||||
|
contents.push_str(&line.text);
|
||||||
|
|
||||||
contents
|
if lines.peek().is_some() {
|
||||||
},
|
contents.push_str(if line.ending == LineEnding::None {
|
||||||
);
|
LineEnding::default().as_str()
|
||||||
|
} else {
|
||||||
if !text.ends_with('\n') {
|
line.ending.as_str()
|
||||||
text.push('\n');
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
text
|
contents
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the kind of [`LineEnding`] used for separating lines in the [`Content`].
|
||||||
|
pub fn line_ending(&self) -> Option<LineEnding> {
|
||||||
|
Some(self.line(0)?.ending)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the selected text of the [`Content`].
|
/// Returns the selected text of the [`Content`].
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue