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:
parent
ec621c60e0
commit
a67a5e0d35
2 changed files with 560 additions and 384 deletions
|
@ -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.
|
let mut end_search_start = start;
|
||||||
/// Follows helix convention.
|
let mut start_search_start = movement::right(map, start);
|
||||||
pub fn helix_next_end(
|
loop {
|
||||||
self,
|
let next_end = self.next_end(map, end_search_start)?;
|
||||||
map: &DisplaySnapshot,
|
let maybe_next_start = self.next_start(map, start_search_start);
|
||||||
relative_to: DisplayPoint,
|
if let Some(next_start) = maybe_next_start
|
||||||
) -> Result<Option<DisplayPoint>, UnboundedErr> {
|
&& next_start <= next_end
|
||||||
try_find_boundary(map, relative_to, |left, right| {
|
{
|
||||||
let classifier = map
|
let closing = self.close_at_end(next_start, map)?;
|
||||||
.buffer_snapshot
|
end_search_start = movement::right(map, closing);
|
||||||
.char_classifier_at(relative_to.to_point(map));
|
start_search_start = movement::right(map, closing);
|
||||||
self.helix_is_end(right, left, classifier)
|
continue;
|
||||||
})
|
} else {
|
||||||
}
|
return Some(next_end);
|
||||||
/// 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))
|
|
||||||
}
|
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
Self::Subword { ignore_punctuation } => {
|
/// The previous start since `end` (inclusive) on the same nesting level.
|
||||||
let classifier = classifier.ignore_punctuation(ignore_punctuation);
|
fn close_at_start(&self, end: DisplayPoint, map: &DisplaySnapshot) -> Option<DisplayPoint> {
|
||||||
Ok(movement::is_subword_end(left, right, &classifier)
|
if !self.can_be_nested() {
|
||||||
|| (is_buffer_end(right) && classifier.kind(right) != CharKind::Whitespace))
|
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::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)
|
||||||
|
}
|
||||||
|
|
|
@ -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));
|
|
||||||
}
|
}
|
||||||
|
Self::VerticalBars => Box::new(ImmediateBoundary::VerticalBars),
|
||||||
let Some(end) = self.helix_next_end(map, movement::right(map, relative_to))? else {
|
Self::Word { ignore_punctuation } => {
|
||||||
return Ok(None);
|
Box::new(ImmediateBoundary::Word { ignore_punctuation })
|
||||||
};
|
|
||||||
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::BackQuotes
|
|
||||||
| Self::CurlyBrackets
|
|
||||||
| Self::DoubleQuotes
|
|
||||||
| Self::Parentheses
|
|
||||||
| Self::SquareBrackets
|
|
||||||
| Self::VerticalBars => {
|
|
||||||
Ok(movement::left(map, selection.start)..movement::right(map, selection.end))
|
|
||||||
}
|
|
||||||
_ => 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.
|
/// 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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue