on master help sign in
find-till (f) prototype, on_next_key mode implementation.
62c9912Blaž Hrastnik
helix-core/src/lib.rs+1
@@ -8,6 +8,7 @@ pub mod macros;
8pub mod object;
8pub mod object;
9mod position;
9mod position;
10pub mod register;
10pub mod register;
11+pub mod search;
11pub mod selection;
12pub mod selection;
12pub mod state;
13pub mod state;
13pub mod syntax;
14pub mod syntax;
helix-core/src/search.rs+20−50
@@ -1,69 +1,39 @@
1use crate::RopeSlice;
1use crate::RopeSlice;
2
2
3−pub fn find_nth_next(text: RopeSlice, ch: char, pos: usize, n: usize) -> Option<usize> {
3+pub fn find_nth_next(text: RopeSlice, ch: char, mut pos: usize, n: usize) -> Option<usize> {
4 // start searching right after pos
4 // start searching right after pos
5− let mut byte_idx = text.char_to_byte(pos + 1);
5+ let mut chars = text.chars_at(pos + 1);
6−
7− let (mut chunks, mut chunk_byte_idx, _chunk_char_idx, _chunk_line_idx) =
8− text.chunks_at_byte(byte_idx);
9−
10− let mut chunk = chunks.next().unwrap_or("");
11−
12− chunk = &chunk[(byte_idx - chunk_byte_idx)..];
13
6
14 for _ in 0..n {
7 for _ in 0..n {
15 loop {
8 loop {
16− match chunk.find(ch) {
9+ let c = chars.next()?;
17− Some(pos) => {
10+
18− byte_idx += pos;
11+ pos += 1;
19− chunk = &chunk[pos + 1..];
12+
13+ if c == ch {
20− break;
14+ break;
21− }
22− None => match chunks.next() {
23− Some(next_chunk) => {
24− byte_idx += chunk.len();
25− chunk = next_chunk;
26− }
27− None => {
28− log::info!("no more chunks");
29− return None;
30− }
31− },
32 }
15 }
33 }
16 }
34 }
17 }
35− Some(text.byte_to_char(byte_idx))
18+
19+ Some(pos)
36}
20}
37
21
38−pub fn find_nth_prev(text: RopeSlice, ch: char, pos: usize, n: usize) -> Option<usize> {
22+pub fn find_nth_prev(text: RopeSlice, ch: char, mut pos: usize, n: usize) -> Option<usize> {
39 // start searching right before pos
23 // start searching right before pos
40− let mut byte_idx = text.char_to_byte(pos.saturating_sub(1));
24+ let mut chars = text.chars_at(pos.saturating_sub(1));
41−
42− let (mut chunks, mut chunk_byte_idx, _chunk_char_idx, _chunk_line_idx) =
43− text.chunks_at_byte(byte_idx);
44−
45− let mut chunk = chunks.prev().unwrap_or("");
46−
47− // start searching from pos
48− chunk = &chunk[..=byte_idx - chunk_byte_idx];
49
25
50 for _ in 0..n {
26 for _ in 0..n {
51 loop {
27 loop {
52− match chunk.rfind(ch) {
28+ let c = chars.prev()?;
53− Some(pos) => {
29+
54− byte_idx = chunk_byte_idx + pos;
30+ pos = pos.saturating_sub(1);
55− chunk = &chunk[..pos];
31+
32+ if c == ch {
56− break;
33+ break;
57− }
58− None => match chunks.prev() {
59− Some(prev_chunk) => {
60− chunk_byte_idx -= chunk.len();
61− chunk = prev_chunk;
62− }
63− None => return None,
64− },
65 }
34 }
66 }
35 }
67 }
36 }
68− Some(text.byte_to_char(byte_idx))
37+
38+ Some(pos)
69}
39}
helix-core/src/state.rs+11−3
@@ -123,6 +123,8 @@ impl State {
123 pub fn move_next_word_start(slice: RopeSlice, mut pos: usize, count: usize) -> usize {
123 pub fn move_next_word_start(slice: RopeSlice, mut pos: usize, count: usize) -> usize {
124 // TODO: confirm it's fine without using graphemes, I think it should be
124 // TODO: confirm it's fine without using graphemes, I think it should be
125 for _ in 0..count {
125 for _ in 0..count {
126+ // TODO: if end return end
127+
126 let ch = slice.char(pos);
128 let ch = slice.char(pos);
127 let next = slice.char(pos.saturating_add(1));
129 let next = slice.char(pos.saturating_add(1));
128 if categorize(ch) != categorize(next) {
130 if categorize(ch) != categorize(next) {
@@ -148,8 +150,12 @@ impl State {
148 pub fn move_prev_word_start(slice: RopeSlice, mut pos: usize, count: usize) -> usize {
150 pub fn move_prev_word_start(slice: RopeSlice, mut pos: usize, count: usize) -> usize {
149 // TODO: confirm it's fine without using graphemes, I think it should be
151 // TODO: confirm it's fine without using graphemes, I think it should be
150 for _ in 0..count {
152 for _ in 0..count {
153+ if pos == 0 {
154+ return pos;
155+ }
156+
151 let ch = slice.char(pos);
157 let ch = slice.char(pos);
152− let prev = slice.char(pos.saturating_sub(1)); // TODO: just return original pos if at start
158+ let prev = slice.char(pos - 1);
153
159
154 if categorize(ch) != categorize(prev) {
160 if categorize(ch) != categorize(prev) {
155 pos -= 1;
161 pos -= 1;
@@ -176,6 +182,8 @@ impl State {
176
182
177 pub fn move_next_word_end(slice: RopeSlice, mut pos: usize, count: usize) -> usize {
183 pub fn move_next_word_end(slice: RopeSlice, mut pos: usize, count: usize) -> usize {
178 for _ in 0..count {
184 for _ in 0..count {
185+ // TODO: if end return end
186+
179 // TODO: confirm it's fine without using graphemes, I think it should be
187 // TODO: confirm it's fine without using graphemes, I think it should be
180 let ch = slice.char(pos);
188 let ch = slice.char(pos);
181 let next = slice.char(pos.saturating_add(1));
189 let next = slice.char(pos.saturating_add(1));
@@ -303,7 +311,7 @@ where
303 if !fun(ch) {
311 if !fun(ch) {
304 break;
312 break;
305 }
313 }
306− *pos += 1;
314+ *pos += 1; // TODO: can go 1 over end of doc
307 }
315 }
308}
316}
309
317
@@ -319,7 +327,7 @@ where
319 if !fun(ch) {
327 if !fun(ch) {
320 break;
328 break;
321 }
329 }
322− *pos -= 1;
330+ *pos -= pos.saturating_sub(1);
323 }
331 }
324}
332}
325
333
helix-term/src/commands.rs+42−1
@@ -3,7 +3,7 @@ use helix_core::{
3 indent::TAB_WIDTH,
3 indent::TAB_WIDTH,
4 object,
4 object,
5 regex::{self, Regex},
5 regex::{self, Regex},
6− register, selection,
6+ register, search, selection,
7 state::{coords_at_pos, pos_at_coords, Direction, Granularity, State},
7 state::{coords_at_pos, pos_at_coords, Direction, Granularity, State},
8 Change, ChangeSet, Position, Range, Selection, Tendril, Transaction,
8 Change, ChangeSet, Position, Range, Selection, Tendril, Transaction,
9};
9};
@@ -19,12 +19,15 @@ use helix_view::{
19 Document, Editor,
19 Document, Editor,
20};
20};
21
21
22+use crossterm::event::{KeyCode, KeyEvent};
23+
22pub struct Context<'a> {
24pub struct Context<'a> {
23 pub count: usize,
25 pub count: usize,
24 pub editor: &'a mut Editor,
26 pub editor: &'a mut Editor,
25 pub executor: &'static smol::Executor<'static>,
27 pub executor: &'static smol::Executor<'static>,
26
28
27 pub callback: Option<crate::compositor::Callback>,
29 pub callback: Option<crate::compositor::Callback>,
30+ pub on_next_key_callback: Option<Box<dyn FnOnce(&mut Context, KeyEvent)>>,
28}
31}
29
32
30impl<'a> Context<'a> {
33impl<'a> Context<'a> {
@@ -49,6 +52,14 @@ impl<'a> Context<'a> {
49 },
52 },
50 ));
53 ));
51 }
54 }
55+
56+ #[inline]
57+ pub fn on_next_key(
58+ &mut self,
59+ on_next_key_callback: impl FnOnce(&mut Context, KeyEvent) + 'static,
60+ ) {
61+ self.on_next_key_callback = Some(Box::new(on_next_key_callback));
62+ }
52}
63}
53
64
54/// A command is a function that takes the current state and a count, and does a side-effect on the
65/// 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) {
225 doc.set_selection(selection);
236 doc.set_selection(selection);
226}
237}
227
238
239+pub fn find_next_char(cx: &mut Context) {
240+ // TODO: count is reset to 1 before next key so we move it into the closure here.
241+ // Would be nice to carry over.
242+ let count = cx.count;
243+
244+ // need to wait for next key
245+ cx.on_next_key(move |cx, event| {
246+ if let KeyEvent {
247+ code: KeyCode::Char(ch),
248+ ..
249+ } = event
250+ {
251+ let doc = cx.doc();
252+ let text = doc.text().slice(..);
253+
254+ let selection = doc.selection().transform(|mut range| {
255+ if let Some(pos) = search::find_nth_next(text, ch, range.head, count) {
256+ Range::new(range.head, pos)
257+ // or (range.anchor, pos) for extend
258+ // or (pos, pos) to move to found val
259+ } else {
260+ range
261+ }
262+ });
263+
264+ doc.set_selection(selection);
265+ }
266+ })
267+}
268+
228fn scroll(view: &mut View, offset: usize, direction: Direction) {
269fn scroll(view: &mut View, offset: usize, direction: Direction) {
229 use Direction::*;
270 use Direction::*;
230 let text = view.doc.text().slice(..);
271 let text = view.doc.text().slice(..);
helix-term/src/keymap.rs+1−1
@@ -138,7 +138,7 @@ pub fn default() -> Keymaps {
138 key!('l') => commands::move_char_right,
138 key!('l') => commands::move_char_right,
139
139
140 // key!('t') => commands::till_next_char,
140 // key!('t') => commands::till_next_char,
141− // key!('f') => commands::find_next_char,
141+ key!('f') => commands::find_next_char,
142 // key!('T') => commands::till_prev_char,
142 // key!('T') => commands::till_prev_char,
143 // key!('f') => commands::find_prev_char,
143 // key!('f') => commands::find_prev_char,
144 // and matching set for select mode (extend)
144 // and matching set for select mode (extend)
helix-term/src/ui/editor.rs+42−31
@@ -20,6 +20,7 @@ use tui::{
20
20
21pub struct EditorView {
21pub struct EditorView {
22 keymap: Keymaps,
22 keymap: Keymaps,
23+ on_next_key: Option<Box<dyn FnOnce(&mut commands::Context, KeyEvent)>>,
23}
24}
24
25
25const OFFSET: u16 = 7; // 1 diagnostic + 5 linenr + 1 gutter
26const OFFSET: u16 = 7; // 1 diagnostic + 5 linenr + 1 gutter
@@ -28,6 +29,7 @@ impl EditorView {
28 pub fn new() -> Self {
29 pub fn new() -> Self {
29 Self {
30 Self {
30 keymap: keymap::default(),
31 keymap: keymap::default(),
32+ on_next_key: None,
31 }
33 }
32 }
34 }
33 pub fn render_view(
35 pub fn render_view(
@@ -366,46 +368,55 @@ impl Component for EditorView {
366 editor: &mut cx.editor,
368 editor: &mut cx.editor,
367 count: 1,
369 count: 1,
368 callback: None,
370 callback: None,
371+ on_next_key_callback: None,
369 };
372 };
370
373
374+ if let Some(on_next_key) = self.on_next_key.take() {
375+ // if there's a command waiting input, do that first
376+ on_next_key(&mut cxt, event);
377+ } else {
371− match mode {
378+ match mode {
372− Mode::Insert => {
379+ Mode::Insert => {
373− if let Some(command) = self.keymap[&Mode::Insert].get(&event) {
380+ if let Some(command) = self.keymap[&Mode::Insert].get(&event) {
374− command(&mut cxt);
381+ command(&mut cxt);
375− } else if let KeyEvent {
382+ } else if let KeyEvent {
376− code: KeyCode::Char(c),
383+ code: KeyCode::Char(c),
377− ..
384+ ..
378− } = event
385+ } = event
379− {
386+ {
380− commands::insert::insert_char(&mut cxt, c);
387+ commands::insert::insert_char(&mut cxt, c);
381− }
382− }
383− mode => {
384− match event {
385− KeyEvent {
386− code: KeyCode::Char(i @ '0'..='9'),
387− modifiers: KeyModifiers::NONE,
388− } => {
389− let i = i.to_digit(10).unwrap() as usize;
390− cxt.editor.count = Some(cxt.editor.count.map_or(i, |c| c * 10 + i));
391 }
388 }
389+ }
390+ mode => {
391+ match event {
392+ KeyEvent {
393+ code: KeyCode::Char(i @ '0'..='9'),
394+ modifiers: KeyModifiers::NONE,
395+ } => {
396+ let i = i.to_digit(10).unwrap() as usize;
397+ cxt.editor.count =
398+ Some(cxt.editor.count.map_or(i, |c| c * 10 + i));
399+ }
392− _ => {
400+ _ => {
393− // set the count
401+ // set the count
394− cxt.count = cxt.editor.count.take().unwrap_or(1);
402+ cxt.count = cxt.editor.count.take().unwrap_or(1);
395− // TODO: edge case: 0j -> reset to 1
403+ // TODO: edge case: 0j -> reset to 1
396− // if this fails, count was Some(0)
404+ // if this fails, count was Some(0)
397− // debug_assert!(cxt.count != 0);
405+ // debug_assert!(cxt.count != 0);
398−
406+
399− if let Some(command) = self.keymap[&mode].get(&event) {
407+ if let Some(command) = self.keymap[&mode].get(&event) {
400− command(&mut cxt);
408+ command(&mut cxt);
401−
409+
402− // TODO: simplistic ensure cursor in view for now
410+ // TODO: simplistic ensure cursor in view for now
411+ }
403 }
412 }
404 }
413 }
405 }
414 }
406 }
415 }
407 }
416 }
408
417
418+ self.on_next_key = cxt.on_next_key_callback.take();
419+
409 // appease borrowck
420 // appease borrowck
410 let callback = cxt.callback.take();
421 let callback = cxt.callback.take();
411 drop(cxt);
422 drop(cxt);