diff --git a/helix-core/src/movement.rs b/helix-core/src/movement.rs index 57abc4989..88d6df0a0 100644 --- a/helix-core/src/movement.rs +++ b/helix-core/src/movement.rs @@ -1,6 +1,66 @@ use crate::graphemes::{nth_next_grapheme_boundary, nth_prev_grapheme_boundary, RopeGraphemes}; use crate::{coords_at_pos, pos_at_coords, ChangeSet, Position, Range, Rope, RopeSlice, Selection}; +#[derive(Copy, Clone, PartialEq, Eq)] +pub enum Direction { + Forward, + Backward, +} + +pub fn move_horizontally( + text: RopeSlice, + range: Range, + dir: Direction, + count: usize, + extend: bool, +) -> Range { + let pos = range.head; + let line = text.char_to_line(pos); + // TODO: we can optimize clamping by passing in RopeSlice limited to current line. that way + // we stop calculating past start/end of line. + let pos = match dir { + Direction::Backward => { + let start = text.line_to_char(line); + nth_prev_grapheme_boundary(text, pos, count).max(start) + } + Direction::Forward => { + // Line end is pos at the start of next line - 1 + // subtract another 1 because the line ends with \n + let end = text.line_to_char(line + 1).saturating_sub(2); + nth_next_grapheme_boundary(text, pos, count).min(end) + } + }; + Range::new(if extend { range.anchor } else { pos }, pos) +} + +pub fn move_vertically( + text: RopeSlice, + range: Range, + dir: Direction, + count: usize, + extend: bool, +) -> Range { + let Position { row, col } = coords_at_pos(text, range.head); + + let horiz = range.horiz.unwrap_or(col as u32); + + let new_line = match dir { + Direction::Backward => row.saturating_sub(count), + Direction::Forward => std::cmp::min(row.saturating_add(count), text.len_lines() - 1), + }; + + // convert to 0-indexed, subtract another 1 because len_chars() counts \n + let new_line_len = text.line(new_line).len_chars().saturating_sub(2); + + let new_col = std::cmp::min(horiz as usize, new_line_len); + + let pos = pos_at_coords(text, Position::new(new_line, new_col)); + + let mut range = Range::new(if extend { range.anchor } else { pos }, pos); + range.horiz = Some(horiz); + range +} + pub fn move_next_word_start(slice: RopeSlice, mut pos: usize, count: usize) -> usize { for _ in 0..count { if pos + 1 == slice.len_chars() { @@ -155,3 +215,24 @@ where *pos = pos.saturating_sub(1); } } + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_vertical_move() { + let text = Rope::from("abcd\nefg\nwrs"); + let slice = text.slice(..); + let pos = pos_at_coords(slice, (0, 4).into()); + + let range = Range::new(pos, pos); + assert_eq!( + coords_at_pos( + slice, + move_vertically(slice, range, Direction::Forward, 1, false).head + ), + (1, 2).into() + ); + } +} diff --git a/helix-core/src/state.rs b/helix-core/src/state.rs index 9dcd5548b..ab475cab7 100644 --- a/helix-core/src/state.rs +++ b/helix-core/src/state.rs @@ -1,25 +1,12 @@ -use crate::graphemes::{nth_next_grapheme_boundary, nth_prev_grapheme_boundary, RopeGraphemes}; -use crate::{coords_at_pos, pos_at_coords, ChangeSet, Position, Range, Rope, RopeSlice, Selection}; +use crate::{Rope, Selection}; /// A state represents the current editor state of a single buffer. #[derive(Clone)] pub struct State { - // TODO: fields should be private but we need to refactor commands.rs first pub doc: Rope, pub selection: Selection, } -#[derive(Copy, Clone, PartialEq, Eq)] -pub enum Direction { - Forward, - Backward, -} -#[derive(Copy, Clone, PartialEq, Eq)] -pub enum Granularity { - Character, - Line, -} - impl State { #[must_use] pub fn new(doc: Rope) -> Self { @@ -51,102 +38,4 @@ impl State { // syntax // foldable // changeFilter/transactionFilter - - pub fn move_range( - &self, - range: Range, - dir: Direction, - granularity: Granularity, - count: usize, - extend: bool, - ) -> Range { - let text = self.doc.slice(..); - let pos = range.head; - let line = text.char_to_line(pos); - // TODO: we can optimize clamping by passing in RopeSlice limited to current line. that way - // we stop calculating past start/end of line. - let pos = match (dir, granularity) { - (Direction::Backward, Granularity::Character) => { - let start = text.line_to_char(line); - nth_prev_grapheme_boundary(text, pos, count).max(start) - } - (Direction::Forward, Granularity::Character) => { - // Line end is pos at the start of next line - 1 - // subtract another 1 because the line ends with \n - let end = text.line_to_char(line + 1).saturating_sub(2); - nth_next_grapheme_boundary(text, pos, count).min(end) - } - (_, Granularity::Line) => return move_vertically(text, dir, range, count, extend), - }; - Range::new(if extend { range.anchor } else { pos }, pos) - } - - pub fn move_selection( - &self, - dir: Direction, - granularity: Granularity, - count: usize, - ) -> Selection { - self.selection - .transform(|range| self.move_range(range, dir, granularity, count, false)) - } - - pub fn extend_selection( - &self, - dir: Direction, - granularity: Granularity, - count: usize, - ) -> Selection { - self.selection - .transform(|range| self.move_range(range, dir, granularity, count, true)) - } -} - -fn move_vertically( - text: RopeSlice, - dir: Direction, - range: Range, - count: usize, - extend: bool, -) -> Range { - let Position { row, col } = coords_at_pos(text, range.head); - - let horiz = range.horiz.unwrap_or(col as u32); - - let new_line = match dir { - Direction::Backward => row.saturating_sub(count), - Direction::Forward => std::cmp::min(row.saturating_add(count), text.len_lines() - 1), - }; - - // convert to 0-indexed, subtract another 1 because len_chars() counts \n - let new_line_len = text.line(new_line).len_chars().saturating_sub(2); - - let new_col = std::cmp::min(horiz as usize, new_line_len); - - let pos = pos_at_coords(text, Position::new(new_line, new_col)); - - let mut range = Range::new(if extend { range.anchor } else { pos }, pos); - range.horiz = Some(horiz); - range -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn test_vertical_move() { - let text = Rope::from("abcd\nefg\nwrs"); - let slice = text.slice(..); - let pos = pos_at_coords(slice, (0, 4).into()); - - let range = Range::new(pos, pos); - assert_eq!( - coords_at_pos( - slice, - move_vertically(slice, Direction::Forward, range, 1, false).head - ), - (1, 2).into() - ); - } } diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 5b742e43e..6165c4c13 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -1,11 +1,11 @@ use helix_core::{ comment, coords_at_pos, graphemes, indent::TAB_WIDTH, - movement, object, pos_at_coords, + movement::{self, Direction}, + object, pos_at_coords, regex::{self, Regex}, - register, search, selection, - state::{Direction, Granularity}, - Change, ChangeSet, Position, Range, Rope, RopeSlice, Selection, Tendril, Transaction, + register, search, selection, Change, ChangeSet, Position, Range, Rope, RopeSlice, Selection, + Tendril, Transaction, }; use once_cell::sync::Lazy; @@ -72,36 +72,64 @@ pub type Command = fn(cx: &mut Context); pub fn move_char_left(cx: &mut Context) { let count = cx.count; let doc = cx.doc(); - let selection = doc - .state - .move_selection(Direction::Backward, Granularity::Character, count); + let text = doc.text().slice(..); + let selection = doc.selection().transform(|range| { + movement::move_horizontally( + text, + range, + Direction::Backward, + count, + false, /* extend */ + ) + }); doc.set_selection(selection); } pub fn move_char_right(cx: &mut Context) { let count = cx.count; let doc = cx.doc(); - let selection = doc - .state - .move_selection(Direction::Forward, Granularity::Character, count); + let text = doc.text().slice(..); + let selection = doc.selection().transform(|range| { + movement::move_horizontally( + text, + range, + Direction::Forward, + count, + false, /* extend */ + ) + }); doc.set_selection(selection); } pub fn move_line_up(cx: &mut Context) { let count = cx.count; let doc = cx.doc(); - let selection = doc - .state - .move_selection(Direction::Backward, Granularity::Line, count); + let text = doc.text().slice(..); + let selection = doc.selection().transform(|range| { + movement::move_vertically( + text, + range, + Direction::Backward, + count, + false, /* extend */ + ) + }); doc.set_selection(selection); } pub fn move_line_down(cx: &mut Context) { let count = cx.count; let doc = cx.doc(); - let selection = doc - .state - .move_selection(Direction::Forward, Granularity::Line, count); + let text = doc.text().slice(..); + let selection = doc.selection().transform(|range| { + movement::move_vertically( + text, + range, + Direction::Forward, + count, + false, /* extend */ + ) + }); doc.set_selection(selection); } @@ -409,36 +437,64 @@ pub fn half_page_down(cx: &mut Context) { pub fn extend_char_left(cx: &mut Context) { let count = cx.count; let doc = cx.doc(); - let selection = doc - .state - .extend_selection(Direction::Backward, Granularity::Character, count); + let text = doc.text().slice(..); + let selection = doc.selection().transform(|range| { + movement::move_horizontally( + text, + range, + Direction::Backward, + count, + true, /* extend */ + ) + }); doc.set_selection(selection); } pub fn extend_char_right(cx: &mut Context) { let count = cx.count; let doc = cx.doc(); - let selection = doc - .state - .extend_selection(Direction::Forward, Granularity::Character, count); + let text = doc.text().slice(..); + let selection = doc.selection().transform(|range| { + movement::move_horizontally( + text, + range, + Direction::Forward, + count, + true, /* extend */ + ) + }); doc.set_selection(selection); } pub fn extend_line_up(cx: &mut Context) { let count = cx.count; let doc = cx.doc(); - let selection = doc - .state - .extend_selection(Direction::Backward, Granularity::Line, count); + let text = doc.text().slice(..); + let selection = doc.selection().transform(|range| { + movement::move_vertically( + text, + range, + Direction::Backward, + count, + true, /* extend */ + ) + }); doc.set_selection(selection); } pub fn extend_line_down(cx: &mut Context) { let count = cx.count; let doc = cx.doc(); - let selection = doc - .state - .extend_selection(Direction::Forward, Granularity::Line, count); + let text = doc.text().slice(..); + let selection = doc.selection().transform(|range| { + movement::move_vertically( + text, + range, + Direction::Forward, + count, + true, /* extend */ + ) + }); doc.set_selection(selection); } diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index f23765048..f7ba59cd4 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -31,7 +31,7 @@ pub fn regex_prompt( prompt: String, fun: impl Fn(&mut Document, Regex) + 'static, ) -> Prompt { - let snapshot = cx.doc().state.clone(); + let snapshot = cx.doc().selection().clone(); Prompt::new( prompt, @@ -39,9 +39,10 @@ pub fn regex_prompt( move |editor: &mut Editor, input: &str, event: PromptEvent| { match event { PromptEvent::Abort => { - // revert state + // TODO: also revert doc + // TODO: also revert text let doc = &mut editor.view_mut().doc; - doc.state = snapshot.clone(); + doc.set_selection(snapshot.clone()); } PromptEvent::Validate => { // @@ -58,7 +59,8 @@ pub fn regex_prompt( let doc = &mut view.doc; // revert state to what it was before the last update - doc.state = snapshot.clone(); + // TODO: also revert text + doc.set_selection(snapshot.clone()); fun(doc, regex); diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 76a3b8e93..e606ec3c4 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -17,7 +17,8 @@ pub enum Mode { } pub struct Document { - pub state: State, // rope + selection + // rope + selection + state: State, path: Option, /// Current editing mode.