Add whole word selection commands

pull/9904/head
chtenb 1 year ago
parent 620dfceb84
commit 07b3c49130

@ -170,6 +170,15 @@ impl Range {
self.from() <= pos && pos < self.to() 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 /// Map a range through a set of changes. Returns a new range representing
/// the same position after the changes are applied. Note that this /// the same position after the changes are applied. Note that this
/// function runs in O(N) (N is number of changes) and can therefore /// 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 { impl<'a> IntoIterator for &'a Selection {

@ -523,6 +523,10 @@ impl MappableCommand {
surround_delete, "Surround delete", surround_delete, "Surround delete",
select_textobject_around, "Select around object", select_textobject_around, "Select around object",
select_textobject_inner, "Select inside 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_next_function, "Goto next function",
goto_prev_function, "Goto previous function", goto_prev_function, "Goto previous function",
goto_next_class, "Goto next type definition", 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.on_next_key(move |cx, event| {
cx.editor.autoinfo = None; cx.editor.autoinfo = None;
if let Some(ch) = event.char() { if let Some(ch) = event.char() {
let textobject = move |editor: &mut Editor| { select_textobject_for_char(cx, ch, objtype, count);
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);
} }
}); });
@ -5555,6 +5485,138 @@ fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) {
cx.editor.autoinfo = Some(Info::new(title, &help_text)); 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) { fn surround_add(cx: &mut Context) {
cx.on_next_key(move |cx, event| { cx.on_next_key(move |cx, event| {
let (view, doc) = current!(cx.editor); let (view, doc) = current!(cx.editor);

Loading…
Cancel
Save