Skip to content

Commit 87165cc

Browse files
committed
Introduce LineEnding to editor and fix inconsistencies
1 parent 00a0486 commit 87165cc

File tree

5 files changed

+93
-60
lines changed

5 files changed

+93
-60
lines changed

core/src/renderer/null.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ impl text::Editor for () {
137137
None
138138
}
139139

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

core/src/text/editor.rs

+40-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use crate::text::highlighter::{self, Highlighter};
33
use crate::text::{LineHeight, Wrapping};
44
use crate::{Pixels, Point, Rectangle, Size};
55

6+
use std::borrow::Cow;
67
use std::sync::Arc;
78

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

3031
/// Returns the text of the given line in the [`Editor`], if it exists.
31-
fn line(&self, index: usize) -> Option<&str>;
32+
fn line(&self, index: usize) -> Option<Line<'_>>;
3233

3334
/// Returns the amount of lines in the [`Editor`].
3435
fn line_count(&self) -> usize;
@@ -189,3 +190,41 @@ pub enum Cursor {
189190
/// Cursor selecting a range of text
190191
Selection(Vec<Rectangle>),
191192
}
193+
194+
/// A line of an [`Editor`].
195+
#[derive(Clone, Debug, Default, Eq, PartialEq)]
196+
pub struct Line<'a> {
197+
/// The raw text of the [`Line`].
198+
pub text: Cow<'a, str>,
199+
/// The line ending of the [`Line`].
200+
pub ending: LineEnding,
201+
}
202+
203+
/// The line ending of a [`Line`].
204+
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
205+
pub enum LineEnding {
206+
/// Use `\n` for line ending (POSIX-style)
207+
#[default]
208+
Lf,
209+
/// Use `\r\n` for line ending (Windows-style)
210+
CrLf,
211+
/// Use `\r` for line ending (many legacy systems)
212+
Cr,
213+
/// Use `\n\r` for line ending (some legacy systems)
214+
LfCr,
215+
/// No line ending
216+
None,
217+
}
218+
219+
impl LineEnding {
220+
/// Gets the string representation of the [`LineEnding`].
221+
pub fn as_str(self) -> &'static str {
222+
match self {
223+
Self::Lf => "\n",
224+
Self::CrLf => "\r\n",
225+
Self::Cr => "\r",
226+
Self::LfCr => "\n\r",
227+
Self::None => "",
228+
}
229+
}
230+
}

examples/editor/src/main.rs

+9-1
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,16 @@ impl Editor {
117117
} else {
118118
self.is_loading = true;
119119

120+
let mut text = self.content.text();
121+
122+
if let Some(ending) = self.content.line_ending() {
123+
if !text.ends_with(ending.as_str()) {
124+
text.push_str(ending.as_str());
125+
}
126+
}
127+
120128
Task::perform(
121-
save_file(self.file.clone(), self.content.text()),
129+
save_file(self.file.clone(), text),
122130
Message::FileSaved,
123131
)
124132
}

graphics/src/text/editor.rs

+12-5
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use crate::text;
99

1010
use cosmic_text::Edit as _;
1111

12+
use std::borrow::Cow;
1213
use std::fmt;
1314
use std::sync::{self, Arc};
1415

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

92-
fn line(&self, index: usize) -> Option<&str> {
93-
self.buffer()
94-
.lines
95-
.get(index)
96-
.map(cosmic_text::BufferLine::text)
93+
fn line(&self, index: usize) -> Option<editor::Line<'_>> {
94+
self.buffer().lines.get(index).map(|line| editor::Line {
95+
text: Cow::Borrowed(line.text()),
96+
ending: match line.ending() {
97+
cosmic_text::LineEnding::Lf => editor::LineEnding::Lf,
98+
cosmic_text::LineEnding::CrLf => editor::LineEnding::CrLf,
99+
cosmic_text::LineEnding::Cr => editor::LineEnding::Cr,
100+
cosmic_text::LineEnding::LfCr => editor::LineEnding::LfCr,
101+
cosmic_text::LineEnding::None => editor::LineEnding::None,
102+
},
103+
})
97104
}
98105

