diff --git a/helix-core/src/selection.rs b/helix-core/src/selection.rs index a382a7186..5c7130258 100644 --- a/helix-core/src/selection.rs +++ b/helix-core/src/selection.rs @@ -170,6 +170,15 @@ impl Range { self.from() <= pos && pos < self.to() } + /// Returns equal if anchor and head are the same, and disregards old_visual_position. When + /// a single character is selected, the orientation (i.e. which is the head and which is the + /// anchor) is indistinguishable and should be disregarded. + pub fn visual_eq(&self, other: Range) -> bool { + self.anchor == other.anchor && self.head == other.head + // TODO: this does not work for graphemes like \r\n + || self.len() == 1 && self.from() == other.from() && self.to() == other.to() + } + /// Map a range through a set of changes. Returns a new range representing /// the same position after the changes are applied. Note that this /// function runs in O(N) (N is number of changes) and can therefore @@ -710,6 +719,14 @@ impl Selection { } } } + + /// Returns true if two selections are identical as perceived by the user + pub fn visual_eq(&self, other: Selection) -> bool { + self.ranges() + .iter() + .zip(other.ranges().iter()) + .all(|(&r1, &r2)| r1.visual_eq(r2)) + } } impl<'a> IntoIterator for &'a Selection { diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 6e037a471..0fa27c392 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -523,6 +523,10 @@ impl MappableCommand { surround_delete, "Surround delete", select_textobject_around, "Select around object", select_textobject_inner, "Select inside object", + select_next_word, "Select next whole word", + select_next_long_word, "Select next whole long word", + select_prev_word, "Select previous whole word", + select_prev_long_word, "Select previous whole long word", goto_next_function, "Goto next function", goto_prev_function, "Goto previous function", goto_next_class, "Goto next type definition", @@ -5454,81 +5458,7 @@ fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) { cx.on_next_key(move |cx, event| { cx.editor.autoinfo = None; if let Some(ch) = event.char() { - let textobject = move |editor: &mut Editor| { - let (view, doc) = current!(editor); - let text = doc.text().slice(..); - - let textobject_treesitter = |obj_name: &str, range: Range| -> Range { - let (lang_config, syntax) = match doc.language_config().zip(doc.syntax()) { - Some(t) => t, - None => return range, - }; - textobject::textobject_treesitter( - text, - range, - objtype, - obj_name, - syntax.tree().root_node(), - lang_config, - count, - ) - }; - - if ch == 'g' && doc.diff_handle().is_none() { - editor.set_status("Diff is not available in current buffer"); - return; - } - - let textobject_change = |range: Range| -> Range { - let diff_handle = doc.diff_handle().unwrap(); - let diff = diff_handle.load(); - let line = range.cursor_line(text); - let hunk_idx = if let Some(hunk_idx) = diff.hunk_at(line as u32, false) { - hunk_idx - } else { - return range; - }; - let hunk = diff.nth_hunk(hunk_idx).after; - - let start = text.line_to_char(hunk.start as usize); - let end = text.line_to_char(hunk.end as usize); - Range::new(start, end).with_direction(range.direction()) - }; - - let selection = doc.selection(view.id).clone().transform(|range| { - match ch { - 'w' => textobject::textobject_word(text, range, objtype, count, false), - 'W' => textobject::textobject_word(text, range, objtype, count, true), - 't' => textobject_treesitter("class", range), - 'f' => textobject_treesitter("function", range), - 'a' => textobject_treesitter("parameter", range), - 'c' => textobject_treesitter("comment", range), - 'T' => textobject_treesitter("test", range), - 'e' => textobject_treesitter("entry", range), - 'p' => textobject::textobject_paragraph(text, range, objtype, count), - 'm' => textobject::textobject_pair_surround_closest( - doc.syntax(), - text, - range, - objtype, - count, - ), - 'g' => textobject_change(range), - // TODO: cancel new ranges if inconsistent surround matches across lines - ch if !ch.is_ascii_alphanumeric() => textobject::textobject_pair_surround( - doc.syntax(), - text, - range, - objtype, - ch, - count, - ), - _ => range, - } - }); - doc.set_selection(view.id, selection); - }; - cx.editor.apply_motion(textobject); + select_textobject_for_char(cx, ch, objtype, count); } }); @@ -5555,6 +5485,138 @@ fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) { cx.editor.autoinfo = Some(Info::new(title, &help_text)); } +fn select_textobject_for_char( + cx: &mut Context, + ch: char, + objtype: textobject::TextObject, + count: usize, +) { + let textobject = move |editor: &mut Editor| { + let (view, doc) = current!(editor); + let text = doc.text().slice(..); + + let textobject_treesitter = |obj_name: &str, range: Range| -> Range { + let (lang_config, syntax) = match doc.language_config().zip(doc.syntax()) { + Some(t) => t, + None => return range, + }; + textobject::textobject_treesitter( + text, + range, + objtype, + obj_name, + syntax.tree().root_node(), + lang_config, + count, + ) + }; + + if ch == 'g' && doc.diff_handle().is_none() { + editor.set_status("Diff is not available in current buffer"); + return; + } + + let textobject_change = |range: Range| -> Range { + let diff_handle = doc.diff_handle().unwrap(); + let diff = diff_handle.load(); + let line = range.cursor_line(text); + let hunk_idx = if let Some(hunk_idx) = diff.hunk_at(line as u32, false) { + hunk_idx + } else { + return range; + }; + let hunk = diff.nth_hunk(hunk_idx).after; + + let start = text.line_to_char(hunk.start as usize); + let end = text.line_to_char(hunk.end as usize); + Range::new(start, end).with_direction(range.direction()) + }; + + let selection = doc.selection(view.id).clone().transform(|range| { + match ch { + 'w' => textobject::textobject_word(text, range, objtype, count, false), + 'W' => textobject::textobject_word(text, range, objtype, count, true), + 't' => textobject_treesitter("class", range), + 'f' => textobject_treesitter("function", range), + 'a' => textobject_treesitter("parameter", range), + 'c' => textobject_treesitter("comment", range), + 'T' => textobject_treesitter("test", range), + 'e' => textobject_treesitter("entry", range), + 'p' => textobject::textobject_paragraph(text, range, objtype, count), + 'm' => textobject::textobject_pair_surround_closest( + doc.syntax(), + text, + range, + objtype, + count, + ), + 'g' => textobject_change(range), + // TODO: cancel new ranges if inconsistent surround matches across lines + ch if !ch.is_ascii_alphanumeric() => textobject::textobject_pair_surround( + doc.syntax(), + text, + range, + objtype, + ch, + count, + ), + _ => range, + } + }); + doc.set_selection(view.id, selection); + }; + cx.editor.apply_motion(textobject); +} + +fn select_next_word(cx: &mut Context) { + let current_selection = get_current_selection(cx); + select_textobject_for_char(cx, 'w', textobject::TextObject::Inside, cx.count()); + let new_selection = get_current_selection(cx); + if current_selection.visual_eq(new_selection) { + move_next_word_end(cx); + select_textobject_for_char(cx, 'w', textobject::TextObject::Inside, cx.count()); + } +} + +fn select_next_long_word(cx: &mut Context) { + let current_selection = get_current_selection(cx); + select_textobject_for_char(cx, 'W', textobject::TextObject::Inside, cx.count()); + let new_selection = get_current_selection(cx); + if current_selection.visual_eq(new_selection) { + move_next_long_word_end(cx); + select_textobject_for_char(cx, 'W', textobject::TextObject::Inside, cx.count()); + } +} + +fn select_prev_word(cx: &mut Context) { + let current_selection = get_current_selection(cx); + select_textobject_for_char(cx, 'w', textobject::TextObject::Inside, cx.count()); + flip_selections(cx); + let new_selection = get_current_selection(cx); + if current_selection.visual_eq(new_selection) { + move_prev_word_start(cx); + select_textobject_for_char(cx, 'w', textobject::TextObject::Inside, cx.count()); + flip_selections(cx); + } +} + +fn select_prev_long_word(cx: &mut Context) { + let current_selection = get_current_selection(cx); + select_textobject_for_char(cx, 'W', textobject::TextObject::Inside, cx.count()); + flip_selections(cx); + let new_selection = get_current_selection(cx); + if current_selection.visual_eq(new_selection) { + move_prev_long_word_start(cx); + select_textobject_for_char(cx, 'W', textobject::TextObject::Inside, cx.count()); + flip_selections(cx); + } +} + +fn get_current_selection(cx: &mut Context) -> Selection { + let (view, doc) = current!(cx.editor); + doc.selection(view.id).clone() +} + fn surround_add(cx: &mut Context) { cx.on_next_key(move |cx, event| { let (view, doc) = current!(cx.editor);