diff --git a/helix-core/src/lib.rs b/helix-core/src/lib.rs index 7b401557..89b82c4e 100644 --- a/helix-core/src/lib.rs +++ b/helix-core/src/lib.rs @@ -8,6 +8,7 @@ pub mod macros; pub mod object; mod position; pub mod register; +pub mod search; pub mod selection; pub mod state; pub mod syntax; diff --git a/helix-core/src/search.rs b/helix-core/src/search.rs index 7d790d66..c03f60df 100644 --- a/helix-core/src/search.rs +++ b/helix-core/src/search.rs @@ -1,69 +1,39 @@ use crate::RopeSlice; -pub fn find_nth_next(text: RopeSlice, ch: char, pos: usize, n: usize) -> Option { +pub fn find_nth_next(text: RopeSlice, ch: char, mut pos: usize, n: usize) -> Option { // start searching right after pos - let mut byte_idx = text.char_to_byte(pos + 1); - - let (mut chunks, mut chunk_byte_idx, _chunk_char_idx, _chunk_line_idx) = - text.chunks_at_byte(byte_idx); - - let mut chunk = chunks.next().unwrap_or(""); - - chunk = &chunk[(byte_idx - chunk_byte_idx)..]; + let mut chars = text.chars_at(pos + 1); for _ in 0..n { loop { - match chunk.find(ch) { - Some(pos) => { - byte_idx += pos; - chunk = &chunk[pos + 1..]; - break; - } - None => match chunks.next() { - Some(next_chunk) => { - byte_idx += chunk.len(); - chunk = next_chunk; - } - None => { - log::info!("no more chunks"); - return None; - } - }, + let c = chars.next()?; + + pos += 1; + + if c == ch { + break; } } } - Some(text.byte_to_char(byte_idx)) + + Some(pos) } -pub fn find_nth_prev(text: RopeSlice, ch: char, pos: usize, n: usize) -> Option { +pub fn find_nth_prev(text: RopeSlice, ch: char, mut pos: usize, n: usize) -> Option { // start searching right before pos - let mut byte_idx = text.char_to_byte(pos.saturating_sub(1)); - - let (mut chunks, mut chunk_byte_idx, _chunk_char_idx, _chunk_line_idx) = - text.chunks_at_byte(byte_idx); - - let mut chunk = chunks.prev().unwrap_or(""); - - // start searching from pos - chunk = &chunk[..=byte_idx - chunk_byte_idx]; + let mut chars = text.chars_at(pos.saturating_sub(1)); for _ in 0..n { loop { - match chunk.rfind(ch) { - Some(pos) => { - byte_idx = chunk_byte_idx + pos; - chunk = &chunk[..pos]; - break; - } - None => match chunks.prev() { - Some(prev_chunk) => { - chunk_byte_idx -= chunk.len(); - chunk = prev_chunk; - } - None => return None, - }, + let c = chars.prev()?; + + pos = pos.saturating_sub(1); + + if c == ch { + break; } } } - Some(text.byte_to_char(byte_idx)) + + Some(pos) } diff --git a/helix-core/src/state.rs b/helix-core/src/state.rs index d2ebca47..8ff86f0c 100644 --- a/helix-core/src/state.rs +++ b/helix-core/src/state.rs @@ -123,6 +123,8 @@ impl State { pub fn move_next_word_start(slice: RopeSlice, mut pos: usize, count: usize) -> usize { // TODO: confirm it's fine without using graphemes, I think it should be for _ in 0..count { + // TODO: if end return end + let ch = slice.char(pos); let next = slice.char(pos.saturating_add(1)); if categorize(ch) != categorize(next) { @@ -148,8 +150,12 @@ impl State { pub fn move_prev_word_start(slice: RopeSlice, mut pos: usize, count: usize) -> usize { // TODO: confirm it's fine without using graphemes, I think it should be for _ in 0..count { + if pos == 0 { + return pos; + } + let ch = slice.char(pos); - let prev = slice.char(pos.saturating_sub(1)); // TODO: just return original pos if at start + let prev = slice.char(pos - 1); if categorize(ch) != categorize(prev) { pos -= 1; @@ -176,6 +182,8 @@ impl State { pub fn move_next_word_end(slice: RopeSlice, mut pos: usize, count: usize) -> usize { for _ in 0..count { + // TODO: if end return end + // TODO: confirm it's fine without using graphemes, I think it should be let ch = slice.char(pos); let next = slice.char(pos.saturating_add(1)); @@ -303,7 +311,7 @@ where if !fun(ch) { break; } - *pos += 1; + *pos += 1; // TODO: can go 1 over end of doc } } @@ -319,7 +327,7 @@ where if !fun(ch) { break; } - *pos -= 1; + *pos -= pos.saturating_sub(1); } } diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 47a61b7f..f60f646e 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -3,7 +3,7 @@ use helix_core::{ indent::TAB_WIDTH, object, regex::{self, Regex}, - register, selection, + register, search, selection, state::{coords_at_pos, pos_at_coords, Direction, Granularity, State}, Change, ChangeSet, Position, Range, Selection, Tendril, Transaction, }; @@ -19,12 +19,15 @@ use helix_view::{ Document, Editor, }; +use crossterm::event::{KeyCode, KeyEvent}; + pub struct Context<'a> { pub count: usize, pub editor: &'a mut Editor, pub executor: &'static smol::Executor<'static>, pub callback: Option, + pub on_next_key_callback: Option>, } impl<'a> Context<'a> { @@ -49,6 +52,14 @@ impl<'a> Context<'a> { }, )); } + + #[inline] + pub fn on_next_key( + &mut self, + on_next_key_callback: impl FnOnce(&mut Context, KeyEvent) + 'static, + ) { + self.on_next_key_callback = Some(Box::new(on_next_key_callback)); + } } /// A command is a function that takes the current state and a count, and does a side-effect on the @@ -225,6 +236,36 @@ pub fn extend_next_word_end(cx: &mut Context) { doc.set_selection(selection); } +pub fn find_next_char(cx: &mut Context) { + // TODO: count is reset to 1 before next key so we move it into the closure here. + // Would be nice to carry over. + let count = cx.count; + + // need to wait for next key + cx.on_next_key(move |cx, event| { + if let KeyEvent { + code: KeyCode::Char(ch), + .. + } = event + { + let doc = cx.doc(); + let text = doc.text().slice(..); + + let selection = doc.selection().transform(|mut range| { + if let Some(pos) = search::find_nth_next(text, ch, range.head, count) { + Range::new(range.head, pos) + // or (range.anchor, pos) for extend + // or (pos, pos) to move to found val + } else { + range + } + }); + + doc.set_selection(selection); + } + }) +} + fn scroll(view: &mut View, offset: usize, direction: Direction) { use Direction::*; let text = view.doc.text().slice(..); diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index 3be92fcc..d956679a 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -138,7 +138,7 @@ pub fn default() -> Keymaps { key!('l') => commands::move_char_right, // key!('t') => commands::till_next_char, - // key!('f') => commands::find_next_char, + key!('f') => commands::find_next_char, // key!('T') => commands::till_prev_char, // key!('f') => commands::find_prev_char, // and matching set for select mode (extend) diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 0bfc1a33..7a5e4aa5 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -20,6 +20,7 @@ use tui::{ pub struct EditorView { keymap: Keymaps, + on_next_key: Option>, } const OFFSET: u16 = 7; // 1 diagnostic + 5 linenr + 1 gutter @@ -28,6 +29,7 @@ impl EditorView { pub fn new() -> Self { Self { keymap: keymap::default(), + on_next_key: None, } } pub fn render_view( @@ -366,46 +368,55 @@ impl Component for EditorView { editor: &mut cx.editor, count: 1, callback: None, + on_next_key_callback: None, }; - match mode { - Mode::Insert => { - if let Some(command) = self.keymap[&Mode::Insert].get(&event) { - command(&mut cxt); - } else if let KeyEvent { - code: KeyCode::Char(c), - .. - } = event - { - commands::insert::insert_char(&mut cxt, c); - } - } - mode => { - match event { - KeyEvent { - code: KeyCode::Char(i @ '0'..='9'), - modifiers: KeyModifiers::NONE, - } => { - let i = i.to_digit(10).unwrap() as usize; - cxt.editor.count = Some(cxt.editor.count.map_or(i, |c| c * 10 + i)); + if let Some(on_next_key) = self.on_next_key.take() { + // if there's a command waiting input, do that first + on_next_key(&mut cxt, event); + } else { + match mode { + Mode::Insert => { + if let Some(command) = self.keymap[&Mode::Insert].get(&event) { + command(&mut cxt); + } else if let KeyEvent { + code: KeyCode::Char(c), + .. + } = event + { + commands::insert::insert_char(&mut cxt, c); } - _ => { - // set the count - cxt.count = cxt.editor.count.take().unwrap_or(1); - // TODO: edge case: 0j -> reset to 1 - // if this fails, count was Some(0) - // debug_assert!(cxt.count != 0); - - if let Some(command) = self.keymap[&mode].get(&event) { - command(&mut cxt); - - // TODO: simplistic ensure cursor in view for now + } + mode => { + match event { + KeyEvent { + code: KeyCode::Char(i @ '0'..='9'), + modifiers: KeyModifiers::NONE, + } => { + let i = i.to_digit(10).unwrap() as usize; + cxt.editor.count = + Some(cxt.editor.count.map_or(i, |c| c * 10 + i)); + } + _ => { + // set the count + cxt.count = cxt.editor.count.take().unwrap_or(1); + // TODO: edge case: 0j -> reset to 1 + // if this fails, count was Some(0) + // debug_assert!(cxt.count != 0); + + if let Some(command) = self.keymap[&mode].get(&event) { + command(&mut cxt); + + // TODO: simplistic ensure cursor in view for now + } } } } } } + self.on_next_key = cxt.on_next_key_callback.take(); + // appease borrowck let callback = cxt.callback.take(); drop(cxt);