99106
fn line_count(&self) -> usize {

widget/src/text_editor.rs

+31-52
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,13 @@ use crate::core::{
5050
Rectangle, Shell, Size, SmolStr, Theme, Vector,
5151
};
5252

53+
use std::borrow::Cow;
5354
use std::cell::RefCell;
5455
use std::fmt;
5556
use std::ops::DerefMut;
5657
use std::sync::Arc;
5758

58-
pub use text::editor::{Action, Edit, Motion};
59+
pub use text::editor::{Action, Edit, Line, LineEnding, Motion};
5960

6061
/// A multi-line text input.
6162
///
@@ -349,69 +350,47 @@ where
349350
}
350351

351352
/// Returns the text of the line at the given index, if it exists.
352-
pub fn line(
353-
&self,
354-
index: usize,
355-
) -> Option<impl std::ops::Deref<Target = str> + '_> {
356-
std::cell::Ref::filter_map(self.0.borrow(), |internal| {
357-
internal.editor.line(index)
353+
pub fn line(&self, index: usize) -> Option<Line<'_>> {
354+
let internal = self.0.borrow();
355+
let line = internal.editor.line(index)?;
356+
357+
Some(Line {
358+
text: Cow::Owned(line.text.into_owned()),
359+
ending: line.ending,
358360
})
359-
.ok()
360361
}
361362

362363
/// Returns an iterator of the text of the lines in the [`Content`].
363-
pub fn lines(
364-
&self,
365-
) -> impl Iterator<Item = impl std::ops::Deref<Target = str> + '_> {
366-
struct Lines<'a, Renderer: text::Renderer> {
367-
internal: std::cell::Ref<'a, Internal<Renderer>>,
368-
current: usize,
369-
}
370-
371-
impl<'a, Renderer: text::Renderer> Iterator for Lines<'a, Renderer> {
372-
type Item = std::cell::Ref<'a, str>;
373-
374-
fn next(&mut self) -> Option<Self::Item> {
375-
let line = std::cell::Ref::filter_map(
376-
std::cell::Ref::clone(&self.internal),
377-
|internal| internal.editor.line(self.current),
378-
)
379-
.ok()?;
380-
381-
self.current += 1;
382-
383-
Some(line)
384-
}
385-
}
386-
387-
Lines {
388-
internal: self.0.borrow(),
389-
current: 0,
390-
}
364+
pub fn lines(&self) -> impl Iterator<Item = Line<'_>> {
365+
(0..)
366+
.map(|i| self.line(i))
367+
.take_while(Option::is_some)
368+
.flatten()
391369
}
392370

393371
/// Returns the text of the [`Content`].
394-
///
395-
/// Lines are joined with `'\n'`.
396372
pub fn text(&self) -> String {
397-
let mut text = self.lines().enumerate().fold(
398-
String::new(),
399-
|mut contents, (i, line)| {
400-
if i > 0 {
401-
contents.push('\n');
402-
}
373+
let mut contents = String::new();
374+
let mut lines = self.lines().peekable();
403375

404-
contents.push_str(&line);
376+
while let Some(line) = lines.next() {
377+
contents.push_str(&line.text);
405378

406-
contents
407-
},
408-
);
409-
410-
if !text.ends_with('\n') {
411-
text.push('\n');
379+
if lines.peek().is_some() {
380+
contents.push_str(if line.ending == LineEnding::None {
381+
LineEnding::default().as_str()
382+
} else {
383+
line.ending.as_str()
384+
});
385+
}
412386
}
413387

414-
text
388+
contents
389+
}
390+
391+
/// Returns the kind of [`LineEnding`] used for separating lines in the [`Content`].
392+
pub fn line_ending(&self) -> Option<LineEnding> {
393+
Some(self.line(0)?.ending)
415394
}
416395

417396
/// Returns the selected text of the [`Content`].

0 commit comments

Comments
 (0)