Add the ] and [ operators to helix
This commit is contained in:
parent
69eaf04dad
commit
9b44bb6706
9 changed files with 504 additions and 71 deletions
|
@ -376,9 +376,11 @@
|
||||||
{
|
{
|
||||||
"context": "vim_mode == helix_normal && !menu",
|
"context": "vim_mode == helix_normal && !menu",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"m": "vim::PushHelixMatch",
|
|
||||||
"ctrl-[": "editor::Cancel",
|
"ctrl-[": "editor::Cancel",
|
||||||
":": "command_palette::Toggle",
|
":": "command_palette::Toggle",
|
||||||
|
"m": "vim::PushHelixMatch",
|
||||||
|
"]": ["vim::PushHelixNext", { "around": true }],
|
||||||
|
"[": ["vim::PushHelixPrevious", { "around": true }],
|
||||||
"left": "vim::WrappingLeft",
|
"left": "vim::WrappingLeft",
|
||||||
"right": "vim::WrappingRight",
|
"right": "vim::WrappingRight",
|
||||||
"h": "vim::WrappingLeft",
|
"h": "vim::WrappingLeft",
|
||||||
|
@ -456,6 +458,12 @@
|
||||||
"alt-shift-c": "editor::AddSelectionAbove"
|
"alt-shift-c": "editor::AddSelectionAbove"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"context": "vim_operator == helix_m",
|
||||||
|
"bindings": {
|
||||||
|
"m": "vim::Matching"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"context": "vim_mode == insert && !(showing_code_actions || showing_completions)",
|
"context": "vim_mode == insert && !(showing_code_actions || showing_completions)",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
|
@ -553,12 +561,6 @@
|
||||||
"e": "vim::EntireFile"
|
"e": "vim::EntireFile"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"context": "vim_operator == helix_m",
|
|
||||||
"bindings": {
|
|
||||||
"m": "vim::Matching"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"context": "vim_operator == c",
|
"context": "vim_operator == c",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
|
|
|
@ -104,6 +104,19 @@ impl<T: Copy + Ord> Selection<T> {
|
||||||
self.goal = new_goal;
|
self.goal = new_goal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_tail_head(&mut self, tail: T, head: T, new_goal: SelectionGoal) {
|
||||||
|
if tail > head {
|
||||||
|
self.start = head;
|
||||||
|
self.end = tail;
|
||||||
|
self.reversed = true;
|
||||||
|
} else {
|
||||||
|
self.start = tail;
|
||||||
|
self.end = head;
|
||||||
|
self.reversed = false;
|
||||||
|
}
|
||||||
|
self.goal = new_goal;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn swap_head_tail(&mut self) {
|
pub fn swap_head_tail(&mut self) {
|
||||||
if self.reversed {
|
if self.reversed {
|
||||||
self.reversed = false;
|
self.reversed = false;
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
mod boundary;
|
||||||
mod object;
|
mod object;
|
||||||
mod select;
|
mod select;
|
||||||
|
|
||||||
|
|
184
crates/vim/src/helix/boundary.rs
Normal file
184
crates/vim/src/helix/boundary.rs
Normal file
|
@ -0,0 +1,184 @@
|
||||||
|
use std::{error::Error, fmt::Display};
|
||||||
|
|
||||||
|
use editor::{
|
||||||
|
DisplayPoint,
|
||||||
|
display_map::{DisplaySnapshot, ToDisplayPoint},
|
||||||
|
};
|
||||||
|
use language::{CharClassifier, CharKind};
|
||||||
|
use text::Bias;
|
||||||
|
|
||||||
|
use crate::object::Object;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct UnboundedErr;
|
||||||
|
impl Display for UnboundedErr {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "object can't be found with simple boundary checking")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Error for UnboundedErr {}
|
||||||
|
|
||||||
|
impl Object {
|
||||||
|
/// Returns the beginning of the inside of the closest object after the cursor if it can easily be found. Follows helix convention;
|
||||||
|
pub fn helix_next_start(
|
||||||
|
self,
|
||||||
|
map: &DisplaySnapshot,
|
||||||
|
relative_to: DisplayPoint,
|
||||||
|
) -> Result<Option<DisplayPoint>, UnboundedErr> {
|
||||||
|
try_find_boundary(map, relative_to, |left, right| {
|
||||||
|
let classifier = map
|
||||||
|
.buffer_snapshot
|
||||||
|
.char_classifier_at(relative_to.to_point(map));
|
||||||
|
self.helix_is_start(right, left, classifier)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/// Returns the end of the inside of the closest object after the cursor if it can easily be found. Follows helix convention;
|
||||||
|
pub fn helix_next_end(
|
||||||
|
self,
|
||||||
|
map: &DisplaySnapshot,
|
||||||
|
relative_to: DisplayPoint,
|
||||||
|
) -> Result<Option<DisplayPoint>, UnboundedErr> {
|
||||||
|
try_find_boundary(map, relative_to, |left, right| {
|
||||||
|
let classifier = map
|
||||||
|
.buffer_snapshot
|
||||||
|
.char_classifier_at(relative_to.to_point(map));
|
||||||
|
self.helix_is_end(right, left, classifier)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/// Returns the beginning of the inside of the closest object before the cursor if it can easily be found. Follows helix convention;
|
||||||
|
pub fn helix_previous_start(
|
||||||
|
self,
|
||||||
|
map: &DisplaySnapshot,
|
||||||
|
relative_to: DisplayPoint,
|
||||||
|
) -> Result<Option<DisplayPoint>, UnboundedErr> {
|
||||||
|
try_find_preceding_boundary(map, relative_to, |left, right| {
|
||||||
|
let classifier = map
|
||||||
|
.buffer_snapshot
|
||||||
|
.char_classifier_at(relative_to.to_point(map));
|
||||||
|
self.helix_is_start(right, left, classifier)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/// Returns the end of the inside of the closest object before the cursor if it can easily be found. Follows helix convention;
|
||||||
|
pub fn helix_previous_end(
|
||||||
|
self,
|
||||||
|
map: &DisplaySnapshot,
|
||||||
|
relative_to: DisplayPoint,
|
||||||
|
) -> Result<Option<DisplayPoint>, UnboundedErr> {
|
||||||
|
try_find_preceding_boundary(map, relative_to, |left, right| {
|
||||||
|
let classifier = map
|
||||||
|
.buffer_snapshot
|
||||||
|
.char_classifier_at(relative_to.to_point(map));
|
||||||
|
self.helix_is_end(right, left, classifier)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn helix_is_start(
|
||||||
|
self,
|
||||||
|
right: char,
|
||||||
|
left: char,
|
||||||
|
classifier: CharClassifier,
|
||||||
|
) -> Result<bool, UnboundedErr> {
|
||||||
|
match self {
|
||||||
|
Self::Word { ignore_punctuation } => {
|
||||||
|
let classifier = classifier.ignore_punctuation(ignore_punctuation);
|
||||||
|
Ok(is_word_start(left, right, classifier))
|
||||||
|
}
|
||||||
|
Self::Subword { ignore_punctuation } => {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
Self::AngleBrackets => Ok(left == '<'),
|
||||||
|
Self::BackQuotes => Ok(left == '`'),
|
||||||
|
Self::CurlyBrackets => Ok(left == '{'),
|
||||||
|
Self::DoubleQuotes => Ok(left == '"'),
|
||||||
|
Self::Parentheses => Ok(left == '('),
|
||||||
|
Self::SquareBrackets => Ok(left == '['),
|
||||||
|
Self::VerticalBars => Ok(left == '|'),
|
||||||
|
_ => Err(UnboundedErr),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn helix_is_end(
|
||||||
|
self,
|
||||||
|
right: char,
|
||||||
|
left: char,
|
||||||
|
classifier: CharClassifier,
|
||||||
|
) -> Result<bool, UnboundedErr> {
|
||||||
|
match self {
|
||||||
|
Self::Word { ignore_punctuation } => {
|
||||||
|
let classifier = classifier.ignore_punctuation(ignore_punctuation);
|
||||||
|
Ok(is_word_end(left, right, classifier))
|
||||||
|
}
|
||||||
|
Self::Subword { ignore_punctuation } => {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
Self::AngleBrackets => Ok(right == '>'),
|
||||||
|
Self::BackQuotes => Ok(right == '`'),
|
||||||
|
Self::CurlyBrackets => Ok(right == '}'),
|
||||||
|
Self::DoubleQuotes => Ok(right == '"'),
|
||||||
|
Self::Parentheses => Ok(right == ')'),
|
||||||
|
Self::SquareBrackets => Ok(right == ']'),
|
||||||
|
Self::VerticalBars => Ok(right == '|'),
|
||||||
|
Self::Sentence => Ok(left == '.'),
|
||||||
|
_ => Err(UnboundedErr),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_find_boundary(
|
||||||
|
map: &DisplaySnapshot,
|
||||||
|
from: DisplayPoint,
|
||||||
|
mut is_boundary: impl FnMut(char, char) -> Result<bool, UnboundedErr>,
|
||||||
|
) -> Result<Option<DisplayPoint>, UnboundedErr> {
|
||||||
|
let mut offset = from.to_offset(map, Bias::Right);
|
||||||
|
let mut prev_ch = map
|
||||||
|
.buffer_snapshot
|
||||||
|
.reversed_chars_at(offset)
|
||||||
|
.next()
|
||||||
|
.unwrap_or('\0');
|
||||||
|
|
||||||
|
for ch in map.buffer_snapshot.chars_at(offset) {
|
||||||
|
if is_boundary(prev_ch, ch)? {
|
||||||
|
return Ok(Some(
|
||||||
|
map.clip_point(offset.to_display_point(map), Bias::Right),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
offset += ch.len_utf8();
|
||||||
|
prev_ch = ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_find_preceding_boundary(
|
||||||
|
map: &DisplaySnapshot,
|
||||||
|
from: DisplayPoint,
|
||||||
|
mut is_boundary: impl FnMut(char, char) -> Result<bool, UnboundedErr>,
|
||||||
|
) -> Result<Option<DisplayPoint>, UnboundedErr> {
|
||||||
|
let mut offset = from.to_offset(map, Bias::Right);
|
||||||
|
let mut prev_ch = map.buffer_snapshot.chars_at(offset).next().unwrap_or('\0');
|
||||||
|
|
||||||
|
for ch in map.buffer_snapshot.reversed_chars_at(offset) {
|
||||||
|
if is_boundary(ch, prev_ch)? {
|
||||||
|
return Ok(Some(
|
||||||
|
map.clip_point(offset.to_display_point(map), Bias::Right),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
offset -= ch.len_utf8();
|
||||||
|
prev_ch = ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_buffer_start(left: char) -> bool {
|
||||||
|
left == '\0'
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_word_start(left: char, right: char, classifier: CharClassifier) -> bool {
|
||||||
|
classifier.kind(left) != classifier.kind(right)
|
||||||
|
&& classifier.kind(right) != CharKind::Whitespace
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_word_end(left: char, right: char, classifier: CharClassifier) -> bool {
|
||||||
|
classifier.kind(left) != classifier.kind(right) && classifier.kind(left) != CharKind::Whitespace
|
||||||
|
}
|
|
@ -1,20 +1,16 @@
|
||||||
use std::ops::Range;
|
use std::{cmp::Ordering, ops::Range};
|
||||||
|
|
||||||
use editor::{
|
use editor::{
|
||||||
DisplayPoint,
|
DisplayPoint,
|
||||||
display_map::DisplaySnapshot,
|
display_map::DisplaySnapshot,
|
||||||
movement::{self, FindRange},
|
movement::{self},
|
||||||
};
|
};
|
||||||
use language::CharKind;
|
use text::Selection;
|
||||||
use text::{Bias, Selection};
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{helix::boundary::UnboundedErr, object::Object};
|
||||||
motion::right,
|
|
||||||
object::{Object, expand_to_include_whitespace},
|
|
||||||
};
|
|
||||||
|
|
||||||
impl Object {
|
impl Object {
|
||||||
/// Returns
|
/// Returns the range of the object the cursor is over.
|
||||||
/// Follows helix convention.
|
/// Follows helix convention.
|
||||||
pub fn helix_range(
|
pub fn helix_range(
|
||||||
self,
|
self,
|
||||||
|
@ -23,65 +19,204 @@ impl Object {
|
||||||
around: bool,
|
around: bool,
|
||||||
) -> Option<Range<DisplayPoint>> {
|
) -> Option<Range<DisplayPoint>> {
|
||||||
let relative_to = selection.head();
|
let relative_to = selection.head();
|
||||||
match self {
|
if let Ok(selection) = self.current_bounded_object(map, relative_to) {
|
||||||
Object::Word { ignore_punctuation } => {
|
if around {
|
||||||
if around {
|
selection.map(|s| self.surround(map, s).unwrap())
|
||||||
helix_around_word(map, relative_to, ignore_punctuation)
|
} else {
|
||||||
} else {
|
selection
|
||||||
helix_in_word(map, relative_to, ignore_punctuation)
|
}
|
||||||
}
|
} else {
|
||||||
|
let head = selection.head();
|
||||||
|
let range = self.range(map, selection, around, None)?;
|
||||||
|
|
||||||
|
if range.start > head {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(range)
|
||||||
}
|
}
|
||||||
_ => self.range(map, selection, around, None),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns a range that surrounds the word `relative_to` is in.
|
/// Returns the range of the next object the cursor is not over.
|
||||||
///
|
/// Follows helix convention.
|
||||||
/// If `relative_to` is between words, return `None`.
|
pub fn helix_next_range(
|
||||||
fn helix_in_word(
|
self,
|
||||||
map: &DisplaySnapshot,
|
map: &DisplaySnapshot,
|
||||||
relative_to: DisplayPoint,
|
selection: Selection<DisplayPoint>,
|
||||||
ignore_punctuation: bool,
|
around: bool,
|
||||||
) -> Option<Range<DisplayPoint>> {
|
) -> Option<Range<DisplayPoint>> {
|
||||||
// Use motion::right so that we consider the character under the cursor when looking for the start
|
let relative_to = selection.head();
|
||||||
let classifier = map
|
if let Ok(selection) = self.next_bounded_object(map, relative_to) {
|
||||||
.buffer_snapshot
|
if around {
|
||||||
.char_classifier_at(relative_to.to_point(map))
|
selection.map(|s| self.surround(map, s).unwrap())
|
||||||
.ignore_punctuation(ignore_punctuation);
|
} else {
|
||||||
let char = map
|
selection
|
||||||
.buffer_chars_at(relative_to.to_offset(map, Bias::Left))
|
}
|
||||||
.next()?
|
} else {
|
||||||
.0;
|
let head = selection.head();
|
||||||
|
let range = self.range(map, selection, around, None)?;
|
||||||
|
|
||||||
if classifier.kind(char) == CharKind::Whitespace {
|
if range.start > head {
|
||||||
return None;
|
Some(range)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let start = movement::find_preceding_boundary_display_point(
|
/// Returns the range of the previous object the cursor is not over.
|
||||||
map,
|
/// Follows helix convention.
|
||||||
right(map, relative_to, 1),
|
pub fn helix_previous_range(
|
||||||
movement::FindRange::SingleLine,
|
self,
|
||||||
|left, right| classifier.kind(left) != classifier.kind(right),
|
map: &DisplaySnapshot,
|
||||||
);
|
selection: Selection<DisplayPoint>,
|
||||||
|
around: bool,
|
||||||
|
) -> Option<Range<DisplayPoint>> {
|
||||||
|
let relative_to = selection.head();
|
||||||
|
if let Ok(selection) = self.previous_bounded_object(map, relative_to) {
|
||||||
|
if around {
|
||||||
|
selection.map(|s| self.surround(map, s).unwrap())
|
||||||
|
} else {
|
||||||
|
selection
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let end = movement::find_boundary(map, relative_to, FindRange::SingleLine, |left, right| {
|
/// Returns the range of the object the cursor is over if it can be found with simple boundary checking. Potentially none. Follows helix convention.
|
||||||
classifier.kind(left) != classifier.kind(right)
|
fn current_bounded_object(
|
||||||
});
|
self,
|
||||||
|
map: &DisplaySnapshot,
|
||||||
|
relative_to: DisplayPoint,
|
||||||
|
) -> Result<Option<Range<DisplayPoint>>, UnboundedErr> {
|
||||||
|
let maybe_prev_end = self.helix_previous_end(map, relative_to)?;
|
||||||
|
let Some(prev_start) = self.helix_previous_start(map, relative_to)? else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
let Some(next_end) = self.helix_next_end(map, movement::right(map, relative_to))? else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
let maybe_next_start = self.helix_next_start(map, movement::right(map, relative_to))?;
|
||||||
|
|
||||||
Some(start..end)
|
if let Some(next_start) = maybe_next_start {
|
||||||
}
|
match next_start.cmp(&next_end) {
|
||||||
|
Ordering::Less => return Ok(None),
|
||||||
|
Ordering::Equal if self.can_be_zero_width() => return Ok(None),
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(prev_end) = maybe_prev_end {
|
||||||
|
if prev_start == prev_end && self.can_be_zero_width() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
debug_assert!(prev_end <= prev_start)
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the range of the word the cursor is over and all the whitespace on one side.
|
Ok(Some(prev_start..next_end))
|
||||||
/// If there is whitespace after that is included, otherwise it's whitespace before the word if any.
|
}
|
||||||
fn helix_around_word(
|
|
||||||
map: &DisplaySnapshot,
|
|
||||||
relative_to: DisplayPoint,
|
|
||||||
ignore_punctuation: bool,
|
|
||||||
) -> Option<Range<DisplayPoint>> {
|
|
||||||
let word_range = helix_in_word(map, relative_to, ignore_punctuation)?;
|
|
||||||
|
|
||||||
Some(expand_to_include_whitespace(map, word_range, true))
|
/// Returns the range of the next object the cursor is not over if it can be found with simple boundary checking. Potentially none. Follows helix convention.
|
||||||
|
fn next_bounded_object(
|
||||||
|
self,
|
||||||
|
map: &DisplaySnapshot,
|
||||||
|
relative_to: DisplayPoint,
|
||||||
|
) -> Result<Option<Range<DisplayPoint>>, UnboundedErr> {
|
||||||
|
let Some(next_start) = self.helix_next_start(map, movement::right(map, relative_to))?
|
||||||
|
else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
let search_start = if self.can_be_zero_width() {
|
||||||
|
next_start
|
||||||
|
} else {
|
||||||
|
movement::right(map, next_start)
|
||||||
|
};
|
||||||
|
let Some(end) = self.helix_next_end(map, search_start)? else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Some(next_start..end))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the previous range of the object the cursor not is over if it can be found with simple boundary checking. Potentially none. Follows helix convention.
|
||||||
|
fn previous_bounded_object(
|
||||||
|
self,
|
||||||
|
map: &DisplaySnapshot,
|
||||||
|
relative_to: DisplayPoint,
|
||||||
|
) -> Result<Option<Range<DisplayPoint>>, UnboundedErr> {
|
||||||
|
let Some(prev_end) = self.helix_previous_end(map, relative_to)? else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
let search_start = if self.can_be_zero_width() {
|
||||||
|
prev_end
|
||||||
|
} else {
|
||||||
|
movement::left(map, prev_end)
|
||||||
|
};
|
||||||
|
let Some(start) = self.helix_previous_start(map, search_start)? else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Some(start..prev_end))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Switches from an 'mi' range to an 'ma' range. Follows helix convention.
|
||||||
|
fn surround(
|
||||||
|
self,
|
||||||
|
map: &DisplaySnapshot,
|
||||||
|
selection: Range<DisplayPoint>,
|
||||||
|
) -> Result<Range<DisplayPoint>, UnboundedErr> {
|
||||||
|
match self {
|
||||||
|
Self::Word { .. } | Self::Subword { .. } => {
|
||||||
|
let row = selection.end.row();
|
||||||
|
let line_start = DisplayPoint::new(row, 0);
|
||||||
|
let line_end = DisplayPoint::new(row, map.line_len(row));
|
||||||
|
let next_start = self
|
||||||
|
.helix_next_start(map, selection.end)
|
||||||
|
.unwrap()
|
||||||
|
.unwrap()
|
||||||
|
.min(line_end);
|
||||||
|
let prev_end = self
|
||||||
|
.helix_previous_end(map, selection.start)
|
||||||
|
.unwrap()
|
||||||
|
.unwrap()
|
||||||
|
.max(line_start);
|
||||||
|
if next_start > selection.end {
|
||||||
|
Ok(selection.start..next_start)
|
||||||
|
} else {
|
||||||
|
Ok(prev_end..selection.end)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Self::AngleBrackets
|
||||||
|
| Self::BackQuotes
|
||||||
|
| Self::CurlyBrackets
|
||||||
|
| Self::DoubleQuotes
|
||||||
|
| Self::Parentheses
|
||||||
|
| Self::SquareBrackets
|
||||||
|
| Self::VerticalBars => {
|
||||||
|
Ok(movement::left(map, selection.start)..movement::right(map, selection.end))
|
||||||
|
}
|
||||||
|
_ => Err(UnboundedErr),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn can_be_zero_width(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
Self::AngleBrackets
|
||||||
|
| Self::AnyBrackets
|
||||||
|
| Self::AnyQuotes
|
||||||
|
| Self::BackQuotes
|
||||||
|
| Self::CurlyBrackets
|
||||||
|
| Self::DoubleQuotes
|
||||||
|
| Self::EntireFile
|
||||||
|
| Self::MiniBrackets
|
||||||
|
| Self::MiniQuotes
|
||||||
|
| Self::Parentheses
|
||||||
|
| Self::Quotes
|
||||||
|
| Self::SquareBrackets
|
||||||
|
| Self::VerticalBars => true,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
|
@ -4,8 +4,9 @@ use ui::{Context, Window};
|
||||||
use crate::{Vim, object::Object};
|
use crate::{Vim, object::Object};
|
||||||
|
|
||||||
impl Vim {
|
impl Vim {
|
||||||
/// Selects the text object each cursor is over.
|
/// Selects the object each cursor is over.
|
||||||
pub fn select_object(
|
/// Follows helix convention.
|
||||||
|
pub fn select_current_object(
|
||||||
&mut self,
|
&mut self,
|
||||||
object: Object,
|
object: Object,
|
||||||
around: bool,
|
around: bool,
|
||||||
|
@ -20,8 +21,55 @@ impl Vim {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
selection.set_head(range.end, SelectionGoal::None);
|
selection.set_tail_head(range.start, range.end, SelectionGoal::None);
|
||||||
selection.start = range.start;
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Selects the next object from each cursor which the cursor is not over.
|
||||||
|
/// Follows helix convention.
|
||||||
|
pub fn select_next_object(
|
||||||
|
&mut self,
|
||||||
|
object: Object,
|
||||||
|
around: bool,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.stop_recording(cx);
|
||||||
|
self.update_editor(window, cx, |_, editor, window, cx| {
|
||||||
|
editor.change_selections(Default::default(), window, cx, |s| {
|
||||||
|
s.move_with(|map, selection| {
|
||||||
|
let Some(range) = object.helix_next_range(map, selection.clone(), around)
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
selection.set_tail_head(range.start, range.end, SelectionGoal::None);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Selects the previous object from each cursor which the cursor is not over.
|
||||||
|
/// Follows helix convention.
|
||||||
|
pub fn select_previous_object(
|
||||||
|
&mut self,
|
||||||
|
object: Object,
|
||||||
|
around: bool,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.stop_recording(cx);
|
||||||
|
self.update_editor(window, cx, |_, editor, window, cx| {
|
||||||
|
editor.change_selections(Default::default(), window, cx, |s| {
|
||||||
|
s.move_with(|map, selection| {
|
||||||
|
let Some(range) = object.helix_previous_range(map, selection.clone(), around)
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
selection.set_tail_head(range.start, range.end, SelectionGoal::None);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -479,7 +479,13 @@ impl Vim {
|
||||||
self.replace_with_register_object(object, around, window, cx)
|
self.replace_with_register_object(object, around, window, cx)
|
||||||
}
|
}
|
||||||
Some(Operator::Exchange) => self.exchange_object(object, around, window, cx),
|
Some(Operator::Exchange) => self.exchange_object(object, around, window, cx),
|
||||||
Some(Operator::HelixMatch) => self.select_object(object, around, window, cx),
|
Some(Operator::HelixMatch) => {
|
||||||
|
self.select_current_object(object, around, window, cx)
|
||||||
|
}
|
||||||
|
Some(Operator::SelectNext) => self.select_next_object(object, around, window, cx),
|
||||||
|
Some(Operator::SelectPrevious) => {
|
||||||
|
self.select_previous_object(object, around, window, cx)
|
||||||
|
}
|
||||||
_ => {
|
_ => {
|
||||||
// Can't do anything for namespace operators. Ignoring
|
// Can't do anything for namespace operators. Ignoring
|
||||||
}
|
}
|
||||||
|
|
|
@ -133,6 +133,8 @@ pub enum Operator {
|
||||||
ReplaceWithRegister,
|
ReplaceWithRegister,
|
||||||
Exchange,
|
Exchange,
|
||||||
HelixMatch,
|
HelixMatch,
|
||||||
|
SelectNext,
|
||||||
|
SelectPrevious,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Clone, Debug)]
|
#[derive(Default, Clone, Debug)]
|
||||||
|
@ -1026,6 +1028,8 @@ impl Operator {
|
||||||
Operator::ReplayRegister => "@",
|
Operator::ReplayRegister => "@",
|
||||||
Operator::ToggleComments => "gc",
|
Operator::ToggleComments => "gc",
|
||||||
Operator::HelixMatch => "helix_m",
|
Operator::HelixMatch => "helix_m",
|
||||||
|
Operator::SelectNext { .. } => "helix_]",
|
||||||
|
Operator::SelectPrevious { .. } => "helix_[",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1079,7 +1083,9 @@ impl Operator {
|
||||||
| Operator::ChangeSurrounds { target: None }
|
| Operator::ChangeSurrounds { target: None }
|
||||||
| Operator::OppositeCase
|
| Operator::OppositeCase
|
||||||
| Operator::ToggleComments
|
| Operator::ToggleComments
|
||||||
| Operator::HelixMatch => false,
|
| Operator::HelixMatch
|
||||||
|
| Operator::SelectNext { .. }
|
||||||
|
| Operator::SelectPrevious { .. } => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1103,7 +1109,9 @@ impl Operator {
|
||||||
| Operator::AddSurrounds { target: None }
|
| Operator::AddSurrounds { target: None }
|
||||||
| Operator::ChangeSurrounds { target: None }
|
| Operator::ChangeSurrounds { target: None }
|
||||||
| Operator::DeleteSurrounds
|
| Operator::DeleteSurrounds
|
||||||
| Operator::Exchange => true,
|
| Operator::Exchange
|
||||||
|
| Operator::SelectNext { .. }
|
||||||
|
| Operator::SelectPrevious { .. } => true,
|
||||||
Operator::Yank
|
Operator::Yank
|
||||||
| Operator::Object { .. }
|
| Operator::Object { .. }
|
||||||
| Operator::FindForward { .. }
|
| Operator::FindForward { .. }
|
||||||
|
|
|
@ -84,6 +84,22 @@ struct PushFindBackward {
|
||||||
multiline: bool,
|
multiline: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
|
||||||
|
#[action(namespace = vim)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
/// Selects the next object.
|
||||||
|
struct PushHelixNext {
|
||||||
|
around: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
|
||||||
|
#[action(namespace = vim)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
/// Selects the previous object.
|
||||||
|
struct PushHelixPrevious {
|
||||||
|
around: bool,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
|
#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
|
||||||
#[action(namespace = vim)]
|
#[action(namespace = vim)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
|
@ -768,6 +784,26 @@ impl Vim {
|
||||||
Vim::action(editor, cx, |vim, _: &PushHelixMatch, window, cx| {
|
Vim::action(editor, cx, |vim, _: &PushHelixMatch, window, cx| {
|
||||||
vim.push_operator(Operator::HelixMatch, window, cx)
|
vim.push_operator(Operator::HelixMatch, window, cx)
|
||||||
});
|
});
|
||||||
|
Vim::action(editor, cx, |vim, action: &PushHelixNext, window, cx| {
|
||||||
|
vim.push_operator(Operator::SelectNext, window, cx);
|
||||||
|
vim.push_operator(
|
||||||
|
Operator::Object {
|
||||||
|
around: action.around,
|
||||||
|
},
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
Vim::action(editor, cx, |vim, action: &PushHelixPrevious, window, cx| {
|
||||||
|
vim.push_operator(Operator::SelectPrevious, window, cx);
|
||||||
|
vim.push_operator(
|
||||||
|
Operator::Object {
|
||||||
|
around: action.around,
|
||||||
|
},
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
normal::register(editor, cx);
|
normal::register(editor, cx);
|
||||||
insert::register(editor, cx);
|
insert::register(editor, cx);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue