use floem_editor_core::{
buffer::rope_text::{RopeText, RopeTextVal},
command::MultiSelectionCommand,
cursor::{ColPosition, Cursor, CursorAffinity, CursorMode},
mode::{Mode, MotionMode, VisualMode},
movement::{LinePosition, Movement},
register::Register,
selection::{SelRegion, Selection},
soft_tab::{snap_to_soft_tab, SnapDirection},
};
use super::{
actions::CommonAction,
visual_line::{RVLine, VLineInfo},
Editor,
};
fn move_region(
view: &Editor,
region: &SelRegion,
affinity: &mut CursorAffinity,
count: usize,
modify: bool,
movement: &Movement,
mode: Mode,
) -> SelRegion {
let (count, region) = if count >= 1 && !modify && !region.is_caret() {
match movement {
Movement::Left | Movement::Up => {
let leftmost = region.min();
(count - 1, SelRegion::new(leftmost, leftmost, region.horiz))
}
Movement::Right | Movement::Down => {
let rightmost = region.max();
(
count - 1,
SelRegion::new(rightmost, rightmost, region.horiz),
)
}
_ => (count, *region),
}
} else {
(count, *region)
};
let (end, horiz) = move_offset(
view,
region.end,
region.horiz.as_ref(),
affinity,
count,
movement,
mode,
);
let start = match modify {
true => region.start,
false => end,
};
SelRegion::new(start, end, horiz)
}
pub fn move_selection(
view: &Editor,
selection: &Selection,
affinity: &mut CursorAffinity,
count: usize,
modify: bool,
movement: &Movement,
mode: Mode,
) -> Selection {
let mut new_selection = Selection::new();
for region in selection.regions() {
new_selection.add_region(move_region(
view, region, affinity, count, modify, movement, mode,
));
}
new_selection
}
pub fn move_offset(
view: &Editor,
offset: usize,
horiz: Option<&ColPosition>,
affinity: &mut CursorAffinity,
count: usize,
movement: &Movement,
mode: Mode,
) -> (usize, Option<ColPosition>) {
let (new_offset, horiz) = match movement {
Movement::Left => {
let new_offset = move_left(view, offset, affinity, mode, count);
(new_offset, None)
}
Movement::Right => {
let new_offset = move_right(view, offset, affinity, mode, count);
(new_offset, None)
}
Movement::Up => {
let (new_offset, horiz) = move_up(view, offset, affinity, horiz.cloned(), mode, count);
(new_offset, Some(horiz))
}
Movement::Down => {
let (new_offset, horiz) =
move_down(view, offset, affinity, horiz.cloned(), mode, count);
(new_offset, Some(horiz))
}
Movement::DocumentStart => {
*affinity = CursorAffinity::Backward;
(0, Some(ColPosition::Start))
}
Movement::DocumentEnd => {
let (new_offset, horiz) = document_end(view.rope_text(), affinity, mode);
(new_offset, Some(horiz))
}
Movement::FirstNonBlank => {
let (new_offset, horiz) = first_non_blank(view, affinity, offset);
(new_offset, Some(horiz))
}
Movement::StartOfLine => {
let (new_offset, horiz) = start_of_line(view, affinity, offset);
(new_offset, Some(horiz))
}
Movement::EndOfLine => {
let (new_offset, horiz) = end_of_line(view, affinity, offset, mode);
(new_offset, Some(horiz))
}
Movement::Line(position) => {
let (new_offset, horiz) = to_line(view, offset, horiz.cloned(), mode, position);
(new_offset, Some(horiz))
}
Movement::Offset(offset) => {
let new_offset = view.text().prev_grapheme_offset(*offset + 1).unwrap();
(new_offset, None)
}
Movement::WordEndForward => {
let new_offset =
view.rope_text()
.move_n_wordends_forward(offset, count, mode == Mode::Insert);
(new_offset, None)
}
Movement::WordForward => {
let new_offset = view.rope_text().move_n_words_forward(offset, count);
(new_offset, None)
}
Movement::WordBackward => {
let new_offset = view.rope_text().move_n_words_backward(offset, count, mode);
(new_offset, None)
}
Movement::NextUnmatched(char) => {
let new_offset = view.doc().find_unmatched(offset, false, *char);
(new_offset, None)
}
Movement::PreviousUnmatched(char) => {
let new_offset = view.doc().find_unmatched(offset, true, *char);
(new_offset, None)
}
Movement::MatchPairs => {
let new_offset = view.doc().find_matching_pair(offset);
(new_offset, None)
}
Movement::ParagraphForward => {
let new_offset = view.rope_text().move_n_paragraphs_forward(offset, count);
(new_offset, None)
}
Movement::ParagraphBackward => {
let new_offset = view.rope_text().move_n_paragraphs_backward(offset, count);
(new_offset, None)
}
};
let new_offset = correct_crlf(&view.rope_text(), new_offset);
(new_offset, horiz)
}
fn correct_crlf(text: &RopeTextVal, offset: usize) -> usize {
if offset == 0 || offset == text.len() {
return offset;
}
let mut cursor = lapce_xi_rope::Cursor::new(text.text(), offset);
if cursor.peek_next_codepoint() == Some('\n') && cursor.prev_codepoint() == Some('\r') {
return offset - 1;
}
offset
}
fn atomic_soft_tab_width_for_offset(ed: &Editor, offset: usize) -> Option<usize> {
let line = ed.line_of_offset(offset);
let style = ed.style();
if style.atomic_soft_tabs(ed.id(), line) {
Some(style.tab_width(ed.id(), line))
} else {
None
}
}
fn move_left(
ed: &Editor,
offset: usize,
affinity: &mut CursorAffinity,
mode: Mode,
count: usize,
) -> usize {
let rope_text = ed.rope_text();
let mut new_offset = rope_text.move_left(offset, mode, count);
if let Some(soft_tab_width) = atomic_soft_tab_width_for_offset(ed, offset) {
if soft_tab_width > 1 {
new_offset = snap_to_soft_tab(
rope_text.text(),
new_offset,
SnapDirection::Left,
soft_tab_width,
);
}
}
*affinity = CursorAffinity::Forward;
new_offset
}
fn move_right(
view: &Editor,
offset: usize,
affinity: &mut CursorAffinity,
mode: Mode,
count: usize,
) -> usize {
let rope_text = view.rope_text();
let mut new_offset = rope_text.move_right(offset, mode, count);
if let Some(soft_tab_width) = atomic_soft_tab_width_for_offset(view, offset) {
if soft_tab_width > 1 {
new_offset = snap_to_soft_tab(
rope_text.text(),
new_offset,
SnapDirection::Right,
soft_tab_width,
);
}
}
let (rvline, col) = view.rvline_col_of_offset(offset, *affinity);
let info = view.rvline_info(rvline);
*affinity = if col == info.last_col(&view.text_prov(), false) {
CursorAffinity::Forward
} else {
CursorAffinity::Backward
};
new_offset
}
fn find_prev_rvline(view: &Editor, start: RVLine, count: usize) -> Option<RVLine> {
if count == 0 {
return Some(start);
}
let mut info = None;
let mut found_count = 0;
for prev_info in view.iter_rvlines(true, start).skip(1) {
if prev_info.is_empty_phantom() {
continue;
}
found_count += 1;
if found_count == count {
info = Some(prev_info);
break;
}
}
info.map(|info| info.rvline)
}
fn move_up(
view: &Editor,
offset: usize,
affinity: &mut CursorAffinity,
horiz: Option<ColPosition>,
mode: Mode,
count: usize,
) -> (usize, ColPosition) {
let rvline = view.rvline_of_offset(offset, *affinity);
if rvline.line == 0 && rvline.line_index == 0 {
let horiz = horiz
.unwrap_or_else(|| ColPosition::Col(view.line_point_of_offset(offset, *affinity).x));
*affinity = CursorAffinity::Backward;
return (0, horiz);
}
let Some(rvline) = find_prev_rvline(view, rvline, count) else {
let horiz = horiz
.unwrap_or_else(|| ColPosition::Col(view.line_point_of_offset(offset, *affinity).x));
*affinity = CursorAffinity::Backward;
return (0, horiz);
};
let horiz =
horiz.unwrap_or_else(|| ColPosition::Col(view.line_point_of_offset(offset, *affinity).x));
let col = view.rvline_horiz_col(rvline, &horiz, mode != Mode::Normal);
*affinity = if col == 0 {
CursorAffinity::Forward
} else {
CursorAffinity::Backward
};
let new_offset = view.offset_of_line_col(rvline.line, col);
(new_offset, horiz)
}
fn move_down_last_rvline(
view: &Editor,
offset: usize,
affinity: &mut CursorAffinity,
horiz: Option<ColPosition>,
mode: Mode,
) -> (usize, ColPosition) {
let rope_text = view.rope_text();
let last_line = rope_text.last_line();
let new_offset = rope_text.line_end_offset(last_line, mode != Mode::Normal);
*affinity = CursorAffinity::Forward;
let horiz =
horiz.unwrap_or_else(|| ColPosition::Col(view.line_point_of_offset(offset, *affinity).x));
(new_offset, horiz)
}
fn find_next_rvline_info(
view: &Editor,
offset: usize,
start: RVLine,
count: usize,
) -> Option<VLineInfo<()>> {
let mut found_count = 0;
for next_info in view.iter_rvlines(false, start) {
if count == 0 {
return Some(next_info);
}
if next_info.is_empty_phantom() {
continue;
}
if next_info.interval.start <= offset {
continue;
}
found_count += 1;
if found_count == count {
return Some(next_info);
}
}
None
}
fn move_down(
view: &Editor,
offset: usize,
affinity: &mut CursorAffinity,
horiz: Option<ColPosition>,
mode: Mode,
count: usize,
) -> (usize, ColPosition) {
let rvline = view.rvline_of_offset(offset, *affinity);
let Some(info) = find_next_rvline_info(view, offset, rvline, count) else {
return move_down_last_rvline(view, offset, affinity, horiz, mode);
};
let horiz =
horiz.unwrap_or_else(|| ColPosition::Col(view.line_point_of_offset(offset, *affinity).x));
let col = view.rvline_horiz_col(info.rvline, &horiz, mode != Mode::Normal);
let new_offset = view.offset_of_line_col(info.rvline.line, col);
*affinity = if new_offset == info.interval.start {
CursorAffinity::Forward
} else {
CursorAffinity::Backward
};
(new_offset, horiz)
}
fn document_end(
rope_text: impl RopeText,
affinity: &mut CursorAffinity,
mode: Mode,
) -> (usize, ColPosition) {
let last_offset = rope_text.offset_line_end(rope_text.len(), mode != Mode::Normal);
*affinity = CursorAffinity::Forward;
(last_offset, ColPosition::End)
}
fn first_non_blank(
view: &Editor,
affinity: &mut CursorAffinity,
offset: usize,
) -> (usize, ColPosition) {
let info = view.rvline_info_of_offset(offset, *affinity);
let non_blank_offset = info.first_non_blank_character(&view.text_prov());
let start_line_offset = info.interval.start;
*affinity = CursorAffinity::Forward;
if offset > non_blank_offset {
(non_blank_offset, ColPosition::FirstNonBlank)
} else {
if start_line_offset == offset {
(non_blank_offset, ColPosition::FirstNonBlank)
} else {
(start_line_offset, ColPosition::Start)
}
}
}
fn start_of_line(
view: &Editor,
affinity: &mut CursorAffinity,
offset: usize,
) -> (usize, ColPosition) {
let rvline = view.rvline_of_offset(offset, *affinity);
let new_offset = view.offset_of_rvline(rvline);
*affinity = CursorAffinity::Forward;
(new_offset, ColPosition::Start)
}
fn end_of_line(
view: &Editor,
affinity: &mut CursorAffinity,
offset: usize,
mode: Mode,
) -> (usize, ColPosition) {
let info = view.rvline_info_of_offset(offset, *affinity);
let new_col = info.last_col(&view.text_prov(), mode != Mode::Normal);
*affinity = if new_col == 0 {
CursorAffinity::Forward
} else {
CursorAffinity::Backward
};
let new_offset = view.offset_of_line_col(info.rvline.line, new_col);
(new_offset, ColPosition::End)
}
fn to_line(
view: &Editor,
offset: usize,
horiz: Option<ColPosition>,
mode: Mode,
position: &LinePosition,
) -> (usize, ColPosition) {
let rope_text = view.rope_text();
let line = match position {
LinePosition::Line(line) => (line - 1).min(rope_text.last_line()),
LinePosition::First => 0,
LinePosition::Last => rope_text.last_line(),
};
let horiz = horiz.unwrap_or_else(|| {
ColPosition::Col(
view.line_point_of_offset(offset, CursorAffinity::Backward)
.x,
)
});
let col = view.line_horiz_col(line, &horiz, mode != Mode::Normal);
let new_offset = rope_text.offset_of_line_col(line, col);
(new_offset, horiz)
}
pub fn move_cursor(
ed: &Editor,
action: &dyn CommonAction,
cursor: &mut Cursor,
movement: &Movement,
count: usize,
modify: bool,
register: &mut Register,
) {
match cursor.mode {
CursorMode::Normal(offset) => {
let count = if let Some(motion_mode) = cursor.motion_mode.as_ref() {
count.max(motion_mode.count())
} else {
count
};
let (new_offset, horiz) = move_offset(
ed,
offset,
cursor.horiz.as_ref(),
&mut cursor.affinity,
count,
movement,
Mode::Normal,
);
if let Some(motion_mode) = cursor.motion_mode.clone() {
let (moved_new_offset, _) = move_offset(
ed,
new_offset,
None,
&mut cursor.affinity,
1,
&Movement::Right,
Mode::Insert,
);
let range = match movement {
Movement::EndOfLine | Movement::WordEndForward => offset..moved_new_offset,
Movement::MatchPairs => {
if new_offset > offset {
offset..moved_new_offset
} else {
moved_new_offset..new_offset
}
}
_ => offset..new_offset,
};
action.exec_motion_mode(
ed,
cursor,
motion_mode,
range,
movement.is_vertical(),
register,
);
cursor.motion_mode = None;
} else {
cursor.mode = CursorMode::Normal(new_offset);
cursor.horiz = horiz;
}
}
CursorMode::Visual { start, end, mode } => {
let (new_offset, horiz) = move_offset(
ed,
end,
cursor.horiz.as_ref(),
&mut cursor.affinity,
count,
movement,
Mode::Visual(VisualMode::Normal),
);
cursor.mode = CursorMode::Visual {
start,
end: new_offset,
mode,
};
cursor.horiz = horiz;
}
CursorMode::Insert(ref selection) => {
let selection = move_selection(
ed,
selection,
&mut cursor.affinity,
count,
modify,
movement,
Mode::Insert,
);
cursor.set_insert(selection);
}
}
}
pub fn do_multi_selection(view: &Editor, cursor: &mut Cursor, cmd: &MultiSelectionCommand) {
use MultiSelectionCommand::*;
let rope_text = view.rope_text();
match cmd {
SelectUndo => {
if let CursorMode::Insert(_) = cursor.mode.clone() {
if let Some(selection) = cursor.history_selections.last().cloned() {
cursor.mode = CursorMode::Insert(selection);
}
cursor.history_selections.pop();
}
}
InsertCursorAbove => {
if let CursorMode::Insert(mut selection) = cursor.mode.clone() {
let offset = selection.first().map(|s| s.end).unwrap_or(0);
let (new_offset, _) = move_offset(
view,
offset,
cursor.horiz.as_ref(),
&mut cursor.affinity,
1,
&Movement::Up,
Mode::Insert,
);
if new_offset != offset {
selection.add_region(SelRegion::new(new_offset, new_offset, None));
}
cursor.set_insert(selection);
}
}
InsertCursorBelow => {
if let CursorMode::Insert(mut selection) = cursor.mode.clone() {
let offset = selection.last().map(|s| s.end).unwrap_or(0);
let (new_offset, _) = move_offset(
view,
offset,
cursor.horiz.as_ref(),
&mut cursor.affinity,
1,
&Movement::Down,
Mode::Insert,
);
if new_offset != offset {
selection.add_region(SelRegion::new(new_offset, new_offset, None));
}
cursor.set_insert(selection);
}
}
InsertCursorEndOfLine => {
if let CursorMode::Insert(selection) = cursor.mode.clone() {
let mut new_selection = Selection::new();
for region in selection.regions() {
let (start_line, _) = rope_text.offset_to_line_col(region.min());
let (end_line, end_col) = rope_text.offset_to_line_col(region.max());
for line in start_line..end_line + 1 {
let offset = if line == end_line {
rope_text.offset_of_line_col(line, end_col)
} else {
rope_text.line_end_offset(line, true)
};
new_selection.add_region(SelRegion::new(offset, offset, None));
}
}
cursor.set_insert(new_selection);
}
}
SelectCurrentLine => {
if let CursorMode::Insert(selection) = cursor.mode.clone() {
let mut new_selection = Selection::new();
for region in selection.regions() {
let start_line = rope_text.line_of_offset(region.min());
let start = rope_text.offset_of_line(start_line);
let end_line = rope_text.line_of_offset(region.max());
let end = rope_text.offset_of_line(end_line + 1);
new_selection.add_region(SelRegion::new(start, end, None));
}
cursor.set_insert(new_selection);
}
}
SelectAllCurrent | SelectNextCurrent | SelectSkipCurrent => {
}
SelectAll => {
let new_selection = Selection::region(0, rope_text.len());
cursor.set_insert(new_selection);
}
}
}
pub fn do_motion_mode(
ed: &Editor,
action: &dyn CommonAction,
cursor: &mut Cursor,
motion_mode: MotionMode,
register: &mut Register,
) {
if let Some(cached_motion_mode) = cursor.motion_mode.take() {
if core::mem::discriminant(&cached_motion_mode) == core::mem::discriminant(&motion_mode) {
let offset = cursor.offset();
action.exec_motion_mode(
ed,
cursor,
cached_motion_mode,
offset..offset,
true,
register,
);
}
} else {
cursor.motion_mode = Some(motion_mode);
}
}
#[cfg(test)]
mod tests {
use std::rc::Rc;
use floem_editor_core::{
buffer::rope_text::{RopeText, RopeTextVal},
cursor::{ColPosition, CursorAffinity},
mode::Mode,
};
use floem_reactive::{Scope, SignalUpdate};
use lapce_xi_rope::Rope;
use peniko::kurbo::{Rect, Size};
use crate::views::editor::{
movement::{correct_crlf, end_of_line, move_down, move_up},
text::SimpleStyling,
text_document::TextDocument,
};
use super::Editor;
fn make_ed(text: &str) -> Editor {
let cx = Scope::new();
let doc = Rc::new(TextDocument::new(cx, text));
let style = Rc::new(SimpleStyling::new());
let editor = Editor::new(cx, doc, style, false);
editor
.viewport
.set(Rect::ZERO.with_size(Size::new(f64::MAX, f64::MAX)));
editor
}
#[test]
fn test_correct_crlf() {
let text = Rope::from("hello\nworld");
let text = RopeTextVal::new(text);
assert_eq!(correct_crlf(&text, 0), 0);
assert_eq!(correct_crlf(&text, 5), 5);
assert_eq!(correct_crlf(&text, 6), 6);
assert_eq!(correct_crlf(&text, text.len()), text.len());
let text = Rope::from("hello\r\nworld");
let text = RopeTextVal::new(text);
assert_eq!(correct_crlf(&text, 0), 0);
assert_eq!(correct_crlf(&text, 5), 5);
assert_eq!(correct_crlf(&text, 6), 5);
assert_eq!(correct_crlf(&text, 7), 7);
assert_eq!(correct_crlf(&text, text.len()), text.len());
}
#[test]
fn test_end_of_line() {
let ed = make_ed("abc\ndef\nghi");
let mut aff = CursorAffinity::Backward;
assert_eq!(end_of_line(&ed, &mut aff, 0, Mode::Insert).0, 3);
assert_eq!(aff, CursorAffinity::Backward);
assert_eq!(end_of_line(&ed, &mut aff, 1, Mode::Insert).0, 3);
assert_eq!(aff, CursorAffinity::Backward);
assert_eq!(end_of_line(&ed, &mut aff, 3, Mode::Insert).0, 3);
assert_eq!(aff, CursorAffinity::Backward);
assert_eq!(end_of_line(&ed, &mut aff, 4, Mode::Insert).0, 7);
assert_eq!(end_of_line(&ed, &mut aff, 5, Mode::Insert).0, 7);
assert_eq!(end_of_line(&ed, &mut aff, 7, Mode::Insert).0, 7);
let ed = make_ed("abc\r\ndef\r\nghi");
let mut aff = CursorAffinity::Forward;
assert_eq!(end_of_line(&ed, &mut aff, 0, Mode::Insert).0, 3);
assert_eq!(aff, CursorAffinity::Backward);
assert_eq!(end_of_line(&ed, &mut aff, 1, Mode::Insert).0, 3);
assert_eq!(aff, CursorAffinity::Backward);
assert_eq!(end_of_line(&ed, &mut aff, 3, Mode::Insert).0, 3);
assert_eq!(aff, CursorAffinity::Backward);
assert_eq!(end_of_line(&ed, &mut aff, 5, Mode::Insert).0, 8);
assert_eq!(end_of_line(&ed, &mut aff, 6, Mode::Insert).0, 8);
assert_eq!(end_of_line(&ed, &mut aff, 7, Mode::Insert).0, 8);
assert_eq!(end_of_line(&ed, &mut aff, 8, Mode::Insert).0, 8);
let ed = make_ed("testing\r\nAbout\r\nblah");
let mut aff = CursorAffinity::Backward;
assert_eq!(end_of_line(&ed, &mut aff, 0, Mode::Insert).0, 7);
}
#[test]
fn test_move_down() {
let ed = make_ed("abc\n\n\ndef\n\nghi");
let mut aff = CursorAffinity::Forward;
assert_eq!(move_down(&ed, 0, &mut aff, None, Mode::Insert, 1).0, 4);
let (offset, horiz) = move_down(&ed, 1, &mut aff, None, Mode::Insert, 1);
assert_eq!(offset, 4);
assert!(matches!(horiz, ColPosition::Col(_)));
let (offset, horiz) = move_down(&ed, 4, &mut aff, Some(horiz), Mode::Insert, 1);
assert_eq!(offset, 5);
assert!(matches!(horiz, ColPosition::Col(_)));
let (offset, _) = move_down(&ed, 5, &mut aff, Some(horiz), Mode::Insert, 1);
assert_eq!(offset, 7);
}
#[test]
fn test_move_up() {
let ed = make_ed("abc\n\n\ndef\n\nghi");
let mut aff = CursorAffinity::Forward;
assert_eq!(move_up(&ed, 0, &mut aff, None, Mode::Insert, 1).0, 0);
let (offset, horiz) = move_up(&ed, 7, &mut aff, None, Mode::Insert, 1);
assert_eq!(offset, 5);
assert!(matches!(horiz, ColPosition::Col(_)));
let (offset, horiz) = move_up(&ed, 5, &mut aff, Some(horiz), Mode::Insert, 1);
assert_eq!(offset, 4);
assert!(matches!(horiz, ColPosition::Col(_)));
let (offset, _) = move_up(&ed, 4, &mut aff, Some(horiz), Mode::Insert, 1);
assert_eq!(offset, 1);
}
}