Skip to content

Commit

Permalink
Merge pull request iced-rs#2759 from iced-rs/fix/respect-editor-line-…
Browse files Browse the repository at this point in the history
…endings

Introduce `LineEnding` to `editor` and fix inconsistencies
  • Loading branch information
hecrj authored Jan 28, 2025
2 parents 00a0486 + 87165cc commit ce4ee93
Show file tree
Hide file tree
Showing 5 changed files with 93 additions and 60 deletions.
2 changes: 1 addition & 1 deletion core/src/renderer/null.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ impl text::Editor for () {
None
}

fn line(&self, _index: usize) -> Option<&str> {
fn line(&self, _index: usize) -> Option<text::editor::Line<'_>> {
None
}

Expand Down
41 changes: 40 additions & 1 deletion core/src/text/editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use crate::text::highlighter::{self, Highlighter};
use crate::text::{LineHeight, Wrapping};
use crate::{Pixels, Point, Rectangle, Size};

use std::borrow::Cow;
use std::sync::Arc;

/// A component that can be used by widgets to edit multi-line text.
Expand All @@ -28,7 +29,7 @@ pub trait Editor: Sized + Default {
fn selection(&self) -> Option<String>;

/// 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`].
fn line_count(&self) -> usize;
Expand Down Expand Up @@ -189,3 +190,41 @@ pub enum Cursor {
/// Cursor selecting a range of text
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 => "",
}
}
}
10 changes: 9 additions & 1 deletion examples/editor/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,16 @@ impl Editor {
} else {
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(
save_file(self.file.clone(), self.content.text()),
save_file(self.file.clone(), text),
Message::FileSaved,
)
}
Expand Down
17 changes: 12 additions & 5 deletions graphics/src/text/editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use crate::text;

use cosmic_text::Edit as _;

use std::borrow::Cow;
use std::fmt;
use std::sync::{self, Arc};

Expand Down Expand Up @@ -89,11 +90,17 @@ impl editor::Editor for Editor {
|| (buffer.lines.len() == 1 && buffer.lines[0].text().is_empty())
}

fn line(&self, index: usize) -> Option<&str> {
self.buffer()
.lines
.get(index)
.map(cosmic_text::BufferLine::text)
fn line(&self, index: usize) -> Option<editor::Line<'_>> {
self.buffer().lines.get(index).map(|line| editor::Line {
text: Cow::Borrowed(line.text()),
ending: match line.ending() {
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 {
Expand Down
83 changes: 31 additions & 52 deletions widget/src/text_editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,13 @@ use crate::core::{
Rectangle, Shell, Size, SmolStr, Theme, Vector,
};

use std::borrow::Cow;
use std::cell::RefCell;
use std::fmt;
use std::ops::DerefMut;
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.
///
Expand Down Expand Up @@ -349,69 +350,47 @@ where
}

/// Returns the text of the line at the given index, if it exists.
pub fn line(
&self,
index: usize,
) -> Option<impl std::ops::Deref<Target = str> + '_> {
std::cell::Ref::filter_map(self.0.borrow(), |internal| {
internal.editor.line(index)
pub fn line(&self, index: usize) -> Option<Line<'_>> {
let internal = self.0.borrow();
let line = internal.editor.line(index)?;

Some(Line {
text: Cow::Owned(line.text.into_owned()),
ending: line.ending,
})
.ok()
}

/// Returns an iterator of the text of the lines in the [`Content`].
pub fn lines(
&self,
) -> impl Iterator<Item = impl std::ops::Deref<Target = str> + '_> {
struct Lines<'a, Renderer: text::Renderer> {
internal: std::cell::Ref<'a, Internal<Renderer>>,
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,
}
pub fn lines(&self) -> impl Iterator<Item = Line<'_>> {
(0..)
.map(|i| self.line(i))
.take_while(Option::is_some)
.flatten()
}

/// Returns the text of the [`Content`].
///
/// Lines are joined with `'\n'`.
pub fn text(&self) -> String {
let mut text = self.lines().enumerate().fold(
String::new(),
|mut contents, (i, line)| {
if i > 0 {
contents.push('\n');
}
let mut contents = String::new();
let mut lines = self.lines().peekable();

contents.push_str(&line);
while let Some(line) = lines.next() {
contents.push_str(&line.text);

contents
},
);

if !text.ends_with('\n') {
text.push('\n');
if lines.peek().is_some() {
contents.push_str(if line.ending == LineEnding::None {
LineEnding::default().as_str()
} else {
line.ending.as_str()
});
}
}

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`].
Expand Down

0 comments on commit ce4ee93

Please sign in to comment.