commit
9a975ad79a
11 changed files with 1342 additions and 399 deletions
12
Cargo.toml
12
Cargo.toml
|
|
@ -10,20 +10,22 @@ repository = "https://github.com/pop-os/cosmic-text"
|
||||||
rust-version = "1.65"
|
rust-version = "1.65"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
cosmic_undo_2 = { version = "0.2.0", optional = true }
|
||||||
fontdb = { version = "0.16.0", default-features = false }
|
fontdb = { version = "0.16.0", default-features = false }
|
||||||
|
hashbrown = { version = "0.14.1", optional = true, default-features = false }
|
||||||
libm = "0.2.8"
|
libm = "0.2.8"
|
||||||
log = "0.4.20"
|
log = "0.4.20"
|
||||||
|
modit = { version = "0.1.0", optional = true }
|
||||||
|
rangemap = "1.4.0"
|
||||||
|
rustc-hash = { version = "1.1.0", default-features = false }
|
||||||
rustybuzz = { version = "0.11.0", default-features = false, features = ["libm"] }
|
rustybuzz = { version = "0.11.0", default-features = false, features = ["libm"] }
|
||||||
|
self_cell = "1.0.1"
|
||||||
swash = { version = "0.1.8", optional = true }
|
swash = { version = "0.1.8", optional = true }
|
||||||
syntect = { version = "5.1.0", optional = true }
|
syntect = { version = "5.1.0", optional = true }
|
||||||
sys-locale = { version = "0.3.1", optional = true }
|
sys-locale = { version = "0.3.1", optional = true }
|
||||||
unicode-linebreak = "0.1.5"
|
unicode-linebreak = "0.1.5"
|
||||||
unicode-script = "0.5.5"
|
unicode-script = "0.5.5"
|
||||||
unicode-segmentation = "1.10.1"
|
unicode-segmentation = "1.10.1"
|
||||||
rangemap = "1.4.0"
|
|
||||||
hashbrown = { version = "0.14.1", optional = true, default-features = false }
|
|
||||||
rustc-hash = { version = "1.1.0", default-features = false }
|
|
||||||
self_cell = "1.0.1"
|
|
||||||
|
|
||||||
[dependencies.unicode-bidi]
|
[dependencies.unicode-bidi]
|
||||||
version = "0.3.13"
|
version = "0.3.13"
|
||||||
|
|
@ -40,7 +42,7 @@ std = [
|
||||||
"sys-locale",
|
"sys-locale",
|
||||||
"unicode-bidi/std",
|
"unicode-bidi/std",
|
||||||
]
|
]
|
||||||
vi = ["syntect"]
|
vi = ["modit", "syntect", "cosmic_undo_2"]
|
||||||
wasm-web = ["sys-locale?/js"]
|
wasm-web = ["sys-locale?/js"]
|
||||||
warn_on_missing_glyphs = []
|
warn_on_missing_glyphs = []
|
||||||
fontconfig = ["fontdb/fontconfig", "std"]
|
fontconfig = ["fontdb/fontconfig", "std"]
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ fn main() {
|
||||||
let display_scale = match orbclient::get_display_size() {
|
let display_scale = match orbclient::get_display_size() {
|
||||||
Ok((w, h)) => {
|
Ok((w, h)) => {
|
||||||
log::info!("Display size: {}, {}", w, h);
|
log::info!("Display size: {}, {}", w, h);
|
||||||
(h as f32 / 1600.0) + 1.0
|
(h / 1600) as f32 + 1.0
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::warn!("Failed to get display size: {}", err);
|
log::warn!("Failed to get display size: {}", err);
|
||||||
|
|
|
||||||
|
|
@ -119,7 +119,7 @@ fn main() {
|
||||||
|
|
||||||
editor
|
editor
|
||||||
.buffer_mut()
|
.buffer_mut()
|
||||||
.set_rich_text(spans.iter().copied(), Shaping::Advanced);
|
.set_rich_text(spans.iter().copied(), attrs, Shaping::Advanced);
|
||||||
|
|
||||||
let mut swash_cache = SwashCache::new();
|
let mut swash_cache = SwashCache::new();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,12 +10,12 @@ redoxer install \
|
||||||
--path examples/editor-orbclient \
|
--path examples/editor-orbclient \
|
||||||
--root "target/redoxer"
|
--root "target/redoxer"
|
||||||
|
|
||||||
cmd="env RUST_LOG=cosmic_text=debug,editor_orbclient=debug ./bin/editor-orbclient"
|
args=(env RUST_LOG=cosmic_text=debug,editor_orbclient=debug /root/bin/editor-orbclient)
|
||||||
if [ -f "$1" ]
|
if [ -f "$1" ]
|
||||||
then
|
then
|
||||||
filename="$(basename "$1")"
|
filename="$(basename "$1")"
|
||||||
cp "$1" "target/redoxer/${filename}"
|
cp "$1" "target/redoxer/${filename}"
|
||||||
cmd="${cmd} '${filename}'"
|
args+=("${filename}")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
cd target/redoxer
|
cd target/redoxer
|
||||||
|
|
@ -24,5 +24,4 @@ cd target/redoxer
|
||||||
redoxer exec \
|
redoxer exec \
|
||||||
--gui \
|
--gui \
|
||||||
--folder . \
|
--folder . \
|
||||||
/bin/sh -c \
|
"${args[@]}"
|
||||||
"${cmd}"
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,7 @@
|
||||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
|
|
||||||
#[cfg(not(feature = "std"))]
|
#[cfg(not(feature = "std"))]
|
||||||
use alloc::{
|
use alloc::{string::String, vec::Vec};
|
||||||
string::{String, ToString},
|
|
||||||
vec::Vec,
|
|
||||||
};
|
|
||||||
use core::{cmp, fmt};
|
use core::{cmp, fmt};
|
||||||
use unicode_segmentation::UnicodeSegmentation;
|
use unicode_segmentation::UnicodeSegmentation;
|
||||||
|
|
||||||
|
|
@ -54,8 +51,9 @@ impl Cursor {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether to associate cursors placed at a boundary between runs with the run before or after it.
|
/// Whether to associate cursors placed at a boundary between runs with the run before or after it.
|
||||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
|
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd)]
|
||||||
pub enum Affinity {
|
pub enum Affinity {
|
||||||
|
#[default]
|
||||||
Before,
|
Before,
|
||||||
After,
|
After,
|
||||||
}
|
}
|
||||||
|
|
@ -86,12 +84,6 @@ impl Affinity {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Affinity {
|
|
||||||
fn default() -> Self {
|
|
||||||
Affinity::Before
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The position of a cursor within a [`Buffer`].
|
/// The position of a cursor within a [`Buffer`].
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct LayoutCursor {
|
pub struct LayoutCursor {
|
||||||
|
|
@ -552,12 +544,7 @@ impl Buffer {
|
||||||
///
|
///
|
||||||
/// Will panic if `metrics.font_size` is zero.
|
/// Will panic if `metrics.font_size` is zero.
|
||||||
pub fn set_metrics(&mut self, font_system: &mut FontSystem, metrics: Metrics) {
|
pub fn set_metrics(&mut self, font_system: &mut FontSystem, metrics: Metrics) {
|
||||||
if metrics != self.metrics {
|
self.set_metrics_and_size(font_system, metrics, self.width, self.height);
|
||||||
assert_ne!(metrics.font_size, 0.0, "font size cannot be 0");
|
|
||||||
self.metrics = metrics;
|
|
||||||
self.relayout(font_system);
|
|
||||||
self.shape_until_scroll(font_system);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the current [`Wrap`]
|
/// Get the current [`Wrap`]
|
||||||
|
|
@ -581,10 +568,27 @@ impl Buffer {
|
||||||
|
|
||||||
/// Set the current buffer dimensions
|
/// Set the current buffer dimensions
|
||||||
pub fn set_size(&mut self, font_system: &mut FontSystem, width: f32, height: f32) {
|
pub fn set_size(&mut self, font_system: &mut FontSystem, width: f32, height: f32) {
|
||||||
|
self.set_metrics_and_size(font_system, self.metrics, width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the current [`Metrics`] and buffer dimensions at the same time
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// Will panic if `metrics.font_size` is zero.
|
||||||
|
pub fn set_metrics_and_size(
|
||||||
|
&mut self,
|
||||||
|
font_system: &mut FontSystem,
|
||||||
|
metrics: Metrics,
|
||||||
|
width: f32,
|
||||||
|
height: f32,
|
||||||
|
) {
|
||||||
let clamped_width = width.max(0.0);
|
let clamped_width = width.max(0.0);
|
||||||
let clamped_height = height.max(0.0);
|
let clamped_height = height.max(0.0);
|
||||||
|
|
||||||
if clamped_width != self.width || clamped_height != self.height {
|
if metrics != self.metrics || clamped_width != self.width || clamped_height != self.height {
|
||||||
|
assert_ne!(metrics.font_size, 0.0, "font size cannot be 0");
|
||||||
|
self.metrics = metrics;
|
||||||
self.width = clamped_width;
|
self.width = clamped_width;
|
||||||
self.height = clamped_height;
|
self.height = clamped_height;
|
||||||
self.relayout(font_system);
|
self.relayout(font_system);
|
||||||
|
|
@ -618,7 +622,7 @@ impl Buffer {
|
||||||
attrs: Attrs,
|
attrs: Attrs,
|
||||||
shaping: Shaping,
|
shaping: Shaping,
|
||||||
) {
|
) {
|
||||||
self.set_rich_text(font_system, [(text, attrs)], shaping);
|
self.set_rich_text(font_system, [(text, attrs)], attrs, shaping);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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)
|
||||||
|
|
@ -634,6 +638,7 @@ impl Buffer {
|
||||||
/// ("hello, ", attrs),
|
/// ("hello, ", attrs),
|
||||||
/// ("cosmic\ntext", attrs.family(Family::Monospace)),
|
/// ("cosmic\ntext", attrs.family(Family::Monospace)),
|
||||||
/// ],
|
/// ],
|
||||||
|
/// attrs,
|
||||||
/// Shaping::Advanced,
|
/// Shaping::Advanced,
|
||||||
/// );
|
/// );
|
||||||
/// ```
|
/// ```
|
||||||
|
|
@ -641,13 +646,14 @@ impl Buffer {
|
||||||
&mut self,
|
&mut self,
|
||||||
font_system: &mut FontSystem,
|
font_system: &mut FontSystem,
|
||||||
spans: I,
|
spans: I,
|
||||||
|
default_attrs: Attrs,
|
||||||
shaping: Shaping,
|
shaping: Shaping,
|
||||||
) where
|
) where
|
||||||
I: IntoIterator<Item = (&'s str, Attrs<'r>)>,
|
I: IntoIterator<Item = (&'s str, Attrs<'r>)>,
|
||||||
{
|
{
|
||||||
self.lines.clear();
|
self.lines.clear();
|
||||||
|
|
||||||
let mut attrs_list = AttrsList::new(Attrs::new());
|
let mut attrs_list = AttrsList::new(default_attrs);
|
||||||
let mut line_string = String::new();
|
let mut line_string = String::new();
|
||||||
let mut end = 0;
|
let mut end = 0;
|
||||||
let (string, spans_data): (String, Vec<_>) = spans
|
let (string, spans_data): (String, Vec<_>) = spans
|
||||||
|
|
@ -676,7 +682,7 @@ impl Buffer {
|
||||||
// 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(),
|
||||||
AttrsList::new(Attrs::new()),
|
AttrsList::new(default_attrs),
|
||||||
shaping,
|
shaping,
|
||||||
));
|
));
|
||||||
break;
|
break;
|
||||||
|
|
@ -690,7 +696,10 @@ impl Buffer {
|
||||||
let text_start = line_string.len();
|
let text_start = line_string.len();
|
||||||
line_string.push_str(text);
|
line_string.push_str(text);
|
||||||
let text_end = line_string.len();
|
let text_end = line_string.len();
|
||||||
attrs_list.add_span(text_start..text_end, *attrs);
|
// Only add attrs if they don't match the defaults
|
||||||
|
if *attrs != attrs_list.defaults() {
|
||||||
|
attrs_list.add_span(text_start..text_end, *attrs);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// we know that at the end of a line,
|
// we know that at the end of a line,
|
||||||
|
|
@ -705,7 +714,7 @@ impl Buffer {
|
||||||
if maybe_line.is_some() {
|
if maybe_line.is_some() {
|
||||||
// finalize this line and start a new line
|
// finalize this line and start a new line
|
||||||
let prev_attrs_list =
|
let prev_attrs_list =
|
||||||
core::mem::replace(&mut attrs_list, AttrsList::new(Attrs::new()));
|
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, prev_attrs_list, shaping);
|
||||||
self.lines.push(buffer_line);
|
self.lines.push(buffer_line);
|
||||||
|
|
@ -923,6 +932,16 @@ impl<'a> BorrowedWithFontSystem<'a, Buffer> {
|
||||||
self.inner.set_size(self.font_system, width, height);
|
self.inner.set_size(self.font_system, width, height);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set the current [`Metrics`] and buffer dimensions at the same time
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// Will panic if `metrics.font_size` is zero.
|
||||||
|
pub fn set_metrics_and_size(&mut self, metrics: Metrics, width: f32, height: f32) {
|
||||||
|
self.inner
|
||||||
|
.set_metrics_and_size(self.font_system, metrics, width, height);
|
||||||
|
}
|
||||||
|
|
||||||
/// Set text of buffer, using provided attributes for each line by default
|
/// Set text of buffer, using provided attributes for each line by default
|
||||||
pub fn set_text(&mut self, text: &str, attrs: Attrs, shaping: Shaping) {
|
pub fn set_text(&mut self, text: &str, attrs: Attrs, shaping: Shaping) {
|
||||||
self.inner.set_text(self.font_system, text, attrs, shaping);
|
self.inner.set_text(self.font_system, text, attrs, shaping);
|
||||||
|
|
@ -941,14 +960,16 @@ impl<'a> BorrowedWithFontSystem<'a, Buffer> {
|
||||||
/// ("hello, ", attrs),
|
/// ("hello, ", attrs),
|
||||||
/// ("cosmic\ntext", attrs.family(Family::Monospace)),
|
/// ("cosmic\ntext", attrs.family(Family::Monospace)),
|
||||||
/// ],
|
/// ],
|
||||||
|
/// attrs,
|
||||||
/// Shaping::Advanced,
|
/// Shaping::Advanced,
|
||||||
/// );
|
/// );
|
||||||
/// ```
|
/// ```
|
||||||
pub fn set_rich_text<'r, 's, I>(&mut self, spans: I, shaping: Shaping)
|
pub fn set_rich_text<'r, 's, I>(&mut self, spans: I, default_attrs: Attrs, shaping: Shaping)
|
||||||
where
|
where
|
||||||
I: IntoIterator<Item = (&'s str, Attrs<'r>)>,
|
I: IntoIterator<Item = (&'s str, Attrs<'r>)>,
|
||||||
{
|
{
|
||||||
self.inner.set_rich_text(self.font_system, spans, shaping);
|
self.inner
|
||||||
|
.set_rich_text(self.font_system, spans, default_attrs, shaping);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draw the buffer
|
/// Draw the buffer
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
|
|
||||||
#[cfg(not(feature = "std"))]
|
#[cfg(not(feature = "std"))]
|
||||||
use alloc::string::String;
|
use alloc::string::{String, ToString};
|
||||||
use core::{
|
use core::{
|
||||||
cmp::{self, Ordering},
|
cmp::{self, Ordering},
|
||||||
iter::once,
|
iter::once,
|
||||||
|
|
@ -11,8 +11,8 @@ use unicode_segmentation::UnicodeSegmentation;
|
||||||
#[cfg(feature = "swash")]
|
#[cfg(feature = "swash")]
|
||||||
use crate::Color;
|
use crate::Color;
|
||||||
use crate::{
|
use crate::{
|
||||||
Action, Affinity, AttrsList, Buffer, BufferLine, Cursor, Edit, FontSystem, LayoutCursor,
|
Action, Affinity, AttrsList, Buffer, BufferLine, Change, ChangeItem, Cursor, Edit, FontSystem,
|
||||||
Shaping,
|
LayoutCursor, Shaping,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// A wrapper of [`Buffer`] for easy editing
|
/// A wrapper of [`Buffer`] for easy editing
|
||||||
|
|
@ -23,6 +23,9 @@ pub struct Editor {
|
||||||
cursor_x_opt: Option<i32>,
|
cursor_x_opt: Option<i32>,
|
||||||
select_opt: Option<Cursor>,
|
select_opt: Option<Cursor>,
|
||||||
cursor_moved: bool,
|
cursor_moved: bool,
|
||||||
|
auto_indent: bool,
|
||||||
|
tab_width: u16,
|
||||||
|
change: Option<Change>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Editor {
|
impl Editor {
|
||||||
|
|
@ -34,6 +37,9 @@ impl Editor {
|
||||||
cursor_x_opt: None,
|
cursor_x_opt: None,
|
||||||
select_opt: None,
|
select_opt: None,
|
||||||
cursor_moved: false,
|
cursor_moved: false,
|
||||||
|
auto_indent: false,
|
||||||
|
tab_width: 4,
|
||||||
|
change: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -70,6 +76,155 @@ impl Editor {
|
||||||
self.buffer.set_redraw(true);
|
self.buffer.set_redraw(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn delete_range(&mut self, start: Cursor, end: Cursor) {
|
||||||
|
// Collect removed data for change tracking
|
||||||
|
let mut change_text = String::new();
|
||||||
|
|
||||||
|
// Delete the selection from the last line
|
||||||
|
let end_line_opt = if end.line > start.line {
|
||||||
|
// Get part of line after selection
|
||||||
|
let after = self.buffer.lines[end.line].split_off(end.index);
|
||||||
|
|
||||||
|
// Remove end line
|
||||||
|
let removed = self.buffer.lines.remove(end.line);
|
||||||
|
change_text.insert_str(0, removed.text());
|
||||||
|
|
||||||
|
Some(after)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
// Delete interior lines (in reverse for safety)
|
||||||
|
for line_i in (start.line + 1..end.line).rev() {
|
||||||
|
let removed = self.buffer.lines.remove(line_i);
|
||||||
|
change_text.insert_str(0, removed.text());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the selection from the first line
|
||||||
|
{
|
||||||
|
// Get part after selection if start line is also end line
|
||||||
|
let after_opt = if start.line == end.line {
|
||||||
|
Some(self.buffer.lines[start.line].split_off(end.index))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
// Delete selected part of line
|
||||||
|
let removed = self.buffer.lines[start.line].split_off(start.index);
|
||||||
|
change_text.insert_str(0, removed.text());
|
||||||
|
|
||||||
|
// Re-add part of line after selection
|
||||||
|
if let Some(after) = after_opt {
|
||||||
|
self.buffer.lines[start.line].append(after);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-add valid parts of end line
|
||||||
|
if let Some(end_line) = end_line_opt {
|
||||||
|
self.buffer.lines[start.line].append(end_line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(ref mut change) = self.change {
|
||||||
|
let item = ChangeItem {
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
text: change_text,
|
||||||
|
insert: false,
|
||||||
|
};
|
||||||
|
change.items.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn insert_at(
|
||||||
|
&mut self,
|
||||||
|
mut cursor: Cursor,
|
||||||
|
data: &str,
|
||||||
|
attrs_list: Option<AttrsList>,
|
||||||
|
) -> Cursor {
|
||||||
|
let mut remaining_split_len = data.len();
|
||||||
|
if remaining_split_len == 0 {
|
||||||
|
return cursor;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save cursor for change tracking
|
||||||
|
let start = cursor;
|
||||||
|
|
||||||
|
let line: &mut BufferLine = &mut self.buffer.lines[cursor.line];
|
||||||
|
let insert_line = cursor.line + 1;
|
||||||
|
|
||||||
|
// Collect text after insertion as a line
|
||||||
|
let after: BufferLine = line.split_off(cursor.index);
|
||||||
|
let after_len = after.text().len();
|
||||||
|
|
||||||
|
// Collect attributes
|
||||||
|
let mut final_attrs = attrs_list.unwrap_or_else(|| {
|
||||||
|
AttrsList::new(line.attrs_list().get_span(cursor.index.saturating_sub(1)))
|
||||||
|
});
|
||||||
|
|
||||||
|
// Append the inserted text, line by line
|
||||||
|
// we want to see a blank entry if the string ends with a newline
|
||||||
|
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() {
|
||||||
|
let mut these_attrs = final_attrs.split_off(data_line.len());
|
||||||
|
remaining_split_len -= data_line.len();
|
||||||
|
core::mem::swap(&mut these_attrs, &mut final_attrs);
|
||||||
|
line.append(BufferLine::new(
|
||||||
|
data_line
|
||||||
|
.strip_suffix(char::is_control)
|
||||||
|
.unwrap_or(data_line),
|
||||||
|
these_attrs,
|
||||||
|
Shaping::Advanced,
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
panic!("str::lines() did not yield any elements");
|
||||||
|
}
|
||||||
|
if let Some(data_line) = lines_iter.next_back() {
|
||||||
|
remaining_split_len -= data_line.len();
|
||||||
|
let mut tmp = BufferLine::new(
|
||||||
|
data_line
|
||||||
|
.strip_suffix(char::is_control)
|
||||||
|
.unwrap_or(data_line),
|
||||||
|
final_attrs.split_off(remaining_split_len),
|
||||||
|
Shaping::Advanced,
|
||||||
|
);
|
||||||
|
tmp.append(after);
|
||||||
|
self.buffer.lines.insert(insert_line, tmp);
|
||||||
|
cursor.line += 1;
|
||||||
|
} else {
|
||||||
|
line.append(after);
|
||||||
|
}
|
||||||
|
for data_line in lines_iter.rev() {
|
||||||
|
remaining_split_len -= data_line.len();
|
||||||
|
let tmp = BufferLine::new(
|
||||||
|
data_line
|
||||||
|
.strip_suffix(char::is_control)
|
||||||
|
.unwrap_or(data_line),
|
||||||
|
final_attrs.split_off(remaining_split_len),
|
||||||
|
Shaping::Advanced,
|
||||||
|
);
|
||||||
|
self.buffer.lines.insert(insert_line, tmp);
|
||||||
|
cursor.line += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(remaining_split_len, 0);
|
||||||
|
|
||||||
|
// Append the text after insertion
|
||||||
|
cursor.index = self.buffer.lines[cursor.line].text().len() - after_len;
|
||||||
|
|
||||||
|
if let Some(ref mut change) = self.change {
|
||||||
|
let item = ChangeItem {
|
||||||
|
start,
|
||||||
|
end: cursor,
|
||||||
|
text: data.to_string(),
|
||||||
|
insert: true,
|
||||||
|
};
|
||||||
|
change.items.push(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Edit for Editor {
|
impl Edit for Editor {
|
||||||
|
|
@ -86,7 +241,12 @@ impl Edit for Editor {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_cursor(&mut self, cursor: Cursor) {
|
fn set_cursor(&mut self, cursor: Cursor) {
|
||||||
self.cursor = cursor;
|
if self.cursor != cursor {
|
||||||
|
self.cursor = cursor;
|
||||||
|
self.cursor_moved = true;
|
||||||
|
self.cursor_x_opt = None;
|
||||||
|
self.buffer.set_redraw(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn select_opt(&self) -> Option<Cursor> {
|
fn select_opt(&self) -> Option<Cursor> {
|
||||||
|
|
@ -100,6 +260,29 @@ impl Edit for Editor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn auto_indent(&self) -> bool {
|
||||||
|
self.auto_indent
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_auto_indent(&mut self, auto_indent: bool) {
|
||||||
|
self.auto_indent = auto_indent;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tab_width(&self) -> u16 {
|
||||||
|
self.tab_width
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_tab_width(&mut self, tab_width: u16) {
|
||||||
|
// A tab width of 0 is not allowed
|
||||||
|
if tab_width == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if self.tab_width != tab_width {
|
||||||
|
self.tab_width = tab_width;
|
||||||
|
self.buffer.set_redraw(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn shape_as_needed(&mut self, font_system: &mut FontSystem) {
|
fn shape_as_needed(&mut self, font_system: &mut FontSystem) {
|
||||||
if self.cursor_moved {
|
if self.cursor_moved {
|
||||||
self.buffer.shape_until_cursor(font_system, self.cursor);
|
self.buffer.shape_until_cursor(font_system, self.cursor);
|
||||||
|
|
@ -176,123 +359,52 @@ impl Edit for Editor {
|
||||||
// Reset cursor to start of selection
|
// Reset cursor to start of selection
|
||||||
self.cursor = start;
|
self.cursor = start;
|
||||||
|
|
||||||
// Delete the selection from the last line
|
// Delete from start to end of selection
|
||||||
let end_line_opt = if end.line > start.line {
|
self.delete_range(start, end);
|
||||||
// Get part of line after selection
|
|
||||||
let after = self.buffer.lines[end.line].split_off(end.index);
|
|
||||||
|
|
||||||
// Remove end line
|
|
||||||
self.buffer.lines.remove(end.line);
|
|
||||||
|
|
||||||
Some(after)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
// Delete interior lines (in reverse for safety)
|
|
||||||
for line_i in (start.line + 1..end.line).rev() {
|
|
||||||
self.buffer.lines.remove(line_i);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete the selection from the first line
|
|
||||||
{
|
|
||||||
// Get part after selection if start line is also end line
|
|
||||||
let after_opt = if start.line == end.line {
|
|
||||||
Some(self.buffer.lines[start.line].split_off(end.index))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
// Delete selected part of line
|
|
||||||
self.buffer.lines[start.line].split_off(start.index);
|
|
||||||
|
|
||||||
// Re-add part of line after selection
|
|
||||||
if let Some(after) = after_opt {
|
|
||||||
self.buffer.lines[start.line].append(after);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Re-add valid parts of end line
|
|
||||||
if let Some(end_line) = end_line_opt {
|
|
||||||
self.buffer.lines[start.line].append(end_line);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
fn insert_string(&mut self, data: &str, attrs_list: Option<AttrsList>) {
|
fn insert_string(&mut self, data: &str, attrs_list: Option<AttrsList>) {
|
||||||
self.delete_selection();
|
self.delete_selection();
|
||||||
let mut remaining_split_len = data.len();
|
let new_cursor = self.insert_at(self.cursor, data, attrs_list);
|
||||||
if remaining_split_len == 0 {
|
self.set_cursor(new_cursor);
|
||||||
return;
|
}
|
||||||
|
|
||||||
|
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 => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
let line: &mut BufferLine = &mut self.buffer.lines[self.cursor.line];
|
for item in change.items.iter() {
|
||||||
let insert_line = self.cursor.line + 1;
|
//TODO: edit cursor if needed?
|
||||||
|
if item.insert {
|
||||||
// Collect text after insertion as a line
|
self.cursor = self.insert_at(item.start, &item.text, None);
|
||||||
let after: BufferLine = line.split_off(self.cursor.index);
|
} else {
|
||||||
let after_len = after.text().len();
|
self.cursor = item.start;
|
||||||
|
self.delete_range(item.start, item.end);
|
||||||
// Collect attributes
|
}
|
||||||
let mut final_attrs = attrs_list.unwrap_or_else(|| {
|
|
||||||
AttrsList::new(
|
|
||||||
line.attrs_list()
|
|
||||||
.get_span(self.cursor.index.saturating_sub(1)),
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
// Append the inserted text, line by line
|
|
||||||
// we want to see a blank entry if the string ends with a newline
|
|
||||||
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() {
|
|
||||||
let mut these_attrs = final_attrs.split_off(data_line.len());
|
|
||||||
remaining_split_len -= data_line.len();
|
|
||||||
core::mem::swap(&mut these_attrs, &mut final_attrs);
|
|
||||||
line.append(BufferLine::new(
|
|
||||||
data_line
|
|
||||||
.strip_suffix(char::is_control)
|
|
||||||
.unwrap_or(data_line),
|
|
||||||
these_attrs,
|
|
||||||
Shaping::Advanced,
|
|
||||||
));
|
|
||||||
} else {
|
|
||||||
panic!("str::lines() did not yield any elements");
|
|
||||||
}
|
|
||||||
if let Some(data_line) = lines_iter.next_back() {
|
|
||||||
remaining_split_len -= data_line.len();
|
|
||||||
let mut tmp = BufferLine::new(
|
|
||||||
data_line
|
|
||||||
.strip_suffix(char::is_control)
|
|
||||||
.unwrap_or(data_line),
|
|
||||||
final_attrs.split_off(remaining_split_len),
|
|
||||||
Shaping::Advanced,
|
|
||||||
);
|
|
||||||
tmp.append(after);
|
|
||||||
self.buffer.lines.insert(insert_line, tmp);
|
|
||||||
self.cursor.line += 1;
|
|
||||||
} else {
|
|
||||||
line.append(after);
|
|
||||||
}
|
|
||||||
for data_line in lines_iter.rev() {
|
|
||||||
remaining_split_len -= data_line.len();
|
|
||||||
let tmp = BufferLine::new(
|
|
||||||
data_line
|
|
||||||
.strip_suffix(char::is_control)
|
|
||||||
.unwrap_or(data_line),
|
|
||||||
final_attrs.split_off(remaining_split_len),
|
|
||||||
Shaping::Advanced,
|
|
||||||
);
|
|
||||||
self.buffer.lines.insert(insert_line, tmp);
|
|
||||||
self.cursor.line += 1;
|
|
||||||
}
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
assert_eq!(remaining_split_len, 0);
|
fn start_change(&mut self) {
|
||||||
|
if self.change.is_none() {
|
||||||
|
self.change = Some(Change::default());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Append the text after insertion
|
fn finish_change(&mut self) -> Option<Change> {
|
||||||
self.cursor.index = self.buffer.lines[self.cursor.line].text().len() - after_len;
|
self.change.take()
|
||||||
self.cursor_moved = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn action(&mut self, font_system: &mut FontSystem, action: Action) {
|
fn action(&mut self, font_system: &mut FontSystem, action: Action) {
|
||||||
|
|
@ -300,7 +412,7 @@ impl Edit for Editor {
|
||||||
|
|
||||||
match action {
|
match action {
|
||||||
Action::Previous => {
|
Action::Previous => {
|
||||||
let line = &mut self.buffer.lines[self.cursor.line];
|
let line = &self.buffer.lines[self.cursor.line];
|
||||||
if self.cursor.index > 0 {
|
if self.cursor.index > 0 {
|
||||||
// Find previous character index
|
// Find previous character index
|
||||||
let mut prev_index = 0;
|
let mut prev_index = 0;
|
||||||
|
|
@ -324,7 +436,7 @@ impl Edit for Editor {
|
||||||
self.cursor_x_opt = None;
|
self.cursor_x_opt = None;
|
||||||
}
|
}
|
||||||
Action::Next => {
|
Action::Next => {
|
||||||
let line = &mut self.buffer.lines[self.cursor.line];
|
let line = &self.buffer.lines[self.cursor.line];
|
||||||
if self.cursor.index < line.text().len() {
|
if self.cursor.index < line.text().len() {
|
||||||
for (i, c) in line.text().grapheme_indices(true) {
|
for (i, c) in line.text().grapheme_indices(true) {
|
||||||
if i == self.cursor.index {
|
if i == self.cursor.index {
|
||||||
|
|
@ -343,9 +455,9 @@ impl Edit for Editor {
|
||||||
self.cursor_x_opt = None;
|
self.cursor_x_opt = None;
|
||||||
}
|
}
|
||||||
Action::Left => {
|
Action::Left => {
|
||||||
let rtl_opt = self.buffer.lines[self.cursor.line]
|
let rtl_opt = self
|
||||||
.shape_opt()
|
.buffer
|
||||||
.as_ref()
|
.line_shape(font_system, self.cursor.line)
|
||||||
.map(|shape| shape.rtl);
|
.map(|shape| shape.rtl);
|
||||||
if let Some(rtl) = rtl_opt {
|
if let Some(rtl) = rtl_opt {
|
||||||
if rtl {
|
if rtl {
|
||||||
|
|
@ -356,9 +468,9 @@ impl Edit for Editor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Action::Right => {
|
Action::Right => {
|
||||||
let rtl_opt = self.buffer.lines[self.cursor.line]
|
let rtl_opt = self
|
||||||
.shape_opt()
|
.buffer
|
||||||
.as_ref()
|
.line_shape(font_system, self.cursor.line)
|
||||||
.map(|shape| shape.rtl);
|
.map(|shape| shape.rtl);
|
||||||
if let Some(rtl) = rtl_opt {
|
if let Some(rtl) = rtl_opt {
|
||||||
if rtl {
|
if rtl {
|
||||||
|
|
@ -426,6 +538,17 @@ impl Edit for Editor {
|
||||||
self.set_layout_cursor(font_system, cursor);
|
self.set_layout_cursor(font_system, cursor);
|
||||||
self.cursor_x_opt = None;
|
self.cursor_x_opt = None;
|
||||||
}
|
}
|
||||||
|
Action::SoftHome => {
|
||||||
|
let line = &self.buffer.lines[self.cursor.line];
|
||||||
|
self.cursor.index = line
|
||||||
|
.text()
|
||||||
|
.char_indices()
|
||||||
|
.filter_map(|(i, c)| if c.is_whitespace() { None } else { Some(i) })
|
||||||
|
.next()
|
||||||
|
.unwrap_or(0);
|
||||||
|
self.buffer.set_redraw(true);
|
||||||
|
self.cursor_x_opt = None;
|
||||||
|
}
|
||||||
Action::End => {
|
Action::End => {
|
||||||
let mut cursor = self.buffer.layout_cursor(&self.cursor);
|
let mut cursor = self.buffer.layout_cursor(&self.cursor);
|
||||||
cursor.glyph = usize::max_value();
|
cursor.glyph = usize::max_value();
|
||||||
|
|
@ -483,82 +606,222 @@ impl Edit for Editor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Action::Enter => {
|
Action::Enter => {
|
||||||
self.delete_selection();
|
//TODO: what about indenting more after opening brackets or parentheses?
|
||||||
|
if self.auto_indent {
|
||||||
|
let mut string = String::from("\n");
|
||||||
|
{
|
||||||
|
let line = &self.buffer.lines[self.cursor.line];
|
||||||
|
let text = line.text();
|
||||||
|
for c in text.chars() {
|
||||||
|
if c.is_whitespace() {
|
||||||
|
string.push(c);
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.insert_string(&string, None);
|
||||||
|
} else {
|
||||||
|
self.insert_string("\n", None);
|
||||||
|
}
|
||||||
|
|
||||||
let new_line = self.buffer.lines[self.cursor.line].split_off(self.cursor.index);
|
// Ensure line is properly shaped and laid out (for potential immediate commands)
|
||||||
|
self.buffer.line_layout(font_system, self.cursor.line);
|
||||||
self.cursor.line += 1;
|
|
||||||
self.cursor.index = 0;
|
|
||||||
|
|
||||||
self.buffer.lines.insert(self.cursor.line, new_line);
|
|
||||||
}
|
}
|
||||||
Action::Backspace => {
|
Action::Backspace => {
|
||||||
if self.delete_selection() {
|
if self.delete_selection() {
|
||||||
// Deleted selection
|
// Deleted selection
|
||||||
} else if self.cursor.index > 0 {
|
} else {
|
||||||
let line = &mut self.buffer.lines[self.cursor.line];
|
// Save current cursor as end
|
||||||
|
let end = self.cursor;
|
||||||
|
|
||||||
// Get text line after cursor
|
if self.cursor.index > 0 {
|
||||||
let after = line.split_off(self.cursor.index);
|
// Move cursor to previous character index
|
||||||
|
let line = &self.buffer.lines[self.cursor.line];
|
||||||
// Find previous character index
|
self.cursor.index = line.text()[..self.cursor.index]
|
||||||
let mut prev_index = 0;
|
.char_indices()
|
||||||
for (i, _) in line.text().char_indices() {
|
.next_back()
|
||||||
if i < self.cursor.index {
|
.map_or(0, |(i, _)| i);
|
||||||
prev_index = i;
|
} else if self.cursor.line > 0 {
|
||||||
} else {
|
// Move cursor to previous line
|
||||||
break;
|
self.cursor.line -= 1;
|
||||||
}
|
let line = &self.buffer.lines[self.cursor.line];
|
||||||
|
self.cursor.index = line.text().len();
|
||||||
}
|
}
|
||||||
|
|
||||||
self.cursor.index = prev_index;
|
if self.cursor != end {
|
||||||
|
// Delete range
|
||||||
// Remove character
|
self.delete_range(self.cursor, end);
|
||||||
line.split_off(self.cursor.index);
|
}
|
||||||
|
|
||||||
// Add text after cursor
|
|
||||||
line.append(after);
|
|
||||||
} else if self.cursor.line > 0 {
|
|
||||||
let mut line_index = self.cursor.line;
|
|
||||||
let old_line = self.buffer.lines.remove(line_index);
|
|
||||||
line_index -= 1;
|
|
||||||
|
|
||||||
let line = &mut self.buffer.lines[line_index];
|
|
||||||
|
|
||||||
self.cursor.line = line_index;
|
|
||||||
self.cursor.index = line.text().len();
|
|
||||||
|
|
||||||
line.append(old_line);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Action::Delete => {
|
Action::Delete => {
|
||||||
if self.delete_selection() {
|
if self.delete_selection() {
|
||||||
// Deleted selection
|
// Deleted selection
|
||||||
} else if self.cursor.index < self.buffer.lines[self.cursor.line].text().len() {
|
} else {
|
||||||
let line = &mut self.buffer.lines[self.cursor.line];
|
// Save current cursor as end
|
||||||
|
let mut end = self.cursor;
|
||||||
|
|
||||||
let range_opt = line
|
if self.cursor.index < self.buffer.lines[self.cursor.line].text().len() {
|
||||||
.text()
|
let line = &self.buffer.lines[self.cursor.line];
|
||||||
.grapheme_indices(true)
|
|
||||||
.take_while(|(i, _)| *i <= self.cursor.index)
|
|
||||||
.last()
|
|
||||||
.map(|(i, c)| i..(i + c.len()));
|
|
||||||
|
|
||||||
if let Some(range) = range_opt {
|
let range_opt = line
|
||||||
self.cursor.index = range.start;
|
.text()
|
||||||
|
.grapheme_indices(true)
|
||||||
|
.take_while(|(i, _)| *i <= self.cursor.index)
|
||||||
|
.last()
|
||||||
|
.map(|(i, c)| i..(i + c.len()));
|
||||||
|
|
||||||
// Get text after deleted EGC
|
if let Some(range) = range_opt {
|
||||||
let after = line.split_off(range.end);
|
self.cursor.index = range.start;
|
||||||
|
end.index = range.end;
|
||||||
// Delete EGC
|
}
|
||||||
line.split_off(range.start);
|
} else if self.cursor.line + 1 < self.buffer.lines.len() {
|
||||||
|
end.line += 1;
|
||||||
// Add text after deleted EGC
|
end.index = 0;
|
||||||
line.append(after);
|
|
||||||
}
|
}
|
||||||
} else if self.cursor.line + 1 < self.buffer.lines.len() {
|
|
||||||
let old_line = self.buffer.lines.remove(self.cursor.line + 1);
|
if self.cursor != end {
|
||||||
self.buffer.lines[self.cursor.line].append(old_line);
|
self.delete_range(self.cursor, end);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Action::Indent => {
|
||||||
|
// Get start and end of selection
|
||||||
|
let (start, end) = match self.select_opt {
|
||||||
|
Some(select) => match select.line.cmp(&self.cursor.line) {
|
||||||
|
cmp::Ordering::Greater => (self.cursor, select),
|
||||||
|
cmp::Ordering::Less => (select, self.cursor),
|
||||||
|
cmp::Ordering::Equal => {
|
||||||
|
/* select.line == self.cursor.line */
|
||||||
|
if select.index < self.cursor.index {
|
||||||
|
(select, self.cursor)
|
||||||
|
} else {
|
||||||
|
/* select.index >= self.cursor.index */
|
||||||
|
(self.cursor, select)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => (self.cursor, self.cursor),
|
||||||
|
};
|
||||||
|
|
||||||
|
// For every line in selection
|
||||||
|
let tab_width: usize = self.tab_width.into();
|
||||||
|
for line_i in start.line..=end.line {
|
||||||
|
// Determine indexes of last indent and first character after whitespace
|
||||||
|
let mut after_whitespace;
|
||||||
|
let mut required_indent = 0;
|
||||||
|
{
|
||||||
|
let line = &self.buffer.lines[line_i];
|
||||||
|
let text = line.text();
|
||||||
|
// Default to end of line if no non-whitespace found
|
||||||
|
after_whitespace = text.len();
|
||||||
|
for (count, (index, c)) in text.char_indices().enumerate() {
|
||||||
|
if !c.is_whitespace() {
|
||||||
|
after_whitespace = index;
|
||||||
|
required_indent = tab_width - (count % tab_width);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No indent required (not possible?)
|
||||||
|
if required_indent == 0 {
|
||||||
|
required_indent = tab_width;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.insert_at(
|
||||||
|
Cursor::new(line_i, after_whitespace),
|
||||||
|
&" ".repeat(required_indent),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Adjust cursor
|
||||||
|
if self.cursor.line == line_i {
|
||||||
|
//TODO: should we be forcing cursor index to current indent location?
|
||||||
|
if self.cursor.index < after_whitespace {
|
||||||
|
self.cursor.index = after_whitespace;
|
||||||
|
}
|
||||||
|
self.cursor.index += required_indent;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adjust selection
|
||||||
|
if let Some(ref mut select) = self.select_opt {
|
||||||
|
if select.line == line_i && select.index >= after_whitespace {
|
||||||
|
select.index += required_indent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request redraw
|
||||||
|
self.buffer.set_redraw(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Action::Unindent => {
|
||||||
|
// Get start and end of selection
|
||||||
|
let (start, end) = match self.select_opt {
|
||||||
|
Some(select) => match select.line.cmp(&self.cursor.line) {
|
||||||
|
cmp::Ordering::Greater => (self.cursor, select),
|
||||||
|
cmp::Ordering::Less => (select, self.cursor),
|
||||||
|
cmp::Ordering::Equal => {
|
||||||
|
/* select.line == self.cursor.line */
|
||||||
|
if select.index < self.cursor.index {
|
||||||
|
(select, self.cursor)
|
||||||
|
} else {
|
||||||
|
/* select.index >= self.cursor.index */
|
||||||
|
(self.cursor, select)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => (self.cursor, self.cursor),
|
||||||
|
};
|
||||||
|
|
||||||
|
// For every line in selection
|
||||||
|
let tab_width: usize = self.tab_width.into();
|
||||||
|
for line_i in start.line..=end.line {
|
||||||
|
// Determine indexes of last indent and first character after whitespace
|
||||||
|
let mut last_indent = 0;
|
||||||
|
let mut after_whitespace;
|
||||||
|
{
|
||||||
|
let line = &self.buffer.lines[line_i];
|
||||||
|
let text = line.text();
|
||||||
|
// Default to end of line if no non-whitespace found
|
||||||
|
after_whitespace = text.len();
|
||||||
|
for (count, (index, c)) in text.char_indices().enumerate() {
|
||||||
|
if !c.is_whitespace() {
|
||||||
|
after_whitespace = index;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if count % tab_width == 0 {
|
||||||
|
last_indent = index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No de-indent required
|
||||||
|
if last_indent == after_whitespace {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete one indent
|
||||||
|
self.delete_range(
|
||||||
|
Cursor::new(line_i, last_indent),
|
||||||
|
Cursor::new(line_i, after_whitespace),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Adjust cursor
|
||||||
|
if self.cursor.line == line_i && self.cursor.index > last_indent {
|
||||||
|
self.cursor.index -= after_whitespace - last_indent;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adjust selection
|
||||||
|
if let Some(ref mut select) = self.select_opt {
|
||||||
|
if select.line == line_i && select.index > last_indent {
|
||||||
|
select.index -= after_whitespace - last_indent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request redraw
|
||||||
|
self.buffer.set_redraw(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Action::Click { x, y } => {
|
Action::Click { x, y } => {
|
||||||
|
|
@ -594,7 +857,7 @@ impl Edit for Editor {
|
||||||
self.buffer.set_scroll(scroll);
|
self.buffer.set_scroll(scroll);
|
||||||
}
|
}
|
||||||
Action::PreviousWord => {
|
Action::PreviousWord => {
|
||||||
let line: &mut BufferLine = &mut self.buffer.lines[self.cursor.line];
|
let line = &self.buffer.lines[self.cursor.line];
|
||||||
if self.cursor.index > 0 {
|
if self.cursor.index > 0 {
|
||||||
self.cursor.index = line
|
self.cursor.index = line
|
||||||
.text()
|
.text()
|
||||||
|
|
@ -613,7 +876,7 @@ impl Edit for Editor {
|
||||||
self.cursor_x_opt = None;
|
self.cursor_x_opt = None;
|
||||||
}
|
}
|
||||||
Action::NextWord => {
|
Action::NextWord => {
|
||||||
let line: &mut BufferLine = &mut self.buffer.lines[self.cursor.line];
|
let line = &self.buffer.lines[self.cursor.line];
|
||||||
if self.cursor.index < line.text().len() {
|
if self.cursor.index < line.text().len() {
|
||||||
self.cursor.index = line
|
self.cursor.index = line
|
||||||
.text()
|
.text()
|
||||||
|
|
@ -631,9 +894,9 @@ impl Edit for Editor {
|
||||||
self.cursor_x_opt = None;
|
self.cursor_x_opt = None;
|
||||||
}
|
}
|
||||||
Action::LeftWord => {
|
Action::LeftWord => {
|
||||||
let rtl_opt = self.buffer.lines[self.cursor.line]
|
let rtl_opt = self
|
||||||
.shape_opt()
|
.buffer
|
||||||
.as_ref()
|
.line_shape(font_system, self.cursor.line)
|
||||||
.map(|shape| shape.rtl);
|
.map(|shape| shape.rtl);
|
||||||
if let Some(rtl) = rtl_opt {
|
if let Some(rtl) = rtl_opt {
|
||||||
if rtl {
|
if rtl {
|
||||||
|
|
@ -644,9 +907,9 @@ impl Edit for Editor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Action::RightWord => {
|
Action::RightWord => {
|
||||||
let rtl_opt = self.buffer.lines[self.cursor.line]
|
let rtl_opt = self
|
||||||
.shape_opt()
|
.buffer
|
||||||
.as_ref()
|
.line_shape(font_system, self.cursor.line)
|
||||||
.map(|shape| shape.rtl);
|
.map(|shape| shape.rtl);
|
||||||
if let Some(rtl) = rtl_opt {
|
if let Some(rtl) = rtl_opt {
|
||||||
if rtl {
|
if rtl {
|
||||||
|
|
@ -666,6 +929,11 @@ impl Edit for Editor {
|
||||||
self.cursor.index = self.buffer.lines[self.cursor.line].text().len();
|
self.cursor.index = self.buffer.lines[self.cursor.line].text().len();
|
||||||
self.cursor_x_opt = None;
|
self.cursor_x_opt = None;
|
||||||
}
|
}
|
||||||
|
Action::GotoLine(line) => {
|
||||||
|
let mut cursor = self.buffer.layout_cursor(&self.cursor);
|
||||||
|
cursor.line = line;
|
||||||
|
self.set_layout_cursor(font_system, cursor);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if old_cursor != self.cursor {
|
if old_cursor != self.cursor {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
#[cfg(not(feature = "std"))]
|
#[cfg(not(feature = "std"))]
|
||||||
use alloc::string::String;
|
use alloc::{string::String, vec::Vec};
|
||||||
|
|
||||||
#[cfg(feature = "swash")]
|
#[cfg(feature = "swash")]
|
||||||
use crate::Color;
|
use crate::Color;
|
||||||
|
|
@ -35,6 +35,8 @@ pub enum Action {
|
||||||
Down,
|
Down,
|
||||||
/// Move cursor to start of line
|
/// Move cursor to start of line
|
||||||
Home,
|
Home,
|
||||||
|
/// Move cursor to start of line, skipping whitespace
|
||||||
|
SoftHome,
|
||||||
/// Move cursor to end of line
|
/// Move cursor to end of line
|
||||||
End,
|
End,
|
||||||
/// Move cursor to start of paragraph
|
/// Move cursor to start of paragraph
|
||||||
|
|
@ -57,12 +59,24 @@ pub enum Action {
|
||||||
Backspace,
|
Backspace,
|
||||||
/// Delete text in front of cursor
|
/// Delete text in front of cursor
|
||||||
Delete,
|
Delete,
|
||||||
|
// Indent text (typically Tab)
|
||||||
|
Indent,
|
||||||
|
// Unindent text (typically Shift+Tab)
|
||||||
|
Unindent,
|
||||||
/// Mouse click at specified position
|
/// Mouse click at specified position
|
||||||
Click { x: i32, y: i32 },
|
Click {
|
||||||
|
x: i32,
|
||||||
|
y: i32,
|
||||||
|
},
|
||||||
/// Mouse drag to specified position
|
/// Mouse drag to specified position
|
||||||
Drag { x: i32, y: i32 },
|
Drag {
|
||||||
|
x: i32,
|
||||||
|
y: i32,
|
||||||
|
},
|
||||||
/// Scroll specified number of lines
|
/// Scroll specified number of lines
|
||||||
Scroll { lines: i32 },
|
Scroll {
|
||||||
|
lines: i32,
|
||||||
|
},
|
||||||
/// Move cursor to previous word boundary
|
/// Move cursor to previous word boundary
|
||||||
PreviousWord,
|
PreviousWord,
|
||||||
/// Move cursor to next word boundary
|
/// Move cursor to next word boundary
|
||||||
|
|
@ -75,6 +89,45 @@ pub enum Action {
|
||||||
BufferStart,
|
BufferStart,
|
||||||
/// Move cursor to the end of the document
|
/// Move cursor to the end of the document
|
||||||
BufferEnd,
|
BufferEnd,
|
||||||
|
/// Move cursor to specific line
|
||||||
|
GotoLine(usize),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A unique change to an editor
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct ChangeItem {
|
||||||
|
/// Cursor indicating start of change
|
||||||
|
pub start: Cursor,
|
||||||
|
/// Cursor indicating end of change
|
||||||
|
pub end: Cursor,
|
||||||
|
/// Text to be inserted or deleted
|
||||||
|
pub text: String,
|
||||||
|
/// Insert if true, delete if false
|
||||||
|
pub insert: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ChangeItem {
|
||||||
|
// Reverse change item (in place)
|
||||||
|
pub fn reverse(&mut self) {
|
||||||
|
self.insert = !self.insert;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A set of change items grouped into one logical change
|
||||||
|
#[derive(Clone, Debug, Default)]
|
||||||
|
pub struct Change {
|
||||||
|
/// Change items grouped into one change
|
||||||
|
pub items: Vec<ChangeItem>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Change {
|
||||||
|
// Reverse change (in place)
|
||||||
|
pub fn reverse(&mut self) {
|
||||||
|
self.items.reverse();
|
||||||
|
for item in self.items.iter_mut() {
|
||||||
|
item.reverse();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A trait to allow easy replacements of [`Editor`], like `SyntaxEditor`
|
/// A trait to allow easy replacements of [`Editor`], like `SyntaxEditor`
|
||||||
|
|
@ -111,6 +164,18 @@ pub trait Edit {
|
||||||
/// Set the current selection position
|
/// Set the current selection position
|
||||||
fn set_select_opt(&mut self, select_opt: Option<Cursor>);
|
fn set_select_opt(&mut self, select_opt: Option<Cursor>);
|
||||||
|
|
||||||
|
/// Get the current automatic indentation setting
|
||||||
|
fn auto_indent(&self) -> bool;
|
||||||
|
|
||||||
|
/// Enable or disable automatic indentation
|
||||||
|
fn set_auto_indent(&mut self, auto_indent: bool);
|
||||||
|
|
||||||
|
/// Get the current tab width
|
||||||
|
fn tab_width(&self) -> u16;
|
||||||
|
|
||||||
|
/// Set the current tab width. A `tab_width` of 0 is not allowed, and will be ignored
|
||||||
|
fn set_tab_width(&mut self, tab_width: u16);
|
||||||
|
|
||||||
/// Shape lines until scroll, after adjusting scroll if the cursor moved
|
/// Shape lines until scroll, after adjusting scroll if the cursor moved
|
||||||
fn shape_as_needed(&mut self, font_system: &mut FontSystem);
|
fn shape_as_needed(&mut self, font_system: &mut FontSystem);
|
||||||
|
|
||||||
|
|
@ -125,6 +190,15 @@ pub trait Edit {
|
||||||
/// attributes, or with the previous character's attributes if None is given.
|
/// attributes, or with the previous character's attributes if None is given.
|
||||||
fn insert_string(&mut self, data: &str, attrs_list: Option<AttrsList>);
|
fn insert_string(&mut self, data: &str, attrs_list: Option<AttrsList>);
|
||||||
|
|
||||||
|
/// Apply a change
|
||||||
|
fn apply_change(&mut self, change: &Change) -> bool;
|
||||||
|
|
||||||
|
/// Start collecting change
|
||||||
|
fn start_change(&mut self);
|
||||||
|
|
||||||
|
/// Get completed change
|
||||||
|
fn finish_change(&mut self) -> Option<Change>;
|
||||||
|
|
||||||
/// Perform an [Action] on the editor
|
/// Perform an [Action] on the editor
|
||||||
fn action(&mut self, font_system: &mut FontSystem, action: Action);
|
fn action(&mut self, font_system: &mut FontSystem, action: Action);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,15 +3,17 @@ use alloc::{string::String, vec::Vec};
|
||||||
#[cfg(feature = "std")]
|
#[cfg(feature = "std")]
|
||||||
use std::{fs, io, path::Path};
|
use std::{fs, io, path::Path};
|
||||||
use syntect::highlighting::{
|
use syntect::highlighting::{
|
||||||
FontStyle, HighlightState, Highlighter, RangedHighlightIterator, Theme, ThemeSet,
|
FontStyle, HighlightState, Highlighter, RangedHighlightIterator, ThemeSet,
|
||||||
};
|
};
|
||||||
use syntect::parsing::{ParseState, ScopeStack, SyntaxReference, SyntaxSet};
|
use syntect::parsing::{ParseState, ScopeStack, SyntaxReference, SyntaxSet};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
Action, AttrsList, BorrowedWithFontSystem, Buffer, Color, Cursor, Edit, Editor, FontSystem,
|
Action, AttrsList, BorrowedWithFontSystem, Buffer, Change, Color, Cursor, Edit, Editor,
|
||||||
Shaping, Style, Weight, Wrap,
|
FontSystem, Shaping, Style, Weight, Wrap,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub use syntect::highlighting::Theme as SyntaxTheme;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct SyntaxSystem {
|
pub struct SyntaxSystem {
|
||||||
pub syntax_set: SyntaxSet,
|
pub syntax_set: SyntaxSet,
|
||||||
|
|
@ -35,7 +37,7 @@ pub struct SyntaxEditor<'a> {
|
||||||
editor: Editor,
|
editor: Editor,
|
||||||
syntax_system: &'a SyntaxSystem,
|
syntax_system: &'a SyntaxSystem,
|
||||||
syntax: &'a SyntaxReference,
|
syntax: &'a SyntaxReference,
|
||||||
theme: &'a Theme,
|
theme: &'a SyntaxTheme,
|
||||||
highlighter: Highlighter<'a>,
|
highlighter: Highlighter<'a>,
|
||||||
syntax_cache: Vec<(ParseState, HighlightState)>,
|
syntax_cache: Vec<(ParseState, HighlightState)>,
|
||||||
}
|
}
|
||||||
|
|
@ -65,9 +67,11 @@ impl<'a> SyntaxEditor<'a> {
|
||||||
/// Modifies the theme of the [`SyntaxEditor`], returning false if the theme is missing
|
/// Modifies the theme of the [`SyntaxEditor`], returning false if the theme is missing
|
||||||
pub fn update_theme(&mut self, theme_name: &str) -> bool {
|
pub fn update_theme(&mut self, theme_name: &str) -> bool {
|
||||||
if let Some(theme) = self.syntax_system.theme_set.themes.get(theme_name) {
|
if let Some(theme) = self.syntax_system.theme_set.themes.get(theme_name) {
|
||||||
self.theme = theme;
|
if self.theme != theme {
|
||||||
self.highlighter = Highlighter::new(theme);
|
self.theme = theme;
|
||||||
self.syntax_cache.clear();
|
self.highlighter = Highlighter::new(theme);
|
||||||
|
self.syntax_cache.clear();
|
||||||
|
}
|
||||||
|
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -130,6 +134,11 @@ impl<'a> SyntaxEditor<'a> {
|
||||||
Color::rgb(0xFF, 0xFF, 0xFF)
|
Color::rgb(0xFF, 0xFF, 0xFF)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the current syntect theme
|
||||||
|
pub fn theme(&self) -> &SyntaxTheme {
|
||||||
|
self.theme
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Edit for SyntaxEditor<'a> {
|
impl<'a> Edit for SyntaxEditor<'a> {
|
||||||
|
|
@ -157,16 +166,50 @@ impl<'a> Edit for SyntaxEditor<'a> {
|
||||||
self.editor.set_select_opt(select_opt);
|
self.editor.set_select_opt(select_opt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn auto_indent(&self) -> bool {
|
||||||
|
self.editor.auto_indent()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_auto_indent(&mut self, auto_indent: bool) {
|
||||||
|
self.editor.set_auto_indent(auto_indent);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tab_width(&self) -> u16 {
|
||||||
|
self.editor.tab_width()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_tab_width(&mut self, tab_width: u16) {
|
||||||
|
self.editor.set_tab_width(tab_width);
|
||||||
|
}
|
||||||
|
|
||||||
fn shape_as_needed(&mut self, font_system: &mut FontSystem) {
|
fn shape_as_needed(&mut self, font_system: &mut FontSystem) {
|
||||||
#[cfg(feature = "std")]
|
#[cfg(feature = "std")]
|
||||||
let now = std::time::Instant::now();
|
let now = std::time::Instant::now();
|
||||||
|
|
||||||
|
let cursor = self.cursor();
|
||||||
let buffer = self.editor.buffer_mut();
|
let buffer = self.editor.buffer_mut();
|
||||||
|
let lines = buffer.visible_lines();
|
||||||
|
let scroll_end = buffer.scroll() + lines;
|
||||||
|
let mut total_layout = 0;
|
||||||
let mut highlighted = 0;
|
let mut highlighted = 0;
|
||||||
for line_i in 0..buffer.lines.len() {
|
for line_i in 0..buffer.lines.len() {
|
||||||
|
// Break out if we have reached the end of scroll and are past the cursor
|
||||||
|
if total_layout >= scroll_end && line_i > cursor.line {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
let line = &mut buffer.lines[line_i];
|
let line = &mut buffer.lines[line_i];
|
||||||
if !line.is_reset() && line_i < self.syntax_cache.len() {
|
if !line.is_reset() && line_i < self.syntax_cache.len() {
|
||||||
|
//TODO: duplicated code!
|
||||||
|
// Perform shaping and layout of this line in order to count if we have reached scroll
|
||||||
|
match buffer.line_layout(font_system, line_i) {
|
||||||
|
Some(layout_lines) => {
|
||||||
|
total_layout += layout_lines.len() as i32;
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
//TODO: should this be possible?
|
||||||
|
}
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
highlighted += 1;
|
highlighted += 1;
|
||||||
|
|
@ -221,8 +264,15 @@ impl<'a> Edit for SyntaxEditor<'a> {
|
||||||
line.set_attrs_list(attrs_list);
|
line.set_attrs_list(attrs_list);
|
||||||
line.set_wrap(Wrap::Word);
|
line.set_wrap(Wrap::Word);
|
||||||
|
|
||||||
//TODO: efficiently do syntax highlighting without having to shape whole buffer
|
// Perform shaping and layout of this line in order to count if we have reached scroll
|
||||||
buffer.line_shape(font_system, line_i);
|
match buffer.line_layout(font_system, line_i) {
|
||||||
|
Some(layout_lines) => {
|
||||||
|
total_layout += layout_lines.len() as i32;
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
//TODO: should this be possible?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let cache_item = (parse_state.clone(), highlight_state.clone());
|
let cache_item = (parse_state.clone(), highlight_state.clone());
|
||||||
if line_i < self.syntax_cache.len() {
|
if line_i < self.syntax_cache.len() {
|
||||||
|
|
@ -262,6 +312,18 @@ impl<'a> Edit for SyntaxEditor<'a> {
|
||||||
self.editor.insert_string(data, attrs_list);
|
self.editor.insert_string(data, attrs_list);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn apply_change(&mut self, change: &Change) -> bool {
|
||||||
|
self.editor.apply_change(change)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start_change(&mut self) {
|
||||||
|
self.editor.start_change();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn finish_change(&mut self) -> Option<Change> {
|
||||||
|
self.editor.finish_change()
|
||||||
|
}
|
||||||
|
|
||||||
fn action(&mut self, font_system: &mut FontSystem, action: Action) {
|
fn action(&mut self, font_system: &mut FontSystem, action: Action) {
|
||||||
self.editor.action(font_system, action);
|
self.editor.action(font_system, action);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
833
src/edit/vi.rs
833
src/edit/vi.rs
|
|
@ -1,35 +1,182 @@
|
||||||
use alloc::string::String;
|
use alloc::string::String;
|
||||||
use core::cmp;
|
use core::cmp;
|
||||||
|
use modit::{Event, Key, Motion, Parser, TextObject, WordIter};
|
||||||
use unicode_segmentation::UnicodeSegmentation;
|
use unicode_segmentation::UnicodeSegmentation;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
Action, AttrsList, BorrowedWithFontSystem, Buffer, Color, Cursor, Edit, FontSystem,
|
Action, AttrsList, BorrowedWithFontSystem, Buffer, Change, Color, Cursor, Edit, FontSystem,
|
||||||
SyntaxEditor,
|
SyntaxEditor, SyntaxTheme,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
pub use modit::{ViMode, ViParser};
|
||||||
enum Mode {
|
|
||||||
Normal,
|
fn undo_2_action<E: Edit>(editor: &mut E, action: cosmic_undo_2::Action<&Change>) {
|
||||||
Insert,
|
match action {
|
||||||
Command,
|
cosmic_undo_2::Action::Do(change) => {
|
||||||
Search,
|
editor.apply_change(change);
|
||||||
SearchBackwards,
|
}
|
||||||
|
cosmic_undo_2::Action::Undo(change) => {
|
||||||
|
//TODO: make this more efficient
|
||||||
|
let mut reversed = change.clone();
|
||||||
|
reversed.reverse();
|
||||||
|
editor.apply_change(&reversed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn finish_change<E: Edit>(
|
||||||
|
editor: &mut E,
|
||||||
|
commands: &mut cosmic_undo_2::Commands<Change>,
|
||||||
|
changed: &mut bool,
|
||||||
|
) {
|
||||||
|
//TODO: join changes together
|
||||||
|
match editor.finish_change() {
|
||||||
|
Some(change) => {
|
||||||
|
if !change.items.is_empty() {
|
||||||
|
commands.push(change);
|
||||||
|
*changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn search<E: Edit>(editor: &mut E, value: &str, forwards: bool) -> bool {
|
||||||
|
let mut cursor = editor.cursor();
|
||||||
|
let start_line = cursor.line;
|
||||||
|
if forwards {
|
||||||
|
while cursor.line < editor.buffer().lines.len() {
|
||||||
|
if let Some(index) = editor.buffer().lines[cursor.line]
|
||||||
|
.text()
|
||||||
|
.match_indices(value)
|
||||||
|
.filter_map(|(i, _)| {
|
||||||
|
if cursor.line != start_line || i > cursor.index {
|
||||||
|
Some(i)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.next()
|
||||||
|
{
|
||||||
|
cursor.index = index;
|
||||||
|
editor.set_cursor(cursor);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor.line += 1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cursor.line += 1;
|
||||||
|
while cursor.line > 0 {
|
||||||
|
cursor.line -= 1;
|
||||||
|
|
||||||
|
if let Some(index) = editor.buffer().lines[cursor.line]
|
||||||
|
.text()
|
||||||
|
.rmatch_indices(value)
|
||||||
|
.filter_map(|(i, _)| {
|
||||||
|
if cursor.line != start_line || i < cursor.index {
|
||||||
|
Some(i)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.next()
|
||||||
|
{
|
||||||
|
cursor.index = index;
|
||||||
|
editor.set_cursor(cursor);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn select_in<E: Edit>(editor: &mut E, start_c: char, end_c: char, include: bool) {
|
||||||
|
// Find the largest encompasing object, or if there is none, find the next one.
|
||||||
|
let cursor = editor.cursor();
|
||||||
|
let buffer = editor.buffer();
|
||||||
|
|
||||||
|
// Search forwards for isolated end character, counting start and end characters found
|
||||||
|
let mut end = cursor;
|
||||||
|
let mut starts = 0;
|
||||||
|
let mut ends = 0;
|
||||||
|
'find_end: loop {
|
||||||
|
let line = &buffer.lines[end.line];
|
||||||
|
let text = line.text();
|
||||||
|
for (i, c) in text[end.index..].char_indices() {
|
||||||
|
if c == end_c {
|
||||||
|
ends += 1;
|
||||||
|
} else if c == start_c {
|
||||||
|
starts += 1;
|
||||||
|
}
|
||||||
|
if ends > starts {
|
||||||
|
end.index += if include { i + c.len_utf8() } else { i };
|
||||||
|
break 'find_end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if end.line + 1 < buffer.lines.len() {
|
||||||
|
end.line += 1;
|
||||||
|
end.index = 0;
|
||||||
|
} else {
|
||||||
|
break 'find_end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search backwards to resolve starts and ends
|
||||||
|
let mut start = cursor;
|
||||||
|
'find_start: loop {
|
||||||
|
let line = &buffer.lines[start.line];
|
||||||
|
let text = line.text();
|
||||||
|
for (i, c) in text[..start.index].char_indices().rev() {
|
||||||
|
if c == start_c {
|
||||||
|
starts += 1;
|
||||||
|
} else if c == end_c {
|
||||||
|
ends += 1;
|
||||||
|
}
|
||||||
|
if starts >= ends {
|
||||||
|
start.index = if include { i } else { i + c.len_utf8() };
|
||||||
|
break 'find_start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if start.line > 0 {
|
||||||
|
start.line -= 1;
|
||||||
|
start.index = buffer.lines[start.line].text().len();
|
||||||
|
} else {
|
||||||
|
break 'find_start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
editor.set_select_opt(Some(start));
|
||||||
|
editor.set_cursor(end);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct ViEditor<'a> {
|
pub struct ViEditor<'a> {
|
||||||
editor: SyntaxEditor<'a>,
|
editor: SyntaxEditor<'a>,
|
||||||
mode: Mode,
|
parser: ViParser,
|
||||||
|
passthrough: bool,
|
||||||
|
search_opt: Option<(String, bool)>,
|
||||||
|
commands: cosmic_undo_2::Commands<Change>,
|
||||||
|
changed: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> ViEditor<'a> {
|
impl<'a> ViEditor<'a> {
|
||||||
pub fn new(editor: SyntaxEditor<'a>) -> Self {
|
pub fn new(editor: SyntaxEditor<'a>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
editor,
|
editor,
|
||||||
mode: Mode::Normal,
|
parser: ViParser::new(),
|
||||||
|
passthrough: false,
|
||||||
|
search_opt: None,
|
||||||
|
commands: cosmic_undo_2::Commands::new(),
|
||||||
|
changed: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Modifies the theme of the [`SyntaxEditor`], returning false if the theme is missing
|
||||||
|
pub fn update_theme(&mut self, theme_name: &str) -> bool {
|
||||||
|
self.editor.update_theme(theme_name)
|
||||||
|
}
|
||||||
|
|
||||||
/// Load text from a file, and also set syntax to the best option
|
/// Load text from a file, and also set syntax to the best option
|
||||||
#[cfg(feature = "std")]
|
#[cfg(feature = "std")]
|
||||||
pub fn load_text<P: AsRef<std::path::Path>>(
|
pub fn load_text<P: AsRef<std::path::Path>>(
|
||||||
|
|
@ -50,6 +197,54 @@ impl<'a> ViEditor<'a> {
|
||||||
pub fn foreground_color(&self) -> Color {
|
pub fn foreground_color(&self) -> Color {
|
||||||
self.editor.foreground_color()
|
self.editor.foreground_color()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the current syntect theme
|
||||||
|
pub fn theme(&self) -> &SyntaxTheme {
|
||||||
|
self.editor.theme()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get changed flag
|
||||||
|
pub fn changed(&self) -> bool {
|
||||||
|
self.changed
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set changed flag
|
||||||
|
pub fn set_changed(&mut self, changed: bool) {
|
||||||
|
self.changed = changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set passthrough mode (true will turn off vi features)
|
||||||
|
pub fn set_passthrough(&mut self, passthrough: bool) {
|
||||||
|
if passthrough != self.passthrough {
|
||||||
|
self.passthrough = passthrough;
|
||||||
|
self.buffer_mut().set_redraw(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get current vi parser
|
||||||
|
pub fn parser(&self) -> &ViParser {
|
||||||
|
&self.parser
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Redo a change
|
||||||
|
pub fn redo(&mut self) {
|
||||||
|
log::info!("Redo");
|
||||||
|
for action in self.commands.redo() {
|
||||||
|
undo_2_action(&mut self.editor, action);
|
||||||
|
//TODO: clear changed flag when back to last saved state?
|
||||||
|
self.changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Undo a change
|
||||||
|
pub fn undo(&mut self) {
|
||||||
|
log::info!("Undo");
|
||||||
|
for action in self.commands.undo() {
|
||||||
|
undo_2_action(&mut self.editor, action);
|
||||||
|
//TODO: clear changed flag when back to last saved state?
|
||||||
|
self.changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Edit for ViEditor<'a> {
|
impl<'a> Edit for ViEditor<'a> {
|
||||||
|
|
@ -77,6 +272,22 @@ impl<'a> Edit for ViEditor<'a> {
|
||||||
self.editor.set_select_opt(select_opt);
|
self.editor.set_select_opt(select_opt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn auto_indent(&self) -> bool {
|
||||||
|
self.editor.auto_indent()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_auto_indent(&mut self, auto_indent: bool) {
|
||||||
|
self.editor.set_auto_indent(auto_indent);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tab_width(&self) -> u16 {
|
||||||
|
self.editor.tab_width()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_tab_width(&mut self, tab_width: u16) {
|
||||||
|
self.editor.set_tab_width(tab_width);
|
||||||
|
}
|
||||||
|
|
||||||
fn shape_as_needed(&mut self, font_system: &mut FontSystem) {
|
fn shape_as_needed(&mut self, font_system: &mut FontSystem) {
|
||||||
self.editor.shape_as_needed(font_system);
|
self.editor.shape_as_needed(font_system);
|
||||||
}
|
}
|
||||||
|
|
@ -93,143 +304,450 @@ impl<'a> Edit for ViEditor<'a> {
|
||||||
self.editor.insert_string(data, attrs_list);
|
self.editor.insert_string(data, attrs_list);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn apply_change(&mut self, change: &Change) -> bool {
|
||||||
|
self.editor.apply_change(change)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start_change(&mut self) {
|
||||||
|
self.editor.start_change();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn finish_change(&mut self) -> Option<Change> {
|
||||||
|
self.editor.finish_change()
|
||||||
|
}
|
||||||
|
|
||||||
fn action(&mut self, font_system: &mut FontSystem, action: Action) {
|
fn action(&mut self, font_system: &mut FontSystem, action: Action) {
|
||||||
let old_mode = self.mode;
|
log::info!("Action {:?}", action);
|
||||||
|
|
||||||
match self.mode {
|
let editor = &mut self.editor;
|
||||||
Mode::Normal => match action {
|
|
||||||
Action::Insert(c) => match c {
|
// Ensure a change is always started
|
||||||
// Enter insert mode after cursor
|
editor.start_change();
|
||||||
'a' => {
|
|
||||||
self.editor.action(font_system, Action::Right);
|
if self.passthrough {
|
||||||
self.mode = Mode::Insert;
|
editor.action(font_system, action);
|
||||||
}
|
// Always finish change when passing through (TODO: group changes)
|
||||||
// Enter insert mode at end of line
|
finish_change(editor, &mut self.commands, &mut self.changed);
|
||||||
'A' => {
|
return;
|
||||||
self.editor.action(font_system, Action::End);
|
}
|
||||||
self.mode = Mode::Insert;
|
|
||||||
}
|
let key = match action {
|
||||||
// Change mode
|
//TODO: this leaves lots of room for issues in translation, should we directly accept Key?
|
||||||
'c' => {
|
Action::Backspace => Key::Backspace,
|
||||||
if self.editor.select_opt().is_some() {
|
Action::Delete => Key::Delete,
|
||||||
self.editor.action(font_system, Action::Delete);
|
Action::Down => Key::Down,
|
||||||
self.mode = Mode::Insert;
|
Action::End => Key::End,
|
||||||
} else {
|
Action::Enter => Key::Enter,
|
||||||
//TODO: change to next cursor movement
|
Action::Escape => Key::Escape,
|
||||||
}
|
Action::Home => Key::Home,
|
||||||
}
|
Action::Indent => Key::Tab,
|
||||||
// Delete mode
|
Action::Insert(c) => Key::Char(c),
|
||||||
'd' => {
|
Action::Left => Key::Left,
|
||||||
if self.editor.select_opt().is_some() {
|
Action::PageDown => Key::PageDown,
|
||||||
self.editor.action(font_system, Action::Delete);
|
Action::PageUp => Key::PageUp,
|
||||||
} else {
|
Action::Right => Key::Right,
|
||||||
//TODO: delete to next cursor movement
|
Action::Unindent => Key::Backtab,
|
||||||
}
|
Action::Up => Key::Up,
|
||||||
}
|
|
||||||
// Enter insert mode at cursor
|
|
||||||
'i' => {
|
|
||||||
self.mode = Mode::Insert;
|
|
||||||
}
|
|
||||||
// Enter insert mode at start of line
|
|
||||||
'I' => {
|
|
||||||
//TODO: soft home, skip whitespace
|
|
||||||
self.editor.action(font_system, Action::Home);
|
|
||||||
self.mode = Mode::Insert;
|
|
||||||
}
|
|
||||||
// Create line after and enter insert mode
|
|
||||||
'o' => {
|
|
||||||
self.editor.action(font_system, Action::End);
|
|
||||||
self.editor.action(font_system, Action::Enter);
|
|
||||||
self.mode = Mode::Insert;
|
|
||||||
}
|
|
||||||
// Create line before and enter insert mode
|
|
||||||
'O' => {
|
|
||||||
self.editor.action(font_system, Action::Home);
|
|
||||||
self.editor.action(font_system, Action::Enter);
|
|
||||||
self.editor.shape_as_needed(font_system); // TODO: do not require this?
|
|
||||||
self.editor.action(font_system, Action::Up);
|
|
||||||
self.mode = Mode::Insert;
|
|
||||||
}
|
|
||||||
// Left
|
|
||||||
'h' => self.editor.action(font_system, Action::Left),
|
|
||||||
// Top of screen
|
|
||||||
//TODO: 'H' => self.editor.action(Action::ScreenHigh),
|
|
||||||
// Down
|
|
||||||
'j' => self.editor.action(font_system, Action::Down),
|
|
||||||
// Up
|
|
||||||
'k' => self.editor.action(font_system, Action::Up),
|
|
||||||
// Right
|
|
||||||
'l' => self.editor.action(font_system, Action::Right),
|
|
||||||
// Bottom of screen
|
|
||||||
//TODO: 'L' => self.editor.action(Action::ScreenLow),
|
|
||||||
// Middle of screen
|
|
||||||
//TODO: 'M' => self.editor.action(Action::ScreenMiddle),
|
|
||||||
// Enter visual mode
|
|
||||||
'v' => {
|
|
||||||
if self.editor.select_opt().is_some() {
|
|
||||||
self.editor.set_select_opt(None);
|
|
||||||
} else {
|
|
||||||
self.editor.set_select_opt(Some(self.editor.cursor()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Enter line visual mode
|
|
||||||
'V' => {
|
|
||||||
if self.editor.select_opt().is_some() {
|
|
||||||
self.editor.set_select_opt(None);
|
|
||||||
} else {
|
|
||||||
self.editor.action(font_system, Action::Home);
|
|
||||||
self.editor.set_select_opt(Some(self.editor.cursor()));
|
|
||||||
//TODO: set cursor_x_opt to max
|
|
||||||
self.editor.action(font_system, Action::End);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Remove character at cursor
|
|
||||||
'x' => self.editor.action(font_system, Action::Delete),
|
|
||||||
// Remove character before cursor
|
|
||||||
'X' => self.editor.action(font_system, Action::Backspace),
|
|
||||||
// Go to start of line
|
|
||||||
'0' => self.editor.action(font_system, Action::Home),
|
|
||||||
// Go to end of line
|
|
||||||
'$' => self.editor.action(font_system, Action::End),
|
|
||||||
// Go to start of line after whitespace
|
|
||||||
//TODO: implement this
|
|
||||||
'^' => self.editor.action(font_system, Action::Home),
|
|
||||||
// Enter command mode
|
|
||||||
':' => {
|
|
||||||
self.mode = Mode::Command;
|
|
||||||
}
|
|
||||||
// Enter search mode
|
|
||||||
'/' => {
|
|
||||||
self.mode = Mode::Search;
|
|
||||||
}
|
|
||||||
// Enter search backwards mode
|
|
||||||
'?' => {
|
|
||||||
self.mode = Mode::SearchBackwards;
|
|
||||||
}
|
|
||||||
_ => (),
|
|
||||||
},
|
|
||||||
_ => self.editor.action(font_system, action),
|
|
||||||
},
|
|
||||||
Mode::Insert => match action {
|
|
||||||
Action::Escape => {
|
|
||||||
let cursor = self.cursor();
|
|
||||||
let layout_cursor = self.buffer().layout_cursor(&cursor);
|
|
||||||
if layout_cursor.glyph > 0 {
|
|
||||||
self.editor.action(font_system, Action::Left);
|
|
||||||
}
|
|
||||||
self.mode = Mode::Normal;
|
|
||||||
}
|
|
||||||
_ => self.editor.action(font_system, action),
|
|
||||||
},
|
|
||||||
_ => {
|
_ => {
|
||||||
//TODO: other modes
|
log::info!("pass through action {:?}", action);
|
||||||
self.mode = Mode::Normal;
|
editor.action(font_system, action);
|
||||||
|
// Always finish change when passing through (TODO: group changes)
|
||||||
|
finish_change(editor, &mut self.commands, &mut self.changed);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
if self.mode != old_mode {
|
self.parser.parse(key, false, |event| {
|
||||||
self.buffer_mut().set_redraw(true);
|
log::info!(" Event {:?}", event);
|
||||||
}
|
let action = match event {
|
||||||
|
Event::AutoIndent => {
|
||||||
|
log::info!("TODO");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Event::Backspace => Action::Backspace,
|
||||||
|
Event::ChangeStart => {
|
||||||
|
editor.start_change();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Event::ChangeFinish => {
|
||||||
|
finish_change(editor, &mut self.commands, &mut self.changed);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Event::Copy => {
|
||||||
|
log::info!("TODO");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Event::Delete => Action::Delete,
|
||||||
|
Event::Escape => Action::Escape,
|
||||||
|
Event::Insert(c) => Action::Insert(c),
|
||||||
|
Event::NewLine => Action::Enter,
|
||||||
|
Event::Paste => {
|
||||||
|
log::info!("TODO");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Event::Redraw => {
|
||||||
|
editor.buffer_mut().set_redraw(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Event::SelectClear => {
|
||||||
|
editor.set_select_opt(None);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Event::SelectStart => {
|
||||||
|
let cursor = editor.cursor();
|
||||||
|
editor.set_select_opt(Some(cursor));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Event::SelectTextObject(text_object, include) => {
|
||||||
|
match text_object {
|
||||||
|
TextObject::AngleBrackets => select_in(editor, '<', '>', include),
|
||||||
|
TextObject::CurlyBrackets => select_in(editor, '{', '}', include),
|
||||||
|
TextObject::DoubleQuotes => select_in(editor, '"', '"', include),
|
||||||
|
TextObject::Parentheses => select_in(editor, '(', ')', include),
|
||||||
|
TextObject::Search { forwards } => {
|
||||||
|
match &self.search_opt {
|
||||||
|
Some((value, _)) => {
|
||||||
|
if search(editor, value, forwards) {
|
||||||
|
let mut cursor = editor.cursor();
|
||||||
|
editor.set_select_opt(Some(cursor));
|
||||||
|
//TODO: traverse lines if necessary
|
||||||
|
cursor.index += value.len();
|
||||||
|
editor.set_cursor(cursor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TextObject::SingleQuotes => select_in(editor, '\'', '\'', include),
|
||||||
|
TextObject::SquareBrackets => select_in(editor, '[', ']', include),
|
||||||
|
TextObject::Ticks => select_in(editor, '`', '`', include),
|
||||||
|
TextObject::Word(word) => {
|
||||||
|
let mut cursor = editor.cursor();
|
||||||
|
let mut select_opt = editor.select_opt();
|
||||||
|
let buffer = editor.buffer();
|
||||||
|
let text = buffer.lines[cursor.line].text();
|
||||||
|
match WordIter::new(text, word)
|
||||||
|
.find(|&(i, w)| i <= cursor.index && i + w.len() > cursor.index)
|
||||||
|
{
|
||||||
|
Some((i, w)) => {
|
||||||
|
cursor.index = i;
|
||||||
|
select_opt = Some(cursor);
|
||||||
|
cursor.index += w.len();
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
//TODO
|
||||||
|
}
|
||||||
|
}
|
||||||
|
editor.set_select_opt(select_opt);
|
||||||
|
editor.set_cursor(cursor);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
log::info!("TODO: {:?}", text_object);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Event::SetSearch(value, forwards) => {
|
||||||
|
self.search_opt = Some((value, forwards));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Event::ShiftLeft => Action::Unindent,
|
||||||
|
Event::ShiftRight => Action::Indent,
|
||||||
|
Event::SwapCase => {
|
||||||
|
log::info!("TODO");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Event::Undo => {
|
||||||
|
for action in self.commands.undo() {
|
||||||
|
undo_2_action(editor, action);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Event::Motion(motion) => {
|
||||||
|
match motion {
|
||||||
|
Motion::Around => {
|
||||||
|
//TODO: what to do for this psuedo-motion?
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Motion::Down => Action::Down,
|
||||||
|
Motion::End => Action::End,
|
||||||
|
Motion::GotoLine(line) => Action::GotoLine(line.saturating_sub(1)),
|
||||||
|
Motion::GotoEof => {
|
||||||
|
Action::GotoLine(editor.buffer().lines.len().saturating_sub(1))
|
||||||
|
}
|
||||||
|
Motion::Home => Action::Home,
|
||||||
|
Motion::Inside => {
|
||||||
|
//TODO: what to do for this psuedo-motion?
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Motion::Left => Action::Left,
|
||||||
|
Motion::LeftInLine => {
|
||||||
|
let cursor = editor.cursor();
|
||||||
|
if cursor.index > 0 {
|
||||||
|
Action::Left
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Motion::Line => {
|
||||||
|
//TODO: what to do for this psuedo-motion?
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Motion::NextChar(find_c) => {
|
||||||
|
let mut cursor = editor.cursor();
|
||||||
|
let buffer = editor.buffer();
|
||||||
|
let text = buffer.lines[cursor.line].text();
|
||||||
|
if cursor.index < text.len() {
|
||||||
|
match text[cursor.index..]
|
||||||
|
.char_indices()
|
||||||
|
.filter(|&(i, c)| i > 0 && c == find_c)
|
||||||
|
.next()
|
||||||
|
{
|
||||||
|
Some((i, _)) => {
|
||||||
|
cursor.index += i;
|
||||||
|
editor.set_cursor(cursor);
|
||||||
|
}
|
||||||
|
None => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Motion::NextCharTill(find_c) => {
|
||||||
|
let mut cursor = editor.cursor();
|
||||||
|
let buffer = editor.buffer();
|
||||||
|
let text = buffer.lines[cursor.line].text();
|
||||||
|
if cursor.index < text.len() {
|
||||||
|
let mut last_i = 0;
|
||||||
|
for (i, c) in text[cursor.index..].char_indices() {
|
||||||
|
if last_i > 0 && c == find_c {
|
||||||
|
cursor.index += last_i;
|
||||||
|
editor.set_cursor(cursor);
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
last_i = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Motion::NextSearch => match &self.search_opt {
|
||||||
|
Some((value, forwards)) => {
|
||||||
|
search(editor, value, *forwards);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
None => return,
|
||||||
|
},
|
||||||
|
Motion::NextWordEnd(word) => {
|
||||||
|
let mut cursor = editor.cursor();
|
||||||
|
let buffer = editor.buffer();
|
||||||
|
loop {
|
||||||
|
let text = buffer.lines[cursor.line].text();
|
||||||
|
if cursor.index < text.len() {
|
||||||
|
cursor.index = WordIter::new(text, word)
|
||||||
|
.map(|(i, w)| {
|
||||||
|
i + w.char_indices().last().map(|(i, _)| i).unwrap_or(0)
|
||||||
|
})
|
||||||
|
.find(|&i| i > cursor.index)
|
||||||
|
.unwrap_or(text.len());
|
||||||
|
if cursor.index == text.len() {
|
||||||
|
// Try again, searching next line
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else if cursor.line + 1 < buffer.lines.len() {
|
||||||
|
// Go to next line and rerun loop
|
||||||
|
cursor.line += 1;
|
||||||
|
cursor.index = 0;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
editor.set_cursor(cursor);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Motion::NextWordStart(word) => {
|
||||||
|
let mut cursor = editor.cursor();
|
||||||
|
let buffer = editor.buffer();
|
||||||
|
loop {
|
||||||
|
let text = buffer.lines[cursor.line].text();
|
||||||
|
if cursor.index < text.len() {
|
||||||
|
cursor.index = WordIter::new(text, word)
|
||||||
|
.map(|(i, _)| i)
|
||||||
|
.find(|&i| i > cursor.index)
|
||||||
|
.unwrap_or(text.len());
|
||||||
|
if cursor.index == text.len() {
|
||||||
|
// Try again, searching next line
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else if cursor.line + 1 < buffer.lines.len() {
|
||||||
|
// Go to next line and rerun loop
|
||||||
|
cursor.line += 1;
|
||||||
|
cursor.index = 0;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
editor.set_cursor(cursor);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Motion::PageDown => Action::PageDown,
|
||||||
|
Motion::PageUp => Action::PageUp,
|
||||||
|
Motion::PreviousChar(find_c) => {
|
||||||
|
let mut cursor = editor.cursor();
|
||||||
|
let buffer = editor.buffer();
|
||||||
|
let text = buffer.lines[cursor.line].text();
|
||||||
|
if cursor.index > 0 {
|
||||||
|
match text[..cursor.index]
|
||||||
|
.char_indices()
|
||||||
|
.filter(|&(_, c)| c == find_c)
|
||||||
|
.last()
|
||||||
|
{
|
||||||
|
Some((i, _)) => {
|
||||||
|
cursor.index = i;
|
||||||
|
editor.set_cursor(cursor);
|
||||||
|
}
|
||||||
|
None => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Motion::PreviousCharTill(find_c) => {
|
||||||
|
let mut cursor = editor.cursor();
|
||||||
|
let buffer = editor.buffer();
|
||||||
|
let text = buffer.lines[cursor.line].text();
|
||||||
|
if cursor.index > 0 {
|
||||||
|
match text[..cursor.index]
|
||||||
|
.char_indices()
|
||||||
|
.filter_map(|(i, c)| {
|
||||||
|
if c == find_c {
|
||||||
|
let end = i + c.len_utf8();
|
||||||
|
if end < cursor.index {
|
||||||
|
return Some(end);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
})
|
||||||
|
.last()
|
||||||
|
{
|
||||||
|
Some(i) => {
|
||||||
|
cursor.index = i;
|
||||||
|
editor.set_cursor(cursor);
|
||||||
|
}
|
||||||
|
None => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Motion::PreviousSearch => match &self.search_opt {
|
||||||
|
Some((value, forwards)) => {
|
||||||
|
search(editor, value, !*forwards);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
None => return,
|
||||||
|
},
|
||||||
|
Motion::PreviousWordEnd(word) => {
|
||||||
|
let mut cursor = editor.cursor();
|
||||||
|
let buffer = editor.buffer();
|
||||||
|
loop {
|
||||||
|
let text = buffer.lines[cursor.line].text();
|
||||||
|
if cursor.index > 0 {
|
||||||
|
cursor.index = WordIter::new(text, word)
|
||||||
|
.map(|(i, w)| {
|
||||||
|
i + w.char_indices().last().map(|(i, _)| i).unwrap_or(0)
|
||||||
|
})
|
||||||
|
.filter(|&i| i < cursor.index)
|
||||||
|
.last()
|
||||||
|
.unwrap_or(0);
|
||||||
|
if cursor.index == 0 {
|
||||||
|
// Try again, searching previous line
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else if cursor.line > 0 {
|
||||||
|
// Go to previous line and rerun loop
|
||||||
|
cursor.line -= 1;
|
||||||
|
cursor.index = buffer.lines[cursor.line].text().len();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
editor.set_cursor(cursor);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Motion::PreviousWordStart(word) => {
|
||||||
|
let mut cursor = editor.cursor();
|
||||||
|
let buffer = editor.buffer();
|
||||||
|
loop {
|
||||||
|
let text = buffer.lines[cursor.line].text();
|
||||||
|
if cursor.index > 0 {
|
||||||
|
cursor.index = WordIter::new(text, word)
|
||||||
|
.map(|(i, _)| i)
|
||||||
|
.filter(|&i| i < cursor.index)
|
||||||
|
.last()
|
||||||
|
.unwrap_or(0);
|
||||||
|
if cursor.index == 0 {
|
||||||
|
// Try again, searching previous line
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else if cursor.line > 0 {
|
||||||
|
// Go to previous line and rerun loop
|
||||||
|
cursor.line -= 1;
|
||||||
|
cursor.index = buffer.lines[cursor.line].text().len();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
editor.set_cursor(cursor);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Motion::Right => Action::Right,
|
||||||
|
Motion::RightInLine => {
|
||||||
|
let cursor = editor.cursor();
|
||||||
|
let buffer = editor.buffer();
|
||||||
|
if cursor.index < buffer.lines[cursor.line].text().len() {
|
||||||
|
Action::Right
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Motion::ScreenHigh => {
|
||||||
|
//TODO: is this efficient?
|
||||||
|
if let Some(first) = editor.buffer().layout_runs().next() {
|
||||||
|
Action::GotoLine(first.line_i)
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Motion::ScreenLow => {
|
||||||
|
//TODO: is this efficient?
|
||||||
|
if let Some(last) = editor.buffer().layout_runs().last() {
|
||||||
|
Action::GotoLine(last.line_i)
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Motion::ScreenMiddle => {
|
||||||
|
//TODO: is this efficient?
|
||||||
|
let mut layout_runs = editor.buffer().layout_runs();
|
||||||
|
if let Some(first) = layout_runs.next() {
|
||||||
|
if let Some(last) = layout_runs.last() {
|
||||||
|
Action::GotoLine((last.line_i + first.line_i) / 2)
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Motion::Selection => {
|
||||||
|
//TODO: what to do for this psuedo-motion?
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Motion::SoftHome => Action::SoftHome,
|
||||||
|
Motion::Up => Action::Up,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
editor.action(font_system, action);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "swash")]
|
#[cfg(feature = "swash")]
|
||||||
|
|
@ -242,12 +760,16 @@ impl<'a> Edit for ViEditor<'a> {
|
||||||
) where
|
) where
|
||||||
F: FnMut(i32, i32, u32, u32, Color),
|
F: FnMut(i32, i32, u32, u32, Color),
|
||||||
{
|
{
|
||||||
|
let size = self.buffer().size();
|
||||||
|
f(0, 0, size.0 as u32, size.1 as u32, self.background_color());
|
||||||
|
|
||||||
let font_size = self.buffer().metrics().font_size;
|
let font_size = self.buffer().metrics().font_size;
|
||||||
let line_height = self.buffer().metrics().line_height;
|
let line_height = self.buffer().metrics().line_height;
|
||||||
|
|
||||||
for run in self.buffer().layout_runs() {
|
for run in self.buffer().layout_runs() {
|
||||||
let line_i = run.line_i;
|
let line_i = run.line_i;
|
||||||
let line_y = run.line_y;
|
let line_y = run.line_y;
|
||||||
|
let line_top = run.line_top;
|
||||||
|
|
||||||
let cursor_glyph_opt = |cursor: &Cursor| -> Option<(usize, f32, f32)> {
|
let cursor_glyph_opt = |cursor: &Cursor| -> Option<(usize, f32, f32)> {
|
||||||
//TODO: better calculation of width
|
//TODO: better calculation of width
|
||||||
|
|
@ -326,7 +848,7 @@ impl<'a> Edit for ViEditor<'a> {
|
||||||
} else if let Some((min, max)) = range_opt.take() {
|
} else if let Some((min, max)) = range_opt.take() {
|
||||||
f(
|
f(
|
||||||
min,
|
min,
|
||||||
(line_y - font_size) as i32,
|
line_top as i32,
|
||||||
cmp::max(0, max - min) as u32,
|
cmp::max(0, max - min) as u32,
|
||||||
line_height as u32,
|
line_height as u32,
|
||||||
Color::rgba(color.r(), color.g(), color.b(), 0x33),
|
Color::rgba(color.r(), color.g(), color.b(), 0x33),
|
||||||
|
|
@ -352,7 +874,7 @@ impl<'a> Edit for ViEditor<'a> {
|
||||||
}
|
}
|
||||||
f(
|
f(
|
||||||
min,
|
min,
|
||||||
(line_y - font_size) as i32,
|
line_top as i32,
|
||||||
cmp::max(0, max - min) as u32,
|
cmp::max(0, max - min) as u32,
|
||||||
line_height as u32,
|
line_height as u32,
|
||||||
Color::rgba(color.r(), color.g(), color.b(), 0x33),
|
Color::rgba(color.r(), color.g(), color.b(), 0x33),
|
||||||
|
|
@ -365,10 +887,13 @@ impl<'a> Edit for ViEditor<'a> {
|
||||||
if let Some((cursor_glyph, cursor_glyph_offset, cursor_glyph_width)) =
|
if let Some((cursor_glyph, cursor_glyph_offset, cursor_glyph_width)) =
|
||||||
cursor_glyph_opt(&self.cursor())
|
cursor_glyph_opt(&self.cursor())
|
||||||
{
|
{
|
||||||
let block_cursor = match self.mode {
|
let block_cursor = if self.passthrough {
|
||||||
Mode::Normal => true,
|
false
|
||||||
Mode::Insert => false,
|
} else {
|
||||||
_ => true, /*TODO: determine block cursor in other modes*/
|
match self.parser.mode {
|
||||||
|
ViMode::Insert | ViMode::Replace => false,
|
||||||
|
_ => true, /*TODO: determine block cursor in other modes*/
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let (start_x, end_x) = match run.glyphs.get(cursor_glyph) {
|
let (start_x, end_x) = match run.glyphs.get(cursor_glyph) {
|
||||||
|
|
@ -411,19 +936,13 @@ impl<'a> Edit for ViEditor<'a> {
|
||||||
let right_x = cmp::max(start_x, end_x);
|
let right_x = cmp::max(start_x, end_x);
|
||||||
f(
|
f(
|
||||||
left_x,
|
left_x,
|
||||||
(line_y - font_size) as i32,
|
line_top as i32,
|
||||||
(right_x - left_x) as u32,
|
(right_x - left_x) as u32,
|
||||||
line_height as u32,
|
line_height as u32,
|
||||||
Color::rgba(color.r(), color.g(), color.b(), 0x33),
|
Color::rgba(color.r(), color.g(), color.b(), 0x33),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
f(
|
f(start_x, line_top as i32, 1, line_height as u32, color);
|
||||||
start_x,
|
|
||||||
(line_y - font_size) as i32,
|
|
||||||
1,
|
|
||||||
line_height as u32,
|
|
||||||
color,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -61,10 +61,10 @@
|
||||||
#![allow(clippy::new_without_default)]
|
#![allow(clippy::new_without_default)]
|
||||||
// TODO: address occurrences and then deny
|
// TODO: address occurrences and then deny
|
||||||
//
|
//
|
||||||
|
// Overflows can produce unpredictable results and are only checked in debug builds
|
||||||
|
#![allow(clippy::arithmetic_side_effects)]
|
||||||
// Indexing a slice can cause panics and that is something we always want to avoid
|
// Indexing a slice can cause panics and that is something we always want to avoid
|
||||||
#![allow(clippy::indexing_slicing)]
|
#![allow(clippy::indexing_slicing)]
|
||||||
// Overflows can produce unpredictable results and are only checked in debug builds
|
|
||||||
#![allow(clippy::integer_arithmetic)]
|
|
||||||
// Soundness issues
|
// Soundness issues
|
||||||
//
|
//
|
||||||
// Dereferencing unaligned pointers may be undefined behavior
|
// Dereferencing unaligned pointers may be undefined behavior
|
||||||
|
|
|
||||||
|
|
@ -1252,12 +1252,10 @@ impl ShapeLine {
|
||||||
layout_lines.push(LayoutLine {
|
layout_lines.push(LayoutLine {
|
||||||
w: if align != Align::Justified {
|
w: if align != Align::Justified {
|
||||||
visual_line.w
|
visual_line.w
|
||||||
|
} else if self.rtl {
|
||||||
|
start_x - x
|
||||||
} else {
|
} else {
|
||||||
if self.rtl {
|
x
|
||||||
start_x - x
|
|
||||||
} else {
|
|
||||||
x
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
max_ascent: max_ascent * font_size,
|
max_ascent: max_ascent * font_size,
|
||||||
max_descent: max_descent * font_size,
|
max_descent: max_descent * font_size,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue