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::{
DisplayPoint,
@ -8,138 +8,474 @@ use editor::{
use language::{CharClassifier, CharKind};
use text::Bias;
use crate::object::Object;
use crate::helix::object::HelixTextObject;
#[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,
/// Text objects (after helix definition) that can easily be
/// found by reading a buffer and comparing two neighboring chars
/// until a start / end is found
trait BoundedObject {
/// The next start since `from` (inclusive).
fn next_start(&self, map: &DisplaySnapshot, from: DisplayPoint) -> Option<DisplayPoint>;
/// The next end since `from` (inclusive).
fn next_end(&self, map: &DisplaySnapshot, from: DisplayPoint) -> Option<DisplayPoint>;
/// The previous start since `from` (inclusive).
fn previous_start(&self, map: &DisplaySnapshot, from: DisplayPoint) -> Option<DisplayPoint>;
/// The previous end since `from` (inclusive).
fn previous_end(&self, map: &DisplaySnapshot, from: DisplayPoint) -> Option<DisplayPoint>;
/// Switches from an 'mi' range to an 'ma' range. Follows helix convention.
fn surround(
&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)
|| (is_buffer_start(left) && classifier.kind(right) != CharKind::Whitespace))
inner_range: Range<DisplayPoint>,
) -> Range<DisplayPoint>;
/// Whether these objects can be inside ones of the same kind.
/// If so, the trait assumes they can have zero width.
fn can_be_nested(&self) -> bool;
/// The next end since `start` (inclusive) on the same nesting level.
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));
}
let mut end_search_start = start;
let mut start_search_start = movement::right(map, start);
loop {
let next_end = self.next_end(map, end_search_start)?;
let maybe_next_start = self.next_start(map, start_search_start);
if let Some(next_start) = maybe_next_start
&& next_start <= next_end
{
let closing = self.close_at_end(next_start, map)?;
end_search_start = movement::right(map, closing);
start_search_start = movement::right(map, closing);
continue;
} else {
return Some(next_end);
}
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),
}
}
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)
|| (is_buffer_end(right) && classifier.kind(left) != CharKind::Whitespace))
/// The previous start since `end` (inclusive) on the same nesting level.
fn close_at_start(&self, end: DisplayPoint, map: &DisplaySnapshot) -> Option<DisplayPoint> {
if !self.can_be_nested() {
return self.previous_start(map, end);
}
let mut start_search_start = end;
let mut end_search_start = movement::left(map, end);
loop {
let prev_start = self.previous_start(map, start_search_start)?;
let maybe_prev_end = self.previous_end(map, end_search_start);
if let Some(prev_end) = maybe_prev_end
&& 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(
map: &DisplaySnapshot,
from: DisplayPoint,
mut is_boundary: impl FnMut(char, char) -> Result<bool, UnboundedErr>,
) -> Result<Option<DisplayPoint>, UnboundedErr> {
is_boundary: impl Fn(char, char) -> bool,
) -> 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 prev_ch = map
.buffer_snapshot
@ -148,37 +484,55 @@ fn try_find_boundary(
.unwrap_or('\0');
for ch in map.buffer_snapshot.chars_at(offset).chain(['\0']) {
if is_boundary(prev_ch, ch)? {
return Ok(Some(
map.clip_point(offset.to_display_point(map), Bias::Right),
));
let display_point = offset.to_display_point(map);
if let Some(boundary_information) = boundary_information(prev_ch, ch, display_point) {
return Some(boundary_information);
}
offset += ch.len_utf8();
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(
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);
is_boundary: impl Fn(char, char) -> bool,
) -> Option<DisplayPoint> {
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');
for ch in map.buffer_snapshot.reversed_chars_at(offset).chain(['\0']) {
if is_boundary(ch, prev_ch)? {
return Ok(Some(
map.clip_point(offset.to_display_point(map), Bias::Right),
));
let display_point = offset.to_display_point(map);
if let Some(boundary_information) = is_boundary(ch, prev_ch, display_point) {
return Some(boundary_information);
}
offset = offset.saturating_sub(ch.len_utf8());
prev_ch = ch;
}
Ok(None)
None
}
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 {
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::{
DisplayPoint,
display_map::DisplaySnapshot,
movement::{self},
};
use editor::{DisplayPoint, display_map::DisplaySnapshot, movement};
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.
/// Follows helix convention.
pub fn helix_range(
@ -18,24 +41,19 @@ impl Object {
selection: Selection<DisplayPoint>,
around: bool,
) -> Option<Range<DisplayPoint>> {
let relative_to = cursor_start(&selection, map);
if let Ok(selection) = self.current_bounded_object(map, relative_to) {
if around {
selection.map(|s| self.surround(map, s).unwrap())
} else {
selection
}
let cursor = cursor_range(&selection, map);
if let Some(helix_object) = self.to_helix_object() {
helix_object.range(map, cursor, around)
} else {
let range = self.range(map, selection, around, None)?;
if range.start > relative_to {
if range.start > cursor.start {
None
} else {
Some(range)
}
}
}
/// Returns the range of the next object the cursor is not over.
/// Follows helix convention.
pub fn helix_next_range(
@ -44,24 +62,10 @@ impl Object {
selection: Selection<DisplayPoint>,
around: bool,
) -> Option<Range<DisplayPoint>> {
let relative_to = cursor_start(&selection, map);
if let Ok(selection) = self.next_bounded_object(map, relative_to) {
if 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
}
}
let cursor = cursor_range(&selection, map);
let helix_object = self.to_helix_object()?;
helix_object.next_range(map, cursor, around)
}
/// Returns the range of the previous object the cursor is not over.
/// Follows helix convention.
pub fn helix_previous_range(
@ -70,233 +74,42 @@ impl Object {
selection: Selection<DisplayPoint>,
around: bool,
) -> Option<Range<DisplayPoint>> {
let relative_to = cursor_start(&selection, map);
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 cursor = cursor_range(&selection, map);
let helix_object = self.to_helix_object()?;
helix_object.previous_range(map, cursor, around)
}
}
/// Returns the range of the object the cursor is over if it can be found with simple boundary checking.
/// Potentially none. Follows helix convention.
fn current_bounded_object(
self,
map: &DisplaySnapshot,
relative_to: DisplayPoint,
) -> Result<Option<Range<DisplayPoint>>, UnboundedErr> {
let Some(start) = self.helix_previous_start(map, relative_to)? else {
return Ok(None);
};
let Some(end) = self.close_at_end(start, map)? else {
return Ok(None);
};
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)
}
impl VimObject {
fn to_helix_object(self) -> Option<Box<dyn HelixTextObject>> {
Some(match self {
Self::AngleBrackets => Box::new(ImmediateBoundary::AngleBrackets),
Self::BackQuotes => Box::new(ImmediateBoundary::BackQuotes),
Self::CurlyBrackets => Box::new(ImmediateBoundary::CurlyBrackets),
Self::DoubleQuotes => Box::new(ImmediateBoundary::DoubleQuotes),
Self::Paragraph => Box::new(FuzzyBoundary::Paragraph),
Self::Parentheses => Box::new(ImmediateBoundary::Parentheses),
Self::Quotes => Box::new(ImmediateBoundary::SingleQuotes),
Self::Sentence => Box::new(FuzzyBoundary::Sentence),
Self::SquareBrackets => Box::new(ImmediateBoundary::SquareBrackets),
Self::Subword { ignore_punctuation } => {
Box::new(ImmediateBoundary::Subword { ignore_punctuation })
}
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))
Self::VerticalBars => Box::new(ImmediateBoundary::VerticalBars),
Self::Word { ignore_punctuation } => {
Box::new(ImmediateBoundary::Word { ignore_punctuation })
}
_ => Err(UnboundedErr),
}
}
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,
}
_ => return None,
})
}
}
/// 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 {
selection.head()
selection.head()..movement::right(map, selection.head())
} else {
movement::left(map, selection.head())
movement::left(map, selection.head())..selection.head()
}
}