Rewrite the logic to use multiple traits instead of a lot of methods on

one object. This makes return types shorter and makes it easy to add an
extra helix object entirely. Also add logic for the sentence and
paragraph text objects. The way this is implemented would make it easy
to for example ignore brackets after a backslash.
This commit is contained in:
fantacell 2025-08-09 12:55:28 +02:00
parent ec621c60e0
commit a67a5e0d35
2 changed files with 560 additions and 384 deletions

View file

@ -1,4 +1,4 @@
use std::{error::Error, fmt::Display}; use std::ops::Range;
use editor::{ use editor::{
DisplayPoint, DisplayPoint,
@ -8,138 +8,474 @@ use editor::{
use language::{CharClassifier, CharKind}; use language::{CharClassifier, CharKind};
use text::Bias; use text::Bias;
use crate::object::Object; use crate::helix::object::HelixTextObject;
#[derive(Debug)] /// Text objects (after helix definition) that can easily be
pub struct UnboundedErr; /// found by reading a buffer and comparing two neighboring chars
impl Display for UnboundedErr { /// until a start / end is found
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { trait BoundedObject {
write!(f, "object can't be found with simple boundary checking") /// The next start since `from` (inclusive).
} fn next_start(&self, map: &DisplaySnapshot, from: DisplayPoint) -> Option<DisplayPoint>;
} /// The next end since `from` (inclusive).
impl Error for UnboundedErr {} fn next_end(&self, map: &DisplaySnapshot, from: DisplayPoint) -> Option<DisplayPoint>;
/// The previous start since `from` (inclusive).
impl Object { fn previous_start(&self, map: &DisplaySnapshot, from: DisplayPoint) -> Option<DisplayPoint>;
/// Returns the beginning of the inside of the closest object after the cursor if it can easily be found. /// The previous end since `from` (inclusive).
/// Follows helix convention. fn previous_end(&self, map: &DisplaySnapshot, from: DisplayPoint) -> Option<DisplayPoint>;
pub fn helix_next_start( /// Switches from an 'mi' range to an 'ma' range. Follows helix convention.
self, fn surround(
&self,
map: &DisplaySnapshot, map: &DisplaySnapshot,
relative_to: DisplayPoint, inner_range: Range<DisplayPoint>,
) -> Result<Option<DisplayPoint>, UnboundedErr> { ) -> Range<DisplayPoint>;
try_find_boundary(map, relative_to, |left, right| { /// Whether these objects can be inside ones of the same kind.
let classifier = map /// If so, the trait assumes they can have zero width.
.buffer_snapshot fn can_be_nested(&self) -> bool;
.char_classifier_at(relative_to.to_point(map)); /// The next end since `start` (inclusive) on the same nesting level.
self.helix_is_start(right, left, classifier) fn close_at_end(&self, start: DisplayPoint, map: &DisplaySnapshot) -> Option<DisplayPoint> {
}) if !self.can_be_nested() {
} return self.next_end(map, movement::right(map, start));
/// Returns the end of the inside of the closest object after the cursor if it can easily be found. }
/// Follows helix convention. let mut end_search_start = start;
pub fn helix_next_end( let mut start_search_start = movement::right(map, start);
self, loop {
map: &DisplaySnapshot, let next_end = self.next_end(map, end_search_start)?;
relative_to: DisplayPoint, let maybe_next_start = self.next_start(map, start_search_start);
) -> Result<Option<DisplayPoint>, UnboundedErr> { if let Some(next_start) = maybe_next_start
try_find_boundary(map, relative_to, |left, right| { && next_start <= next_end
let classifier = map {
.buffer_snapshot let closing = self.close_at_end(next_start, map)?;
.char_classifier_at(relative_to.to_point(map)); end_search_start = movement::right(map, closing);
self.helix_is_end(right, left, classifier) start_search_start = movement::right(map, closing);
}) continue;
} } else {
/// Returns the beginning of the inside of the closest object before the cursor if it can easily be found. return Some(next_end);
/// 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)
|| (is_buffer_start(left) && classifier.kind(right) != CharKind::Whitespace))
} }
Self::Subword { ignore_punctuation } => {
let classifier = classifier.ignore_punctuation(ignore_punctuation);
Ok(movement::is_subword_start(left, right, &classifier)
|| (is_buffer_start(left) && classifier.kind(right) != CharKind::Whitespace))
}
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),
} }
} }
/// The previous start since `end` (inclusive) on the same nesting level.
fn helix_is_end( fn close_at_start(&self, end: DisplayPoint, map: &DisplaySnapshot) -> Option<DisplayPoint> {
self, if !self.can_be_nested() {
right: char, return self.previous_start(map, end);
left: char, }
classifier: CharClassifier, let mut start_search_start = end;
) -> Result<bool, UnboundedErr> { let mut end_search_start = movement::left(map, end);
match self { loop {
Self::Word { ignore_punctuation } => { let prev_start = self.previous_start(map, start_search_start)?;
let classifier = classifier.ignore_punctuation(ignore_punctuation); let maybe_prev_end = self.previous_end(map, end_search_start);
Ok(is_word_end(left, right, &classifier) if let Some(prev_end) = maybe_prev_end
|| (is_buffer_end(right) && classifier.kind(left) != CharKind::Whitespace)) && prev_end >= prev_start
{
let closing = self.close_at_start(prev_end, map)?;
end_search_start = movement::left(map, closing);
start_search_start = movement::left(map, closing);
continue;
} else {
return Some(prev_start);
} }
Self::Subword { ignore_punctuation } => {
let classifier = classifier.ignore_punctuation(ignore_punctuation);
Ok(movement::is_subword_end(left, right, &classifier)
|| (is_buffer_end(right) && classifier.kind(right) != CharKind::Whitespace))
}
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),
} }
} }
} }
impl<B: BoundedObject> HelixTextObject for B {
fn range(
&self,
map: &DisplaySnapshot,
relative_to: Range<DisplayPoint>,
around: bool,
) -> Option<Range<DisplayPoint>> {
let start = self.close_at_start(relative_to.start, map)?;
let end = self.close_at_end(start, map)?;
if end < relative_to.end {
return None;
}
if around {
Some(self.surround(map, start..end))
} else {
Some(start..end)
}
}
fn next_range(
&self,
map: &DisplaySnapshot,
relative_to: Range<DisplayPoint>,
around: bool,
) -> Option<Range<DisplayPoint>> {
let start = self.next_start(map, relative_to.end)?;
let end = self.close_at_end(start, map)?;
let range = if around {
self.surround(map, start..end)
} else {
start..end
};
Some(range)
}
fn previous_range(
&self,
map: &DisplaySnapshot,
relative_to: Range<DisplayPoint>,
around: bool,
) -> Option<Range<DisplayPoint>> {
let end = self.previous_end(map, relative_to.start)?;
let start = self.close_at_start(end, map)?;
let range = if around {
self.surround(map, start..end)
} else {
start..end
};
Some(range)
}
}
/// A textobject whose boundaries can easily be found between two chars
pub enum ImmediateBoundary {
Word { ignore_punctuation: bool },
Subword { ignore_punctuation: bool },
AngleBrackets,
BackQuotes,
CurlyBrackets,
DoubleQuotes,
Parentheses,
SingleQuotes,
SquareBrackets,
VerticalBars,
}
/// A textobject whose start and end can be found from an easy-to-find
/// boundary between two chars by following a simple path from there
pub enum FuzzyBoundary {
Sentence,
Paragraph,
}
impl ImmediateBoundary {
fn is_start(&self, left: char, right: char, classifier: CharClassifier) -> bool {
match self {
Self::Word { ignore_punctuation } => {
let classifier = classifier.ignore_punctuation(*ignore_punctuation);
is_word_start(left, right, &classifier)
|| (is_buffer_start(left) && classifier.kind(right) != CharKind::Whitespace)
}
Self::Subword { ignore_punctuation } => {
let classifier = classifier.ignore_punctuation(*ignore_punctuation);
movement::is_subword_start(left, right, &classifier)
|| (is_buffer_start(left) && classifier.kind(right) != CharKind::Whitespace)
}
Self::AngleBrackets => left == '<',
Self::BackQuotes => left == '`',
Self::CurlyBrackets => left == '{',
Self::DoubleQuotes => left == '"',
Self::Parentheses => left == '(',
Self::SingleQuotes => left == '\'',
Self::SquareBrackets => left == '[',
Self::VerticalBars => left == '|',
}
}
fn is_end(&self, left: char, right: char, classifier: CharClassifier) -> bool {
match self {
Self::Word { ignore_punctuation } => {
let classifier = classifier.ignore_punctuation(*ignore_punctuation);
is_word_end(left, right, &classifier)
|| (is_buffer_end(right) && classifier.kind(left) != CharKind::Whitespace)
}
Self::Subword { ignore_punctuation } => {
let classifier = classifier.ignore_punctuation(*ignore_punctuation);
movement::is_subword_start(left, right, &classifier)
|| (is_buffer_end(right) && classifier.kind(left) != CharKind::Whitespace)
}
Self::AngleBrackets => right == '>',
Self::BackQuotes => right == '`',
Self::CurlyBrackets => right == '}',
Self::DoubleQuotes => right == '"',
Self::Parentheses => right == ')',
Self::SingleQuotes => right == '\'',
Self::SquareBrackets => right == ']',
Self::VerticalBars => right == '|',
}
}
}
impl BoundedObject for ImmediateBoundary {
fn next_start(&self, map: &DisplaySnapshot, from: DisplayPoint) -> Option<DisplayPoint> {
try_find_boundary(map, from, |left, right| {
let classifier = map
.buffer_snapshot
.char_classifier_at(from.to_offset(map, Bias::Left));
self.is_start(left, right, classifier)
})
}
fn next_end(&self, map: &DisplaySnapshot, from: DisplayPoint) -> Option<DisplayPoint> {
try_find_boundary(map, from, |left, right| {
let classifier = map
.buffer_snapshot
.char_classifier_at(from.to_offset(map, Bias::Left));
self.is_end(left, right, classifier)
})
}
fn previous_start(&self, map: &DisplaySnapshot, from: DisplayPoint) -> Option<DisplayPoint> {
try_find_preceding_boundary(map, from, |left, right| {
let classifier = map
.buffer_snapshot
.char_classifier_at(from.to_offset(map, Bias::Left));
self.is_start(left, right, classifier)
})
}
fn previous_end(&self, map: &DisplaySnapshot, from: DisplayPoint) -> Option<DisplayPoint> {
try_find_preceding_boundary(map, from, |left, right| {
let classifier = map
.buffer_snapshot
.char_classifier_at(from.to_offset(map, Bias::Left));
self.is_end(left, right, classifier)
})
}
fn surround(
&self,
map: &DisplaySnapshot,
inner_range: Range<DisplayPoint>,
) -> Range<DisplayPoint> {
match self {
Self::AngleBrackets
| Self::BackQuotes
| Self::CurlyBrackets
| Self::DoubleQuotes
| Self::Parentheses
| Self::SingleQuotes
| Self::SquareBrackets
| Self::VerticalBars => {
movement::left(map, inner_range.start)..movement::right(map, inner_range.end)
}
Self::Subword { .. } | Self::Word { .. } => {
let row = inner_range.end.row();
let line_start = DisplayPoint::new(row, 0);
let line_end = DisplayPoint::new(row, map.line_len(row));
let next_start = self.next_start(map, inner_range.end).unwrap().min(line_end);
let prev_end = self
.previous_end(map, inner_range.start)
.unwrap()
.max(line_start);
if next_start > inner_range.end {
inner_range.start..next_start
} else {
prev_end..inner_range.end
}
}
}
}
fn can_be_nested(&self) -> bool {
match self {
Self::Subword { .. } | Self::Word { .. } => false,
_ => true,
}
}
}
impl FuzzyBoundary {
/// When between two chars that form an easy-to-find identifier boundary,
/// what's the way to get to the actual start of the object, if any
fn is_near_potential_start<'a>(
&self,
left: char,
right: char,
classifier: CharClassifier,
) -> Option<Box<dyn Fn(DisplayPoint, &'a DisplaySnapshot) -> Option<DisplayPoint>>> {
if is_buffer_start(left) {
return Some(Box::new(|identifier, _| Some(identifier)));
}
match self {
Self::Paragraph => {
if left != '\n' || right != '\n' {
return None;
}
Some(Box::new(|identifier, map| {
try_find_boundary(map, identifier, |left, right| left == '\n' && right != '\n')
}))
}
Self::Sentence => {
if !is_sentence_end(left, right, &classifier) {
return None;
}
Some(Box::new(|identifier, map| {
let word = ImmediateBoundary::Word {
ignore_punctuation: false,
};
word.next_start(map, identifier)
}))
}
}
}
/// When between two chars that form an easy-to-find identifier boundary,
/// what's the way to get to the actual end of the object, if any
fn is_near_potential_end<'a>(
&self,
left: char,
right: char,
classifier: CharClassifier,
) -> Option<Box<dyn Fn(DisplayPoint, &'a DisplaySnapshot) -> Option<DisplayPoint>>> {
if is_buffer_end(right) {
return Some(Box::new(|identifier, _| Some(identifier)));
}
match self {
Self::Paragraph => {
if left != '\n' || right != '\n' {
return None;
}
Some(Box::new(|identifier, map| {
try_find_preceding_boundary(map, identifier, |left, right| {
left != '\n' && right == '\n'
})
}))
}
Self::Sentence => {
if !is_sentence_end(left, right, &classifier) {
return None;
}
Some(Box::new(|identifier, _| Some(identifier)))
}
}
}
}
impl BoundedObject for FuzzyBoundary {
fn next_start(&self, map: &DisplaySnapshot, from: DisplayPoint) -> Option<DisplayPoint> {
let mut previous_search_start = from;
while let Some((identifier, reach_start)) =
try_find_boundary_data(map, previous_search_start, |left, right, point| {
let classifier = map
.buffer_snapshot
.char_classifier_at(point.to_offset(map, Bias::Left));
self.is_near_potential_start(left, right, classifier)
.map(|reach_start| (point, reach_start))
})
{
let Some(start) = reach_start(identifier, map) else {
continue;
};
if start < from {
previous_search_start = movement::right(map, identifier);
} else {
return Some(start);
}
}
None
}
fn next_end(&self, map: &DisplaySnapshot, from: DisplayPoint) -> Option<DisplayPoint> {
let mut previous_search_start = from;
while let Some((identifier, reach_end)) =
try_find_boundary_data(map, previous_search_start, |left, right, point| {
let classifier = map
.buffer_snapshot
.char_classifier_at(point.to_offset(map, Bias::Left));
self.is_near_potential_end(left, right, classifier)
.map(|reach_end| (point, reach_end))
})
{
let Some(end) = reach_end(identifier, map) else {
continue;
};
if end < from {
previous_search_start = movement::right(map, identifier);
} else {
return Some(end);
}
}
None
}
fn previous_start(&self, map: &DisplaySnapshot, from: DisplayPoint) -> Option<DisplayPoint> {
let mut previous_search_start = from;
while let Some((identifier, reach_start)) =
try_find_preceding_boundary_data(map, previous_search_start, |left, right, point| {
let classifier = map
.buffer_snapshot
.char_classifier_at(point.to_offset(map, Bias::Left));
self.is_near_potential_start(left, right, classifier)
.map(|reach_start| (point, reach_start))
})
{
let Some(start) = reach_start(identifier, map) else {
continue;
};
if start > from {
previous_search_start = movement::left(map, identifier);
} else {
return Some(start);
}
}
None
}
fn previous_end(&self, map: &DisplaySnapshot, from: DisplayPoint) -> Option<DisplayPoint> {
let mut previous_search_start = from;
while let Some((identifier, reach_end)) =
try_find_preceding_boundary_data(map, previous_search_start, |left, right, point| {
let classifier = map
.buffer_snapshot
.char_classifier_at(point.to_offset(map, Bias::Left));
self.is_near_potential_end(left, right, classifier)
.map(|reach_end| (point, reach_end))
})
{
let Some(end) = reach_end(identifier, map) else {
continue;
};
if end > from {
previous_search_start = movement::left(map, identifier);
} else {
return Some(end);
}
}
None
}
fn surround(
&self,
map: &DisplaySnapshot,
inner_range: Range<DisplayPoint>,
) -> Range<DisplayPoint> {
let next_start = self
.next_start(map, inner_range.end)
.unwrap_or(map.max_point());
if next_start > inner_range.end {
return inner_range.start..next_start;
}
let previous_end = self
.previous_end(map, inner_range.end)
.unwrap_or(DisplayPoint::zero());
previous_end..inner_range.end
}
fn can_be_nested(&self) -> bool {
false
}
}
/// Returns the first boundary after or at `from` in text direction.
/// The start and end of the file are the chars `'\0'`.
fn try_find_boundary( fn try_find_boundary(
map: &DisplaySnapshot, map: &DisplaySnapshot,
from: DisplayPoint, from: DisplayPoint,
mut is_boundary: impl FnMut(char, char) -> Result<bool, UnboundedErr>, is_boundary: impl Fn(char, char) -> bool,
) -> Result<Option<DisplayPoint>, UnboundedErr> { ) -> Option<DisplayPoint> {
let boundary = try_find_boundary_data(map, from, |left, right, point| {
if is_boundary(left, right) {
Some(point)
} else {
None
}
})?;
Some(boundary)
}
/// Returns some information about it (of type `T`) as soon as
/// there is a boundary after or at `from` in text direction
/// The start and end of the file are the chars `'\0'`.
fn try_find_boundary_data<T>(
map: &DisplaySnapshot,
from: DisplayPoint,
boundary_information: impl Fn(char, char, DisplayPoint) -> Option<T>,
) -> Option<T> {
let mut offset = from.to_offset(map, Bias::Right); let mut offset = from.to_offset(map, Bias::Right);
let mut prev_ch = map let mut prev_ch = map
.buffer_snapshot .buffer_snapshot
@ -148,37 +484,55 @@ fn try_find_boundary(
.unwrap_or('\0'); .unwrap_or('\0');
for ch in map.buffer_snapshot.chars_at(offset).chain(['\0']) { for ch in map.buffer_snapshot.chars_at(offset).chain(['\0']) {
if is_boundary(prev_ch, ch)? { let display_point = offset.to_display_point(map);
return Ok(Some( if let Some(boundary_information) = boundary_information(prev_ch, ch, display_point) {
map.clip_point(offset.to_display_point(map), Bias::Right), return Some(boundary_information);
));
} }
offset += ch.len_utf8(); offset += ch.len_utf8();
prev_ch = ch; prev_ch = ch;
} }
Ok(None) None
} }
/// Returns the first boundary after or at `from` in text direction.
/// The start and end of the file are the chars `'\0'`.
fn try_find_preceding_boundary( fn try_find_preceding_boundary(
map: &DisplaySnapshot, map: &DisplaySnapshot,
from: DisplayPoint, from: DisplayPoint,
mut is_boundary: impl FnMut(char, char) -> Result<bool, UnboundedErr>, is_boundary: impl Fn(char, char) -> bool,
) -> Result<Option<DisplayPoint>, UnboundedErr> { ) -> Option<DisplayPoint> {
let mut offset = from.to_offset(map, Bias::Right); let boundary = try_find_preceding_boundary_data(map, from, |left, right, point| {
if is_boundary(left, right) {
Some(point)
} else {
None
}
})?;
Some(boundary)
}
/// Returns some information about it (of type `T`) as soon as
/// there is a boundary before or at `from` in opposite text direction
/// The start and end of the file are the chars `'\0'`.
fn try_find_preceding_boundary_data<T>(
map: &DisplaySnapshot,
from: DisplayPoint,
is_boundary: impl Fn(char, char, DisplayPoint) -> Option<T>,
) -> Option<T> {
let mut offset = from.to_offset(map, Bias::Left);
let mut prev_ch = map.buffer_snapshot.chars_at(offset).next().unwrap_or('\0'); let mut prev_ch = map.buffer_snapshot.chars_at(offset).next().unwrap_or('\0');
for ch in map.buffer_snapshot.reversed_chars_at(offset).chain(['\0']) { for ch in map.buffer_snapshot.reversed_chars_at(offset).chain(['\0']) {
if is_boundary(ch, prev_ch)? { let display_point = offset.to_display_point(map);
return Ok(Some( if let Some(boundary_information) = is_boundary(ch, prev_ch, display_point) {
map.clip_point(offset.to_display_point(map), Bias::Right), return Some(boundary_information);
));
} }
offset = offset.saturating_sub(ch.len_utf8()); offset = offset.saturating_sub(ch.len_utf8());
prev_ch = ch; prev_ch = ch;
} }
Ok(None) None
} }
fn is_buffer_start(left: char) -> bool { fn is_buffer_start(left: char) -> bool {
@ -197,3 +551,12 @@ fn is_word_start(left: char, right: char, classifier: &CharClassifier) -> bool {
fn is_word_end(left: char, right: char, classifier: &CharClassifier) -> bool { fn is_word_end(left: char, right: char, classifier: &CharClassifier) -> bool {
classifier.kind(left) != classifier.kind(right) && classifier.kind(left) != CharKind::Whitespace classifier.kind(left) != classifier.kind(right) && classifier.kind(left) != CharKind::Whitespace
} }
fn is_sentence_end(left: char, right: char, classifier: &CharClassifier) -> bool {
const ENDS: [char; 1] = ['.'];
if classifier.kind(right) != CharKind::Whitespace {
return false;
}
ENDS.into_iter().any(|end| left == end)
}

View file

@ -1,15 +1,38 @@
use std::{cmp::Ordering, ops::Range}; use std::ops::Range;
use editor::{ use editor::{DisplayPoint, display_map::DisplaySnapshot, movement};
DisplayPoint,
display_map::DisplaySnapshot,
movement::{self},
};
use text::Selection; use text::Selection;
use crate::{helix::boundary::UnboundedErr, object::Object}; use crate::{
helix::boundary::{FuzzyBoundary, ImmediateBoundary},
object::Object as VimObject,
};
impl Object { /// A text object from helix or an extra one
pub trait HelixTextObject {
fn range(
&self,
map: &DisplaySnapshot,
relative_to: Range<DisplayPoint>,
around: bool,
) -> Option<Range<DisplayPoint>>;
fn next_range(
&self,
map: &DisplaySnapshot,
relative_to: Range<DisplayPoint>,
around: bool,
) -> Option<Range<DisplayPoint>>;
fn previous_range(
&self,
map: &DisplaySnapshot,
relative_to: Range<DisplayPoint>,
around: bool,
) -> Option<Range<DisplayPoint>>;
}
impl VimObject {
/// Returns the range of the object the cursor is over. /// Returns the range of the object the cursor is over.
/// Follows helix convention. /// Follows helix convention.
pub fn helix_range( pub fn helix_range(
@ -18,24 +41,19 @@ impl Object {
selection: Selection<DisplayPoint>, selection: Selection<DisplayPoint>,
around: bool, around: bool,
) -> Option<Range<DisplayPoint>> { ) -> Option<Range<DisplayPoint>> {
let relative_to = cursor_start(&selection, map); let cursor = cursor_range(&selection, map);
if let Ok(selection) = self.current_bounded_object(map, relative_to) { if let Some(helix_object) = self.to_helix_object() {
if around { helix_object.range(map, cursor, around)
selection.map(|s| self.surround(map, s).unwrap())
} else {
selection
}
} else { } else {
let range = self.range(map, selection, around, None)?; let range = self.range(map, selection, around, None)?;
if range.start > relative_to { if range.start > cursor.start {
None None
} else { } else {
Some(range) Some(range)
} }
} }
} }
/// Returns the range of the next object the cursor is not over. /// Returns the range of the next object the cursor is not over.
/// Follows helix convention. /// Follows helix convention.
pub fn helix_next_range( pub fn helix_next_range(
@ -44,24 +62,10 @@ impl Object {
selection: Selection<DisplayPoint>, selection: Selection<DisplayPoint>,
around: bool, around: bool,
) -> Option<Range<DisplayPoint>> { ) -> Option<Range<DisplayPoint>> {
let relative_to = cursor_start(&selection, map); let cursor = cursor_range(&selection, map);
if let Ok(selection) = self.next_bounded_object(map, relative_to) { let helix_object = self.to_helix_object()?;
if around { helix_object.next_range(map, cursor, around)
selection.map(|s| self.surround(map, s).unwrap())
} else {
selection
}
} else {
let range = self.range(map, selection, around, None)?;
if range.start > relative_to {
Some(range)
} else {
None
}
}
} }
/// Returns the range of the previous object the cursor is not over. /// Returns the range of the previous object the cursor is not over.
/// Follows helix convention. /// Follows helix convention.
pub fn helix_previous_range( pub fn helix_previous_range(
@ -70,233 +74,42 @@ impl Object {
selection: Selection<DisplayPoint>, selection: Selection<DisplayPoint>,
around: bool, around: bool,
) -> Option<Range<DisplayPoint>> { ) -> Option<Range<DisplayPoint>> {
let relative_to = cursor_start(&selection, map); let cursor = cursor_range(&selection, map);
if let Ok(selection) = self.previous_bounded_object(map, relative_to) { let helix_object = self.to_helix_object()?;
if around { helix_object.previous_range(map, cursor, around)
selection.map(|s| self.surround(map, s).unwrap())
} else {
selection
}
} else {
None
}
} }
}
/// Returns the range of the object the cursor is over if it can be found with simple boundary checking. impl VimObject {
/// Potentially none. Follows helix convention. fn to_helix_object(self) -> Option<Box<dyn HelixTextObject>> {
fn current_bounded_object( Some(match self {
self, Self::AngleBrackets => Box::new(ImmediateBoundary::AngleBrackets),
map: &DisplaySnapshot, Self::BackQuotes => Box::new(ImmediateBoundary::BackQuotes),
relative_to: DisplayPoint, Self::CurlyBrackets => Box::new(ImmediateBoundary::CurlyBrackets),
) -> Result<Option<Range<DisplayPoint>>, UnboundedErr> { Self::DoubleQuotes => Box::new(ImmediateBoundary::DoubleQuotes),
let Some(start) = self.helix_previous_start(map, relative_to)? else { Self::Paragraph => Box::new(FuzzyBoundary::Paragraph),
return Ok(None); Self::Parentheses => Box::new(ImmediateBoundary::Parentheses),
}; Self::Quotes => Box::new(ImmediateBoundary::SingleQuotes),
let Some(end) = self.close_at_end(start, map)? else { Self::Sentence => Box::new(FuzzyBoundary::Sentence),
return Ok(None); Self::SquareBrackets => Box::new(ImmediateBoundary::SquareBrackets),
}; Self::Subword { ignore_punctuation } => {
Box::new(ImmediateBoundary::Subword { ignore_punctuation })
if end > relative_to {
return Ok(Some(start..end));
}
let Some(end) = self.helix_next_end(map, movement::right(map, relative_to))? else {
return Ok(None);
};
let Some(start) = self.close_at_start(end, map)? else {
return Ok(None);
};
if start <= relative_to {
return Ok(Some(start..end));
}
Ok(None)
}
/// 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 Some(end) = self.close_at_end(next_start, map)? 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 Some(start) = self.close_at_start(prev_end, map)? 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::VerticalBars => Box::new(ImmediateBoundary::VerticalBars),
| Self::BackQuotes Self::Word { ignore_punctuation } => {
| Self::CurlyBrackets Box::new(ImmediateBoundary::Word { ignore_punctuation })
| Self::DoubleQuotes
| Self::Parentheses
| Self::SquareBrackets
| Self::VerticalBars => {
Ok(movement::left(map, selection.start)..movement::right(map, selection.end))
} }
_ => Err(UnboundedErr), _ => return None,
} })
}
fn close_at_end(
self,
start: DisplayPoint,
map: &DisplaySnapshot,
) -> Result<Option<DisplayPoint>, UnboundedErr> {
let mut last_start = movement::right(map, start);
let mut opened = 1;
while let Some(next_end) = self.helix_next_end(map, last_start)? {
if !self.can_be_nested() {
return Ok(Some(next_end));
}
if let Some(next_start) = self.helix_next_start(map, last_start)? {
match next_start.cmp(&next_end) {
Ordering::Less => {
opened += 1;
last_start = movement::right(map, next_start);
continue;
}
Ordering::Equal if self.can_be_zero_width() => {
last_start = movement::right(map, next_start);
continue;
}
_ => (),
}
}
// When this is reached one opened object can be closed.
opened -= 1;
if opened == 0 {
return Ok(Some(next_end));
}
last_start = movement::right(map, next_end);
}
Ok(None)
}
fn close_at_start(
self,
end: DisplayPoint,
map: &DisplaySnapshot,
) -> Result<Option<DisplayPoint>, UnboundedErr> {
let mut last_end = movement::left(map, end);
let mut opened = 1;
while let Some(previous_start) = self.helix_previous_start(map, last_end)? {
if !self.can_be_nested() {
return Ok(Some(previous_start));
}
if let Some(previous_end) = self.helix_previous_end(map, last_end)? {
if previous_end > previous_start
|| previous_end == previous_start && self.can_be_zero_width()
{
opened += 1;
last_end = movement::left(map, previous_end);
continue;
}
}
// When this is reached one opened object can be closed.
opened -= 1;
if opened == 0 {
return Ok(Some(previous_start));
}
last_end = movement::left(map, previous_start);
}
Ok(None)
}
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,
}
}
const fn can_be_nested(&self) -> bool {
match self {
Self::AngleBrackets
| Self::AnyBrackets
| Self::CurlyBrackets
| Self::MiniBrackets
| Self::Parentheses
| Self::SquareBrackets
| Self::AnyQuotes
| Self::Class
| Self::Method
| Self::Tag
| Self::Argument => true,
_ => false,
}
} }
} }
/// Returns the start of the cursor of a selection, whether that is collapsed or not. /// Returns the start of the cursor of a selection, whether that is collapsed or not.
fn cursor_start(selection: &Selection<DisplayPoint>, map: &DisplaySnapshot) -> DisplayPoint { fn cursor_range(selection: &Selection<DisplayPoint>, map: &DisplaySnapshot) -> Range<DisplayPoint> {
if selection.is_empty() | selection.reversed { if selection.is_empty() | selection.reversed {
selection.head() selection.head()..movement::right(map, selection.head())
} else { } else {
movement::left(map, selection.head()) movement::left(map, selection.head())..selection.head()
} }
} }