Merge 2f9a6d9b37
into bd4e943597
This commit is contained in:
commit
66b6abf538
12 changed files with 1200 additions and 69 deletions
|
@ -32,34 +32,6 @@
|
||||||
"(": "vim::SentenceBackward",
|
"(": "vim::SentenceBackward",
|
||||||
")": "vim::SentenceForward",
|
")": "vim::SentenceForward",
|
||||||
"|": "vim::GoToColumn",
|
"|": "vim::GoToColumn",
|
||||||
"] ]": "vim::NextSectionStart",
|
|
||||||
"] [": "vim::NextSectionEnd",
|
|
||||||
"[ [": "vim::PreviousSectionStart",
|
|
||||||
"[ ]": "vim::PreviousSectionEnd",
|
|
||||||
"] m": "vim::NextMethodStart",
|
|
||||||
"] shift-m": "vim::NextMethodEnd",
|
|
||||||
"[ m": "vim::PreviousMethodStart",
|
|
||||||
"[ shift-m": "vim::PreviousMethodEnd",
|
|
||||||
"[ *": "vim::PreviousComment",
|
|
||||||
"[ /": "vim::PreviousComment",
|
|
||||||
"] *": "vim::NextComment",
|
|
||||||
"] /": "vim::NextComment",
|
|
||||||
"[ -": "vim::PreviousLesserIndent",
|
|
||||||
"[ +": "vim::PreviousGreaterIndent",
|
|
||||||
"[ =": "vim::PreviousSameIndent",
|
|
||||||
"] -": "vim::NextLesserIndent",
|
|
||||||
"] +": "vim::NextGreaterIndent",
|
|
||||||
"] =": "vim::NextSameIndent",
|
|
||||||
"] b": "pane::ActivateNextItem",
|
|
||||||
"[ b": "pane::ActivatePreviousItem",
|
|
||||||
"] shift-b": "pane::ActivateLastItem",
|
|
||||||
"[ shift-b": ["pane::ActivateItem", 0],
|
|
||||||
"] space": "vim::InsertEmptyLineBelow",
|
|
||||||
"[ space": "vim::InsertEmptyLineAbove",
|
|
||||||
"[ e": "editor::MoveLineUp",
|
|
||||||
"] e": "editor::MoveLineDown",
|
|
||||||
"[ f": "workspace::FollowNextCollaborator",
|
|
||||||
"] f": "workspace::FollowNextCollaborator",
|
|
||||||
|
|
||||||
// Word motions
|
// Word motions
|
||||||
"w": "vim::NextWordStart",
|
"w": "vim::NextWordStart",
|
||||||
|
@ -83,10 +55,6 @@
|
||||||
"n": "vim::MoveToNextMatch",
|
"n": "vim::MoveToNextMatch",
|
||||||
"shift-n": "vim::MoveToPreviousMatch",
|
"shift-n": "vim::MoveToPreviousMatch",
|
||||||
"%": "vim::Matching",
|
"%": "vim::Matching",
|
||||||
"] }": ["vim::UnmatchedForward", { "char": "}" }],
|
|
||||||
"[ {": ["vim::UnmatchedBackward", { "char": "{" }],
|
|
||||||
"] )": ["vim::UnmatchedForward", { "char": ")" }],
|
|
||||||
"[ (": ["vim::UnmatchedBackward", { "char": "(" }],
|
|
||||||
"f": ["vim::PushFindForward", { "before": false, "multiline": false }],
|
"f": ["vim::PushFindForward", { "before": false, "multiline": false }],
|
||||||
"t": ["vim::PushFindForward", { "before": true, "multiline": false }],
|
"t": ["vim::PushFindForward", { "before": true, "multiline": false }],
|
||||||
"shift-f": ["vim::PushFindBackward", { "after": false, "multiline": false }],
|
"shift-f": ["vim::PushFindBackward", { "after": false, "multiline": false }],
|
||||||
|
@ -219,6 +187,46 @@
|
||||||
".": "vim::Repeat"
|
".": "vim::Repeat"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"context": "vim_mode == normal || vim_mode == visual",
|
||||||
|
"bindings": {
|
||||||
|
"] ]": "vim::NextSectionStart",
|
||||||
|
"] [": "vim::NextSectionEnd",
|
||||||
|
"[ [": "vim::PreviousSectionStart",
|
||||||
|
"[ ]": "vim::PreviousSectionEnd",
|
||||||
|
"] m": "vim::NextMethodStart",
|
||||||
|
"] shift-m": "vim::NextMethodEnd",
|
||||||
|
"[ m": "vim::PreviousMethodStart",
|
||||||
|
"[ shift-m": "vim::PreviousMethodEnd",
|
||||||
|
"[ *": "vim::PreviousComment",
|
||||||
|
"[ /": "vim::PreviousComment",
|
||||||
|
"] *": "vim::NextComment",
|
||||||
|
"] /": "vim::NextComment",
|
||||||
|
"[ -": "vim::PreviousLesserIndent",
|
||||||
|
"[ +": "vim::PreviousGreaterIndent",
|
||||||
|
"[ =": "vim::PreviousSameIndent",
|
||||||
|
"] -": "vim::NextLesserIndent",
|
||||||
|
"] +": "vim::NextGreaterIndent",
|
||||||
|
"] =": "vim::NextSameIndent",
|
||||||
|
"] b": "pane::ActivateNextItem",
|
||||||
|
"[ b": "pane::ActivatePreviousItem",
|
||||||
|
"] shift-b": "pane::ActivateLastItem",
|
||||||
|
"[ shift-b": ["pane::ActivateItem", 0],
|
||||||
|
"] space": "vim::InsertEmptyLineBelow",
|
||||||
|
"[ space": "vim::InsertEmptyLineAbove",
|
||||||
|
"[ e": "editor::MoveLineUp",
|
||||||
|
"] e": "editor::MoveLineDown",
|
||||||
|
"[ f": "workspace::FollowNextCollaborator",
|
||||||
|
"] f": "workspace::FollowNextCollaborator",
|
||||||
|
"] }": ["vim::UnmatchedForward", { "char": "}" }],
|
||||||
|
"[ {": ["vim::UnmatchedBackward", { "char": "{" }],
|
||||||
|
"] )": ["vim::UnmatchedForward", { "char": ")" }],
|
||||||
|
"[ (": ["vim::UnmatchedBackward", { "char": "(" }],
|
||||||
|
// tree-sitter related commands
|
||||||
|
"[ x": "vim::SelectLargerSyntaxNode",
|
||||||
|
"] x": "vim::SelectSmallerSyntaxNode"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"context": "vim_mode == normal",
|
"context": "vim_mode == normal",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
|
@ -249,9 +257,6 @@
|
||||||
"g w": "vim::PushRewrap",
|
"g w": "vim::PushRewrap",
|
||||||
"g q": "vim::PushRewrap",
|
"g q": "vim::PushRewrap",
|
||||||
"insert": "vim::InsertBefore",
|
"insert": "vim::InsertBefore",
|
||||||
// tree-sitter related commands
|
|
||||||
"[ x": "vim::SelectLargerSyntaxNode",
|
|
||||||
"] x": "vim::SelectSmallerSyntaxNode",
|
|
||||||
"] d": "editor::GoToDiagnostic",
|
"] d": "editor::GoToDiagnostic",
|
||||||
"[ d": "editor::GoToPreviousDiagnostic",
|
"[ d": "editor::GoToPreviousDiagnostic",
|
||||||
"] c": "editor::GoToHunk",
|
"] c": "editor::GoToHunk",
|
||||||
|
@ -317,10 +322,7 @@
|
||||||
"g w": "vim::Rewrap",
|
"g w": "vim::Rewrap",
|
||||||
"g ?": "vim::ConvertToRot13",
|
"g ?": "vim::ConvertToRot13",
|
||||||
// "g ?": "vim::ConvertToRot47",
|
// "g ?": "vim::ConvertToRot47",
|
||||||
"\"": "vim::PushRegister",
|
"\"": "vim::PushRegister"
|
||||||
// tree-sitter related commands
|
|
||||||
"[ x": "editor::SelectLargerSyntaxNode",
|
|
||||||
"] x": "editor::SelectSmallerSyntaxNode"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -388,6 +390,9 @@
|
||||||
"ctrl-[": "editor::Cancel",
|
"ctrl-[": "editor::Cancel",
|
||||||
";": "vim::HelixCollapseSelection",
|
";": "vim::HelixCollapseSelection",
|
||||||
":": "command_palette::Toggle",
|
":": "command_palette::Toggle",
|
||||||
|
"m": "vim::PushHelixMatch",
|
||||||
|
"]": ["vim::PushHelixNext", { "around": true }],
|
||||||
|
"[": ["vim::PushHelixPrevious", { "around": true }],
|
||||||
"left": "vim::WrappingLeft",
|
"left": "vim::WrappingLeft",
|
||||||
"right": "vim::WrappingRight",
|
"right": "vim::WrappingRight",
|
||||||
"h": "vim::WrappingLeft",
|
"h": "vim::WrappingLeft",
|
||||||
|
@ -410,13 +415,6 @@
|
||||||
"insert": "vim::InsertBefore",
|
"insert": "vim::InsertBefore",
|
||||||
"alt-.": "vim::RepeatFind",
|
"alt-.": "vim::RepeatFind",
|
||||||
"alt-s": ["editor::SplitSelectionIntoLines", { "keep_selections": true }],
|
"alt-s": ["editor::SplitSelectionIntoLines", { "keep_selections": true }],
|
||||||
// tree-sitter related commands
|
|
||||||
"[ x": "editor::SelectLargerSyntaxNode",
|
|
||||||
"] x": "editor::SelectSmallerSyntaxNode",
|
|
||||||
"] d": "editor::GoToDiagnostic",
|
|
||||||
"[ d": "editor::GoToPreviousDiagnostic",
|
|
||||||
"] c": "editor::GoToHunk",
|
|
||||||
"[ c": "editor::GoToPreviousHunk",
|
|
||||||
// Goto mode
|
// Goto mode
|
||||||
"g n": "pane::ActivateNextItem",
|
"g n": "pane::ActivateNextItem",
|
||||||
"g p": "pane::ActivatePreviousItem",
|
"g p": "pane::ActivatePreviousItem",
|
||||||
|
@ -460,9 +458,6 @@
|
||||||
"space c": "editor::ToggleComments",
|
"space c": "editor::ToggleComments",
|
||||||
"space y": "editor::Copy",
|
"space y": "editor::Copy",
|
||||||
"space p": "editor::Paste",
|
"space p": "editor::Paste",
|
||||||
// Match mode
|
|
||||||
"m m": "vim::Matching",
|
|
||||||
"m i w": ["workspace::SendKeystrokes", "v i w"],
|
|
||||||
"shift-u": "editor::Redo",
|
"shift-u": "editor::Redo",
|
||||||
"ctrl-c": "editor::ToggleComments",
|
"ctrl-c": "editor::ToggleComments",
|
||||||
"d": "vim::HelixDelete",
|
"d": "vim::HelixDelete",
|
||||||
|
@ -531,7 +526,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "vim_operator == a || vim_operator == i || vim_operator == cs",
|
"context": "vim_operator == a || vim_operator == i || vim_operator == cs || vim_operator == helix_next || vim_operator == helix_previous",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"w": "vim::Word",
|
"w": "vim::Word",
|
||||||
"shift-w": ["vim::Word", { "ignore_punctuation": true }],
|
"shift-w": ["vim::Word", { "ignore_punctuation": true }],
|
||||||
|
@ -568,6 +563,48 @@
|
||||||
"e": "vim::EntireFile"
|
"e": "vim::EntireFile"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"context": "vim_operator == helix_m",
|
||||||
|
"bindings": {
|
||||||
|
"m": "vim::Matching"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"context": "vim_operator == helix_next",
|
||||||
|
"bindings": {
|
||||||
|
"z": "vim::NextSectionStart",
|
||||||
|
"shift-z": "vim::NextSectionEnd",
|
||||||
|
"*": "vim::NextComment",
|
||||||
|
"/": "vim::NextComment",
|
||||||
|
"-": "vim::NextLesserIndent",
|
||||||
|
"+": "vim::NextGreaterIndent",
|
||||||
|
"=": "vim::NextSameIndent",
|
||||||
|
"b": "pane::ActivateNextItem",
|
||||||
|
"shift-b": "pane::ActivateLastItem",
|
||||||
|
"x": "editor::SelectSmallerSyntaxNode",
|
||||||
|
"d": "editor::GoToDiagnostic",
|
||||||
|
"c": "editor::GoToHunk",
|
||||||
|
"space": "vim::InsertEmptyLineBelow"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"context": "vim_operator == helix_previous",
|
||||||
|
"bindings": {
|
||||||
|
"z": "vim::PreviousSectionStart",
|
||||||
|
"shift-z": "vim::PreviousSectionEnd",
|
||||||
|
"*": "vim::PreviousComment",
|
||||||
|
"/": "vim::PreviousComment",
|
||||||
|
"-": "vim::PreviousLesserIndent",
|
||||||
|
"+": "vim::PreviousGreaterIndent",
|
||||||
|
"=": "vim::PreviousSameIndent",
|
||||||
|
"b": "pane::ActivatePreviousItem",
|
||||||
|
"shift-b": ["pane::ActivateItem", 0],
|
||||||
|
"x": "editor::SelectLargerSyntaxNode",
|
||||||
|
"d": "editor::GoToPreviousDiagnostic",
|
||||||
|
"c": "editor::GoToPreviousHunk",
|
||||||
|
"space": "vim::InsertEmptyLineAbove"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"context": "vim_operator == c",
|
"context": "vim_operator == c",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
use super::{Bias, DisplayPoint, DisplaySnapshot, SelectionGoal, ToDisplayPoint};
|
use super::{Bias, DisplayPoint, DisplaySnapshot, SelectionGoal, ToDisplayPoint};
|
||||||
use crate::{DisplayRow, EditorStyle, ToOffset, ToPoint, scroll::ScrollAnchor};
|
use crate::{DisplayRow, EditorStyle, ToOffset, ToPoint, scroll::ScrollAnchor};
|
||||||
use gpui::{Pixels, WindowTextSystem};
|
use gpui::{Pixels, WindowTextSystem};
|
||||||
use language::Point;
|
use language::{CharClassifier, Point};
|
||||||
use multi_buffer::{MultiBufferRow, MultiBufferSnapshot};
|
use multi_buffer::{MultiBufferRow, MultiBufferSnapshot};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use workspace::searchable::Direction;
|
use workspace::searchable::Direction;
|
||||||
|
@ -303,15 +303,18 @@ pub fn previous_subword_start(map: &DisplaySnapshot, point: DisplayPoint) -> Dis
|
||||||
let classifier = map.buffer_snapshot.char_classifier_at(raw_point);
|
let classifier = map.buffer_snapshot.char_classifier_at(raw_point);
|
||||||
|
|
||||||
find_preceding_boundary_display_point(map, point, FindRange::MultiLine, |left, right| {
|
find_preceding_boundary_display_point(map, point, FindRange::MultiLine, |left, right| {
|
||||||
let is_word_start =
|
is_subword_start(left, right, &classifier) || left == '\n'
|
||||||
classifier.kind(left) != classifier.kind(right) && !right.is_whitespace();
|
|
||||||
let is_subword_start = classifier.is_word('-') && left == '-' && right != '-'
|
|
||||||
|| left == '_' && right != '_'
|
|
||||||
|| left.is_lowercase() && right.is_uppercase();
|
|
||||||
is_word_start || is_subword_start || left == '\n'
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_subword_start(left: char, right: char, classifier: &CharClassifier) -> bool {
|
||||||
|
let is_word_start = classifier.kind(left) != classifier.kind(right) && !right.is_whitespace();
|
||||||
|
let is_subword_start = classifier.is_word('-') && left == '-' && right != '-'
|
||||||
|
|| left == '_' && right != '_'
|
||||||
|
|| left.is_lowercase() && right.is_uppercase();
|
||||||
|
is_word_start || is_subword_start
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns a position of the next word boundary, where a word character is defined as either
|
/// Returns a position of the next word boundary, where a word character is defined as either
|
||||||
/// uppercase letter, lowercase letter, '_' character or language-specific word character (like '-' in CSS).
|
/// uppercase letter, lowercase letter, '_' character or language-specific word character (like '-' in CSS).
|
||||||
pub fn next_word_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
|
pub fn next_word_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
|
||||||
|
@ -361,15 +364,19 @@ pub fn next_subword_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPo
|
||||||
let classifier = map.buffer_snapshot.char_classifier_at(raw_point);
|
let classifier = map.buffer_snapshot.char_classifier_at(raw_point);
|
||||||
|
|
||||||
find_boundary(map, point, FindRange::MultiLine, |left, right| {
|
find_boundary(map, point, FindRange::MultiLine, |left, right| {
|
||||||
let is_word_end =
|
is_subword_end(left, right, &classifier) || right == '\n'
|
||||||
(classifier.kind(left) != classifier.kind(right)) && !classifier.is_whitespace(left);
|
|
||||||
let is_subword_end = classifier.is_word('-') && left != '-' && right == '-'
|
|
||||||
|| left != '_' && right == '_'
|
|
||||||
|| left.is_lowercase() && right.is_uppercase();
|
|
||||||
is_word_end || is_subword_end || right == '\n'
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_subword_end(left: char, right: char, classifier: &CharClassifier) -> bool {
|
||||||
|
let is_word_end =
|
||||||
|
(classifier.kind(left) != classifier.kind(right)) && !classifier.is_whitespace(left);
|
||||||
|
let is_subword_end = classifier.is_word('-') && left != '-' && right == '-'
|
||||||
|
|| left != '_' && right == '_'
|
||||||
|
|| left.is_lowercase() && right.is_uppercase();
|
||||||
|
is_word_end || is_subword_end
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns a position of the start of the current paragraph, where a paragraph
|
/// Returns a position of the start of the current paragraph, where a paragraph
|
||||||
/// is defined as a run of non-blank lines.
|
/// is defined as a run of non-blank lines.
|
||||||
pub fn start_of_paragraph(
|
pub fn start_of_paragraph(
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
|
mod boundary;
|
||||||
|
mod object;
|
||||||
|
mod select;
|
||||||
|
|
||||||
use editor::display_map::DisplaySnapshot;
|
use editor::display_map::DisplaySnapshot;
|
||||||
use editor::{DisplayPoint, Editor, SelectionEffects, ToOffset, ToPoint, movement};
|
use editor::{DisplayPoint, Editor, SelectionEffects, ToOffset, ToPoint, movement};
|
||||||
use gpui::{Action, actions};
|
use gpui::{Action, actions};
|
||||||
|
|
744
crates/vim/src/helix/boundary.rs
Normal file
744
crates/vim/src/helix/boundary.rs
Normal file
|
@ -0,0 +1,744 @@
|
||||||
|
use std::{cmp::Ordering, ops::Range};
|
||||||
|
|
||||||
|
use editor::{
|
||||||
|
DisplayPoint,
|
||||||
|
display_map::{DisplaySnapshot, ToDisplayPoint},
|
||||||
|
movement,
|
||||||
|
};
|
||||||
|
use language::{CharClassifier, CharKind};
|
||||||
|
use text::Bias;
|
||||||
|
|
||||||
|
use crate::helix::object::HelixTextObject;
|
||||||
|
|
||||||
|
/// 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).
|
||||||
|
/// If outer is true it is the start of "a" object (m a) rather than "inner" object (m i).
|
||||||
|
fn next_start(
|
||||||
|
&self,
|
||||||
|
map: &DisplaySnapshot,
|
||||||
|
from: DisplayPoint,
|
||||||
|
outer: bool,
|
||||||
|
) -> Option<DisplayPoint>;
|
||||||
|
/// The next end since `from` (inclusive).
|
||||||
|
/// If outer is true it is the end of "a" object (m a) rather than "inner" object (m i).
|
||||||
|
fn next_end(
|
||||||
|
&self,
|
||||||
|
map: &DisplaySnapshot,
|
||||||
|
from: DisplayPoint,
|
||||||
|
outer: bool,
|
||||||
|
) -> Option<DisplayPoint>;
|
||||||
|
/// The previous start since `from` (inclusive).
|
||||||
|
/// If outer is true it is the start of "a" object (m a) rather than "inner" object (m i).
|
||||||
|
fn previous_start(
|
||||||
|
&self,
|
||||||
|
map: &DisplaySnapshot,
|
||||||
|
from: DisplayPoint,
|
||||||
|
outer: bool,
|
||||||
|
) -> Option<DisplayPoint>;
|
||||||
|
/// The previous end since `from` (inclusive).
|
||||||
|
/// If outer is true it is the end of "a" object (m a) rather than "inner" object (m i).
|
||||||
|
fn previous_end(
|
||||||
|
&self,
|
||||||
|
map: &DisplaySnapshot,
|
||||||
|
from: DisplayPoint,
|
||||||
|
outer: bool,
|
||||||
|
) -> Option<DisplayPoint>;
|
||||||
|
|
||||||
|
/// Whether the range inside or outside the object can have be zero characters wide.
|
||||||
|
/// If so, the trait assumes that these ranges can't be directly adjacent to each other.
|
||||||
|
fn can_be_zero_width(&self, around: bool) -> bool;
|
||||||
|
/// Whether the "ma" can exceed the "mi" range on both sides at the same time
|
||||||
|
fn surround_on_both_sides(&self) -> bool;
|
||||||
|
|
||||||
|
/// Switches from an "mi" range to an "ma" one.
|
||||||
|
/// Assumes the inner range is valid.
|
||||||
|
fn around(
|
||||||
|
&self,
|
||||||
|
map: &DisplaySnapshot,
|
||||||
|
inner_range: Range<DisplayPoint>,
|
||||||
|
) -> Range<DisplayPoint> {
|
||||||
|
if self.surround_on_both_sides() {
|
||||||
|
let start = self
|
||||||
|
.previous_start(map, inner_range.start, true)
|
||||||
|
.unwrap_or(inner_range.start);
|
||||||
|
let end = self
|
||||||
|
.next_end(map, inner_range.end, true)
|
||||||
|
.unwrap_or(inner_range.end);
|
||||||
|
|
||||||
|
return start..end;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut start = inner_range.start;
|
||||||
|
let end = self
|
||||||
|
.next_end(map, inner_range.end, true)
|
||||||
|
.unwrap_or(inner_range.end);
|
||||||
|
if end == inner_range.end {
|
||||||
|
start = self
|
||||||
|
.previous_start(map, inner_range.start, true)
|
||||||
|
.unwrap_or(inner_range.start)
|
||||||
|
}
|
||||||
|
|
||||||
|
start..end
|
||||||
|
}
|
||||||
|
/// Switches from an "ma" range to an "mi" one.
|
||||||
|
/// Assumes the inner range is valid.
|
||||||
|
fn inside(
|
||||||
|
&self,
|
||||||
|
map: &DisplaySnapshot,
|
||||||
|
outer_range: Range<DisplayPoint>,
|
||||||
|
) -> Range<DisplayPoint> {
|
||||||
|
let inner_start = self
|
||||||
|
.next_start(map, outer_range.start, false)
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
log::warn!("The motion might not have found the text object correctly");
|
||||||
|
outer_range.start
|
||||||
|
});
|
||||||
|
let inner_end = self
|
||||||
|
.previous_end(map, outer_range.end, false)
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
log::warn!("The motion might not have found the text object correctly");
|
||||||
|
outer_range.end
|
||||||
|
});
|
||||||
|
inner_start..inner_end
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The next end since `start` (inclusive) on the same nesting level.
|
||||||
|
fn close_at_end(
|
||||||
|
&self,
|
||||||
|
start: DisplayPoint,
|
||||||
|
map: &DisplaySnapshot,
|
||||||
|
outer: bool,
|
||||||
|
) -> Option<DisplayPoint> {
|
||||||
|
let mut end_search_start = if self.can_be_zero_width(outer) {
|
||||||
|
start
|
||||||
|
} else {
|
||||||
|
movement::right(map, start)
|
||||||
|
};
|
||||||
|
let mut start_search_start = movement::right(map, start);
|
||||||
|
loop {
|
||||||
|
let next_end = self.next_end(map, end_search_start, outer)?;
|
||||||
|
let maybe_next_start = self.next_start(map, start_search_start, outer);
|
||||||
|
if let Some(next_start) = maybe_next_start
|
||||||
|
&& ((next_start < next_end)
|
||||||
|
|| (next_start == next_end && self.can_be_zero_width(outer)))
|
||||||
|
{
|
||||||
|
let closing = self.close_at_end(next_start, map, outer)?;
|
||||||
|
end_search_start = movement::right(map, closing);
|
||||||
|
start_search_start = if self.can_be_zero_width(outer) {
|
||||||
|
movement::right(map, closing)
|
||||||
|
} else {
|
||||||
|
closing
|
||||||
|
};
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
return Some(next_end);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// The previous start since `end` (inclusive) on the same nesting level.
|
||||||
|
fn close_at_start(
|
||||||
|
&self,
|
||||||
|
end: DisplayPoint,
|
||||||
|
map: &DisplaySnapshot,
|
||||||
|
outer: bool,
|
||||||
|
) -> Option<DisplayPoint> {
|
||||||
|
let mut start_search_start = if self.can_be_zero_width(outer) {
|
||||||
|
end
|
||||||
|
} else {
|
||||||
|
movement::left(map, end)
|
||||||
|
};
|
||||||
|
let mut end_search_start = movement::left(map, end);
|
||||||
|
loop {
|
||||||
|
let prev_start = self.previous_start(map, start_search_start, outer)?;
|
||||||
|
let maybe_prev_end = self.previous_end(map, end_search_start, outer);
|
||||||
|
if let Some(prev_end) = maybe_prev_end
|
||||||
|
&& ((prev_end > prev_start)
|
||||||
|
|| (prev_end == prev_start && self.can_be_zero_width(outer)))
|
||||||
|
{
|
||||||
|
let closing = self.close_at_start(prev_end, map, outer)?;
|
||||||
|
end_search_start = if self.can_be_zero_width(outer) {
|
||||||
|
movement::left(map, closing)
|
||||||
|
} else {
|
||||||
|
closing
|
||||||
|
};
|
||||||
|
start_search_start = movement::left(map, closing);
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
return Some(prev_start);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<B: BoundedObject> HelixTextObject for B {
|
||||||
|
fn range(
|
||||||
|
&self,
|
||||||
|
map: &DisplaySnapshot,
|
||||||
|
relative_to: Range<DisplayPoint>,
|
||||||
|
around: bool,
|
||||||
|
) -> Option<Range<DisplayPoint>> {
|
||||||
|
let search_start = if self.can_be_zero_width(true) {
|
||||||
|
relative_to.start
|
||||||
|
} else {
|
||||||
|
// If the objects can be directly next to each other an object start at the
|
||||||
|
// cursor (relative_to) start would not count for close_at_start, so the search
|
||||||
|
// needs to start one character to the left.
|
||||||
|
movement::right(map, relative_to.start)
|
||||||
|
};
|
||||||
|
let min_start = self.close_at_start(search_start, map, self.surround_on_both_sides())?;
|
||||||
|
let max_end = self.close_at_end(min_start, map, self.surround_on_both_sides())?;
|
||||||
|
|
||||||
|
if max_end < relative_to.end {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if around && !self.surround_on_both_sides() {
|
||||||
|
// max_end is not yet the outer end
|
||||||
|
Some(self.around(map, min_start..max_end))
|
||||||
|
} else if !around && self.surround_on_both_sides() {
|
||||||
|
// max_end is the outer end, but the final result should have the inner end
|
||||||
|
Some(self.inside(map, min_start..max_end))
|
||||||
|
} else {
|
||||||
|
Some(min_start..max_end)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next_range(
|
||||||
|
&self,
|
||||||
|
map: &DisplaySnapshot,
|
||||||
|
relative_to: Range<DisplayPoint>,
|
||||||
|
around: bool,
|
||||||
|
) -> Option<Range<DisplayPoint>> {
|
||||||
|
let min_start = self.next_start(map, relative_to.end, self.surround_on_both_sides())?;
|
||||||
|
let max_end = self.close_at_end(min_start, map, self.surround_on_both_sides())?;
|
||||||
|
|
||||||
|
if around && !self.surround_on_both_sides() {
|
||||||
|
// max_end is not yet the outer end
|
||||||
|
Some(self.around(map, min_start..max_end))
|
||||||
|
} else if !around && self.surround_on_both_sides() {
|
||||||
|
// max_end is the outer end, but the final result should have the inner end
|
||||||
|
Some(self.inside(map, min_start..max_end))
|
||||||
|
} else {
|
||||||
|
Some(min_start..max_end)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn previous_range(
|
||||||
|
&self,
|
||||||
|
map: &DisplaySnapshot,
|
||||||
|
relative_to: Range<DisplayPoint>,
|
||||||
|
around: bool,
|
||||||
|
) -> Option<Range<DisplayPoint>> {
|
||||||
|
let max_end = self.previous_end(map, relative_to.start, self.surround_on_both_sides())?;
|
||||||
|
let min_start = self.close_at_start(max_end, map, self.surround_on_both_sides())?;
|
||||||
|
|
||||||
|
if around && !self.surround_on_both_sides() {
|
||||||
|
// max_end is not yet the outer end
|
||||||
|
Some(self.around(map, min_start..max_end))
|
||||||
|
} else if !around && self.surround_on_both_sides() {
|
||||||
|
// max_end is the outer end, but the final result should have the inner end
|
||||||
|
Some(self.inside(map, min_start..max_end))
|
||||||
|
} else {
|
||||||
|
Some(min_start..max_end)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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_inner_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_inner_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 == '|',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn is_outer_start(&self, left: char, right: char, classifier: CharClassifier) -> bool {
|
||||||
|
match self {
|
||||||
|
word @ Self::Word { .. } => word.is_inner_end(left, right, classifier) || left == '\n',
|
||||||
|
subword @ Self::Subword { .. } => {
|
||||||
|
subword.is_inner_end(left, right, classifier) || left == '\n'
|
||||||
|
}
|
||||||
|
Self::AngleBrackets => right == '<',
|
||||||
|
Self::BackQuotes => right == '`',
|
||||||
|
Self::CurlyBrackets => right == '{',
|
||||||
|
Self::DoubleQuotes => right == '"',
|
||||||
|
Self::Parentheses => right == '(',
|
||||||
|
Self::SingleQuotes => right == '\'',
|
||||||
|
Self::SquareBrackets => right == '[',
|
||||||
|
Self::VerticalBars => right == '|',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn is_outer_end(&self, left: char, right: char, classifier: CharClassifier) -> bool {
|
||||||
|
match self {
|
||||||
|
word @ Self::Word { .. } => {
|
||||||
|
word.is_inner_start(left, right, classifier) || right == '\n'
|
||||||
|
}
|
||||||
|
subword @ Self::Subword { .. } => {
|
||||||
|
subword.is_inner_start(left, right, classifier) || right == '\n'
|
||||||
|
}
|
||||||
|
Self::AngleBrackets => left == '>',
|
||||||
|
Self::BackQuotes => left == '`',
|
||||||
|
Self::CurlyBrackets => left == '}',
|
||||||
|
Self::DoubleQuotes => left == '"',
|
||||||
|
Self::Parentheses => left == ')',
|
||||||
|
Self::SingleQuotes => left == '\'',
|
||||||
|
Self::SquareBrackets => left == ']',
|
||||||
|
Self::VerticalBars => left == '|',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BoundedObject for ImmediateBoundary {
|
||||||
|
fn next_start(
|
||||||
|
&self,
|
||||||
|
map: &DisplaySnapshot,
|
||||||
|
from: DisplayPoint,
|
||||||
|
outer: bool,
|
||||||
|
) -> Option<DisplayPoint> {
|
||||||
|
try_find_boundary(map, from, |left, right| {
|
||||||
|
let classifier = map
|
||||||
|
.buffer_snapshot
|
||||||
|
.char_classifier_at(from.to_offset(map, Bias::Left));
|
||||||
|
if outer {
|
||||||
|
self.is_outer_start(left, right, classifier)
|
||||||
|
} else {
|
||||||
|
self.is_inner_start(left, right, classifier)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
fn next_end(
|
||||||
|
&self,
|
||||||
|
map: &DisplaySnapshot,
|
||||||
|
from: DisplayPoint,
|
||||||
|
outer: bool,
|
||||||
|
) -> Option<DisplayPoint> {
|
||||||
|
try_find_boundary(map, from, |left, right| {
|
||||||
|
let classifier = map
|
||||||
|
.buffer_snapshot
|
||||||
|
.char_classifier_at(from.to_offset(map, Bias::Left));
|
||||||
|
if outer {
|
||||||
|
self.is_outer_end(left, right, classifier)
|
||||||
|
} else {
|
||||||
|
self.is_inner_end(left, right, classifier)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
fn previous_start(
|
||||||
|
&self,
|
||||||
|
map: &DisplaySnapshot,
|
||||||
|
from: DisplayPoint,
|
||||||
|
outer: bool,
|
||||||
|
) -> Option<DisplayPoint> {
|
||||||
|
try_find_preceding_boundary(map, from, |left, right| {
|
||||||
|
let classifier = map
|
||||||
|
.buffer_snapshot
|
||||||
|
.char_classifier_at(from.to_offset(map, Bias::Left));
|
||||||
|
if outer {
|
||||||
|
self.is_outer_start(left, right, classifier)
|
||||||
|
} else {
|
||||||
|
self.is_inner_start(left, right, classifier)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
fn previous_end(
|
||||||
|
&self,
|
||||||
|
map: &DisplaySnapshot,
|
||||||
|
from: DisplayPoint,
|
||||||
|
outer: bool,
|
||||||
|
) -> Option<DisplayPoint> {
|
||||||
|
try_find_preceding_boundary(map, from, |left, right| {
|
||||||
|
let classifier = map
|
||||||
|
.buffer_snapshot
|
||||||
|
.char_classifier_at(from.to_offset(map, Bias::Left));
|
||||||
|
if outer {
|
||||||
|
self.is_outer_end(left, right, classifier)
|
||||||
|
} else {
|
||||||
|
self.is_inner_end(left, right, classifier)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
fn can_be_zero_width(&self, around: bool) -> bool {
|
||||||
|
match self {
|
||||||
|
Self::Subword { .. } | Self::Word { .. } => false,
|
||||||
|
_ => !around,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn surround_on_both_sides(&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_inner_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 let Some(find_paragraph_start) =
|
||||||
|
Self::Paragraph.is_near_potential_inner_start(left, right, classifier)
|
||||||
|
{
|
||||||
|
return Some(find_paragraph_start);
|
||||||
|
} else 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, false)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// 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_inner_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 let Some(find_paragraph_end) =
|
||||||
|
Self::Paragraph.is_near_potential_inner_end(left, right, classifier)
|
||||||
|
{
|
||||||
|
return Some(find_paragraph_end);
|
||||||
|
} else if !is_sentence_end(left, right, classifier) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(Box::new(|identifier, _| Some(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_outer_start<'a>(
|
||||||
|
&self,
|
||||||
|
left: char,
|
||||||
|
right: char,
|
||||||
|
classifier: &CharClassifier,
|
||||||
|
) -> Option<Box<dyn Fn(DisplayPoint, &'a DisplaySnapshot) -> Option<DisplayPoint>>> {
|
||||||
|
match self {
|
||||||
|
paragraph @ Self::Paragraph => {
|
||||||
|
paragraph.is_near_potential_inner_end(left, right, classifier)
|
||||||
|
}
|
||||||
|
sentence @ Self::Sentence => {
|
||||||
|
sentence.is_near_potential_inner_end(left, right, classifier)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// 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_outer_end<'a>(
|
||||||
|
&self,
|
||||||
|
left: char,
|
||||||
|
right: char,
|
||||||
|
classifier: &CharClassifier,
|
||||||
|
) -> Option<Box<dyn Fn(DisplayPoint, &'a DisplaySnapshot) -> Option<DisplayPoint>>> {
|
||||||
|
match self {
|
||||||
|
paragraph @ Self::Paragraph => {
|
||||||
|
paragraph.is_near_potential_inner_start(left, right, classifier)
|
||||||
|
}
|
||||||
|
sentence @ Self::Sentence => {
|
||||||
|
sentence.is_near_potential_inner_start(left, right, classifier)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The boundary can be on the other side of `from` than the identifier, so the search needs to go both ways.
|
||||||
|
// Also, the distance (and direction) between identifier and boundary could vary, so a few ones need to be
|
||||||
|
// compared, even if one boundary was already found on the right side of `from`.
|
||||||
|
fn to_boundary(
|
||||||
|
&self,
|
||||||
|
map: &DisplaySnapshot,
|
||||||
|
from: DisplayPoint,
|
||||||
|
outer: bool,
|
||||||
|
backward: bool,
|
||||||
|
boundary_kind: Boundary,
|
||||||
|
) -> Option<DisplayPoint> {
|
||||||
|
let generate_boundary_data = |left, right, point: DisplayPoint| {
|
||||||
|
let classifier = map
|
||||||
|
.buffer_snapshot
|
||||||
|
.char_classifier_at(point.to_offset(map, Bias::Left));
|
||||||
|
let reach_boundary = if outer && boundary_kind == Boundary::Start {
|
||||||
|
self.is_near_potential_outer_start(left, right, &classifier)
|
||||||
|
} else if !outer && boundary_kind == Boundary::Start {
|
||||||
|
self.is_near_potential_inner_start(left, right, &classifier)
|
||||||
|
} else if outer && boundary_kind == Boundary::End {
|
||||||
|
self.is_near_potential_outer_end(left, right, &classifier)
|
||||||
|
} else {
|
||||||
|
self.is_near_potential_inner_end(left, right, &classifier)
|
||||||
|
};
|
||||||
|
|
||||||
|
reach_boundary.map(|reach_start| (point, reach_start))
|
||||||
|
};
|
||||||
|
|
||||||
|
let forwards = try_find_boundary_data(map, from, generate_boundary_data);
|
||||||
|
let backwards = try_find_preceding_boundary_data(map, from, generate_boundary_data);
|
||||||
|
let boundaries = [forwards, backwards]
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.filter_map(|(identifier, reach_boundary)| reach_boundary(identifier, map))
|
||||||
|
.filter(|boundary| match boundary.cmp(&from) {
|
||||||
|
Ordering::Equal => true,
|
||||||
|
Ordering::Less => backward,
|
||||||
|
Ordering::Greater => !backward,
|
||||||
|
});
|
||||||
|
if backward {
|
||||||
|
boundaries.max()
|
||||||
|
} else {
|
||||||
|
boundaries.min()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq)]
|
||||||
|
enum Boundary {
|
||||||
|
Start,
|
||||||
|
End,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BoundedObject for FuzzyBoundary {
|
||||||
|
fn next_start(
|
||||||
|
&self,
|
||||||
|
map: &DisplaySnapshot,
|
||||||
|
from: DisplayPoint,
|
||||||
|
outer: bool,
|
||||||
|
) -> Option<DisplayPoint> {
|
||||||
|
self.to_boundary(map, from, outer, false, Boundary::Start)
|
||||||
|
}
|
||||||
|
fn next_end(
|
||||||
|
&self,
|
||||||
|
map: &DisplaySnapshot,
|
||||||
|
from: DisplayPoint,
|
||||||
|
outer: bool,
|
||||||
|
) -> Option<DisplayPoint> {
|
||||||
|
self.to_boundary(map, from, outer, false, Boundary::End)
|
||||||
|
}
|
||||||
|
fn previous_start(
|
||||||
|
&self,
|
||||||
|
map: &DisplaySnapshot,
|
||||||
|
from: DisplayPoint,
|
||||||
|
outer: bool,
|
||||||
|
) -> Option<DisplayPoint> {
|
||||||
|
self.to_boundary(map, from, outer, true, Boundary::Start)
|
||||||
|
}
|
||||||
|
fn previous_end(
|
||||||
|
&self,
|
||||||
|
map: &DisplaySnapshot,
|
||||||
|
from: DisplayPoint,
|
||||||
|
outer: bool,
|
||||||
|
) -> Option<DisplayPoint> {
|
||||||
|
self.to_boundary(map, from, outer, true, Boundary::End)
|
||||||
|
}
|
||||||
|
fn can_be_zero_width(&self, _: bool) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
fn surround_on_both_sides(&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,
|
||||||
|
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
|
||||||
|
.reversed_chars_at(offset)
|
||||||
|
.next()
|
||||||
|
.unwrap_or('\0');
|
||||||
|
|
||||||
|
for ch in map.buffer_snapshot.chars_at(offset).chain(['\0']) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
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']) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_buffer_start(left: char) -> bool {
|
||||||
|
left == '\0'
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_buffer_end(right: char) -> bool {
|
||||||
|
right == '\0'
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_word_start(left: char, right: char, classifier: &CharClassifier) -> bool {
|
||||||
|
classifier.kind(left) != classifier.kind(right)
|
||||||
|
&& classifier.kind(right) != CharKind::Whitespace
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_word_end(left: char, right: char, classifier: &CharClassifier) -> bool {
|
||||||
|
classifier.kind(left) != classifier.kind(right) && classifier.kind(left) != CharKind::Whitespace
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
182
crates/vim/src/helix/object.rs
Normal file
182
crates/vim/src/helix/object.rs
Normal file
|
@ -0,0 +1,182 @@
|
||||||
|
use std::{
|
||||||
|
error::Error,
|
||||||
|
fmt::{self, Display},
|
||||||
|
ops::Range,
|
||||||
|
};
|
||||||
|
|
||||||
|
use editor::{DisplayPoint, display_map::DisplaySnapshot, movement};
|
||||||
|
use text::Selection;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
helix::boundary::{FuzzyBoundary, ImmediateBoundary},
|
||||||
|
object::Object as VimObject,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// 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(
|
||||||
|
self,
|
||||||
|
map: &DisplaySnapshot,
|
||||||
|
selection: Selection<DisplayPoint>,
|
||||||
|
around: bool,
|
||||||
|
) -> Result<Option<Range<DisplayPoint>>, VimToHelixError> {
|
||||||
|
let cursor = cursor_range(&selection, map);
|
||||||
|
if let Some(helix_object) = self.to_helix_object() {
|
||||||
|
Ok(helix_object.range(map, cursor, around))
|
||||||
|
} else {
|
||||||
|
Err(VimToHelixError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Returns the range of the next object the cursor is not over.
|
||||||
|
/// Follows helix convention.
|
||||||
|
pub fn helix_next_range(
|
||||||
|
self,
|
||||||
|
map: &DisplaySnapshot,
|
||||||
|
selection: Selection<DisplayPoint>,
|
||||||
|
around: bool,
|
||||||
|
) -> Result<Option<Range<DisplayPoint>>, VimToHelixError> {
|
||||||
|
let cursor = cursor_range(&selection, map);
|
||||||
|
if let Some(helix_object) = self.to_helix_object() {
|
||||||
|
Ok(helix_object.next_range(map, cursor, around))
|
||||||
|
} else {
|
||||||
|
Err(VimToHelixError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Returns the range of the previous object the cursor is not over.
|
||||||
|
/// Follows helix convention.
|
||||||
|
pub fn helix_previous_range(
|
||||||
|
self,
|
||||||
|
map: &DisplaySnapshot,
|
||||||
|
selection: Selection<DisplayPoint>,
|
||||||
|
around: bool,
|
||||||
|
) -> Result<Option<Range<DisplayPoint>>, VimToHelixError> {
|
||||||
|
let cursor = cursor_range(&selection, map);
|
||||||
|
if let Some(helix_object) = self.to_helix_object() {
|
||||||
|
Ok(helix_object.previous_range(map, cursor, around))
|
||||||
|
} else {
|
||||||
|
Err(VimToHelixError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct VimToHelixError;
|
||||||
|
impl Display for VimToHelixError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"Not all vim text objects have an implemented helix equivalent"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Error for VimToHelixError {}
|
||||||
|
|
||||||
|
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::VerticalBars => Box::new(ImmediateBoundary::VerticalBars),
|
||||||
|
Self::Word { ignore_punctuation } => {
|
||||||
|
Box::new(ImmediateBoundary::Word { ignore_punctuation })
|
||||||
|
}
|
||||||
|
_ => return None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the start of the cursor of a selection, whether that is collapsed or not.
|
||||||
|
pub(crate) fn cursor_range(
|
||||||
|
selection: &Selection<DisplayPoint>,
|
||||||
|
map: &DisplaySnapshot,
|
||||||
|
) -> Range<DisplayPoint> {
|
||||||
|
if selection.is_empty() | selection.reversed {
|
||||||
|
selection.head()..movement::right(map, selection.head())
|
||||||
|
} else {
|
||||||
|
movement::left(map, selection.head())..selection.head()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use db::indoc;
|
||||||
|
|
||||||
|
use crate::{state::Mode, test::VimTestContext};
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_select_word_object(cx: &mut gpui::TestAppContext) {
|
||||||
|
let mut cx = VimTestContext::new(cx, true).await;
|
||||||
|
let start = indoc! {"
|
||||||
|
The quick brˇowˇnˇ
|
||||||
|
fox «ˇjumps» ov«er
|
||||||
|
the laˇ»zy dogˇ
|
||||||
|
|
||||||
|
"
|
||||||
|
};
|
||||||
|
|
||||||
|
cx.set_state(start, Mode::HelixNormal);
|
||||||
|
|
||||||
|
cx.simulate_keystrokes("m i w");
|
||||||
|
|
||||||
|
cx.assert_state(
|
||||||
|
indoc! {"
|
||||||
|
The quick «brownˇ»
|
||||||
|
fox «jumpsˇ» over
|
||||||
|
the «lazyˇ» dogˇ
|
||||||
|
|
||||||
|
"
|
||||||
|
},
|
||||||
|
Mode::HelixNormal,
|
||||||
|
);
|
||||||
|
|
||||||
|
cx.set_state(start, Mode::HelixNormal);
|
||||||
|
|
||||||
|
cx.simulate_keystrokes("m a w");
|
||||||
|
|
||||||
|
cx.assert_state(
|
||||||
|
indoc! {"
|
||||||
|
The quick« brownˇ»
|
||||||
|
fox «jumps ˇ»over
|
||||||
|
the «lazy ˇ»dogˇ
|
||||||
|
|
||||||
|
"
|
||||||
|
},
|
||||||
|
Mode::HelixNormal,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
84
crates/vim/src/helix/select.rs
Normal file
84
crates/vim/src/helix/select.rs
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
use text::SelectionGoal;
|
||||||
|
use ui::{Context, Window};
|
||||||
|
|
||||||
|
use crate::{Vim, helix::object::cursor_range, object::Object};
|
||||||
|
|
||||||
|
impl Vim {
|
||||||
|
/// Selects the object each cursor is over.
|
||||||
|
/// Follows helix convention.
|
||||||
|
pub fn select_current_object(
|
||||||
|
&mut self,
|
||||||
|
object: Object,
|
||||||
|
around: bool,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.stop_recording(cx);
|
||||||
|
self.update_editor(cx, |_, editor, cx| {
|
||||||
|
editor.change_selections(Default::default(), window, cx, |s| {
|
||||||
|
s.move_with(|map, selection| {
|
||||||
|
let Some(range) = object
|
||||||
|
.helix_range(map, selection.clone(), around)
|
||||||
|
.unwrap_or({
|
||||||
|
let vim_range = object.range(map, selection.clone(), around, None);
|
||||||
|
vim_range.filter(|r| r.start <= cursor_range(selection, map).start)
|
||||||
|
})
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
selection.set_head_tail(range.end, range.start, SelectionGoal::None);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Selects the next object from each cursor which the cursor is not over.
|
||||||
|
/// Follows helix convention.
|
||||||
|
pub fn select_next_object(
|
||||||
|
&mut self,
|
||||||
|
object: Object,
|
||||||
|
around: bool,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.stop_recording(cx);
|
||||||
|
self.update_editor(cx, |_, editor, cx| {
|
||||||
|
editor.change_selections(Default::default(), window, cx, |s| {
|
||||||
|
s.move_with(|map, selection| {
|
||||||
|
let Ok(Some(range)) = object.helix_next_range(map, selection.clone(), around)
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
selection.set_head_tail(range.end, range.start, SelectionGoal::None);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Selects the previous object from each cursor which the cursor is not over.
|
||||||
|
/// Follows helix convention.
|
||||||
|
pub fn select_previous_object(
|
||||||
|
&mut self,
|
||||||
|
object: Object,
|
||||||
|
around: bool,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.stop_recording(cx);
|
||||||
|
self.update_editor(cx, |_, editor, cx| {
|
||||||
|
editor.change_selections(Default::default(), window, cx, |s| {
|
||||||
|
s.move_with(|map, selection| {
|
||||||
|
let Ok(Some(range)) =
|
||||||
|
object.helix_previous_range(map, selection.clone(), around)
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
selection.set_head_tail(range.start, range.end, SelectionGoal::None);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -495,10 +495,19 @@ impl Vim {
|
||||||
self.replace_with_register_object(object, around, window, cx)
|
self.replace_with_register_object(object, around, window, cx)
|
||||||
}
|
}
|
||||||
Some(Operator::Exchange) => self.exchange_object(object, around, window, cx),
|
Some(Operator::Exchange) => self.exchange_object(object, around, window, cx),
|
||||||
|
Some(Operator::HelixMatch) => {
|
||||||
|
self.select_current_object(object, around, window, cx)
|
||||||
|
}
|
||||||
_ => {
|
_ => {
|
||||||
// Can't do anything for namespace operators. Ignoring
|
// Can't do anything for namespace operators. Ignoring
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
Some(Operator::HelixNext { around }) => {
|
||||||
|
self.select_next_object(object, around, window, cx);
|
||||||
|
}
|
||||||
|
Some(Operator::HelixPrevious { around }) => {
|
||||||
|
self.select_previous_object(object, around, window, cx);
|
||||||
|
}
|
||||||
Some(Operator::DeleteSurrounds) => {
|
Some(Operator::DeleteSurrounds) => {
|
||||||
waiting_operator = Some(Operator::DeleteSurrounds);
|
waiting_operator = Some(Operator::DeleteSurrounds);
|
||||||
}
|
}
|
||||||
|
|
|
@ -397,11 +397,11 @@ impl Vim {
|
||||||
let count = Self::take_count(cx);
|
let count = Self::take_count(cx);
|
||||||
|
|
||||||
match self.mode {
|
match self.mode {
|
||||||
Mode::Normal => self.normal_object(object, count, window, cx),
|
Mode::Normal | Mode::HelixNormal => self.normal_object(object, count, window, cx),
|
||||||
Mode::Visual | Mode::VisualLine | Mode::VisualBlock => {
|
Mode::Visual | Mode::VisualLine | Mode::VisualBlock => {
|
||||||
self.visual_object(object, count, window, cx)
|
self.visual_object(object, count, window, cx)
|
||||||
}
|
}
|
||||||
Mode::Insert | Mode::Replace | Mode::HelixNormal => {
|
Mode::Insert | Mode::Replace => {
|
||||||
// Shouldn't execute a text object in insert mode. Ignoring
|
// Shouldn't execute a text object in insert mode. Ignoring
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1364,7 +1364,7 @@ fn is_sentence_end(map: &DisplaySnapshot, offset: usize) -> bool {
|
||||||
|
|
||||||
/// Expands the passed range to include whitespace on one side or the other in a line. Attempts to add the
|
/// Expands the passed range to include whitespace on one side or the other in a line. Attempts to add the
|
||||||
/// whitespace to the end first and falls back to the start if there was none.
|
/// whitespace to the end first and falls back to the start if there was none.
|
||||||
fn expand_to_include_whitespace(
|
pub fn expand_to_include_whitespace(
|
||||||
map: &DisplaySnapshot,
|
map: &DisplaySnapshot,
|
||||||
range: Range<DisplayPoint>,
|
range: Range<DisplayPoint>,
|
||||||
stop_at_newline: bool,
|
stop_at_newline: bool,
|
||||||
|
|
|
@ -134,6 +134,13 @@ pub enum Operator {
|
||||||
ToggleComments,
|
ToggleComments,
|
||||||
ReplaceWithRegister,
|
ReplaceWithRegister,
|
||||||
Exchange,
|
Exchange,
|
||||||
|
HelixMatch,
|
||||||
|
HelixNext {
|
||||||
|
around: bool,
|
||||||
|
},
|
||||||
|
HelixPrevious {
|
||||||
|
around: bool,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Clone, Debug)]
|
#[derive(Default, Clone, Debug)]
|
||||||
|
@ -1020,6 +1027,9 @@ impl Operator {
|
||||||
Operator::RecordRegister => "q",
|
Operator::RecordRegister => "q",
|
||||||
Operator::ReplayRegister => "@",
|
Operator::ReplayRegister => "@",
|
||||||
Operator::ToggleComments => "gc",
|
Operator::ToggleComments => "gc",
|
||||||
|
Operator::HelixMatch => "helix_m",
|
||||||
|
Operator::HelixNext { .. } => "helix_next",
|
||||||
|
Operator::HelixPrevious { .. } => "helix_previous",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1041,6 +1051,9 @@ impl Operator {
|
||||||
} => format!("^V{}", make_visible(prefix)),
|
} => format!("^V{}", make_visible(prefix)),
|
||||||
Operator::AutoIndent => "=".to_string(),
|
Operator::AutoIndent => "=".to_string(),
|
||||||
Operator::ShellCommand => "=".to_string(),
|
Operator::ShellCommand => "=".to_string(),
|
||||||
|
Operator::HelixMatch => "m".to_string(),
|
||||||
|
Operator::HelixNext { .. } => "]".to_string(),
|
||||||
|
Operator::HelixPrevious { .. } => "[".to_string(),
|
||||||
_ => self.id().to_string(),
|
_ => self.id().to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1079,7 +1092,10 @@ impl Operator {
|
||||||
| Operator::Object { .. }
|
| Operator::Object { .. }
|
||||||
| Operator::ChangeSurrounds { target: None }
|
| Operator::ChangeSurrounds { target: None }
|
||||||
| Operator::OppositeCase
|
| Operator::OppositeCase
|
||||||
| Operator::ToggleComments => false,
|
| Operator::ToggleComments
|
||||||
|
| Operator::HelixMatch
|
||||||
|
| Operator::HelixNext { .. }
|
||||||
|
| Operator::HelixPrevious { .. } => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1103,7 +1119,9 @@ impl Operator {
|
||||||
| Operator::AddSurrounds { target: None }
|
| Operator::AddSurrounds { target: None }
|
||||||
| Operator::ChangeSurrounds { target: None }
|
| Operator::ChangeSurrounds { target: None }
|
||||||
| Operator::DeleteSurrounds
|
| Operator::DeleteSurrounds
|
||||||
| Operator::Exchange => true,
|
| Operator::Exchange
|
||||||
|
| Operator::HelixNext { .. }
|
||||||
|
| Operator::HelixPrevious { .. } => true,
|
||||||
Operator::Yank
|
Operator::Yank
|
||||||
| Operator::Object { .. }
|
| Operator::Object { .. }
|
||||||
| Operator::FindForward { .. }
|
| Operator::FindForward { .. }
|
||||||
|
@ -1118,7 +1136,8 @@ impl Operator {
|
||||||
| Operator::Jump { .. }
|
| Operator::Jump { .. }
|
||||||
| Operator::Register
|
| Operator::Register
|
||||||
| Operator::RecordRegister
|
| Operator::RecordRegister
|
||||||
| Operator::ReplayRegister => false,
|
| Operator::ReplayRegister
|
||||||
|
| Operator::HelixMatch => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -84,6 +84,22 @@ struct PushFindBackward {
|
||||||
multiline: bool,
|
multiline: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
|
||||||
|
#[action(namespace = vim)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
/// Selects the next object.
|
||||||
|
struct PushHelixNext {
|
||||||
|
around: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
|
||||||
|
#[action(namespace = vim)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
/// Selects the previous object.
|
||||||
|
struct PushHelixPrevious {
|
||||||
|
around: bool,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
|
#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
|
||||||
#[action(namespace = vim)]
|
#[action(namespace = vim)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
|
@ -222,6 +238,8 @@ actions!(
|
||||||
PushReplaceWithRegister,
|
PushReplaceWithRegister,
|
||||||
/// Toggles comments.
|
/// Toggles comments.
|
||||||
PushToggleComments,
|
PushToggleComments,
|
||||||
|
/// Starts a match operation.
|
||||||
|
PushHelixMatch,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -759,6 +777,27 @@ impl Vim {
|
||||||
Vim::action(editor, cx, |vim, _: &Enter, window, cx| {
|
Vim::action(editor, cx, |vim, _: &Enter, window, cx| {
|
||||||
vim.input_ignored("\n".into(), window, cx)
|
vim.input_ignored("\n".into(), window, cx)
|
||||||
});
|
});
|
||||||
|
Vim::action(editor, cx, |vim, _: &PushHelixMatch, window, cx| {
|
||||||
|
vim.push_operator(Operator::HelixMatch, window, cx)
|
||||||
|
});
|
||||||
|
Vim::action(editor, cx, |vim, action: &PushHelixNext, window, cx| {
|
||||||
|
vim.push_operator(
|
||||||
|
Operator::HelixNext {
|
||||||
|
around: action.around,
|
||||||
|
},
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
Vim::action(editor, cx, |vim, action: &PushHelixPrevious, window, cx| {
|
||||||
|
vim.push_operator(
|
||||||
|
Operator::HelixPrevious {
|
||||||
|
around: action.around,
|
||||||
|
},
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
normal::register(editor, cx);
|
normal::register(editor, cx);
|
||||||
insert::register(editor, cx);
|
insert::register(editor, cx);
|
||||||
|
|
|
@ -4356,6 +4356,8 @@ mod tests {
|
||||||
| "vim::PushJump"
|
| "vim::PushJump"
|
||||||
| "vim::PushDigraph"
|
| "vim::PushDigraph"
|
||||||
| "vim::PushLiteral"
|
| "vim::PushLiteral"
|
||||||
|
| "vim::PushHelixNext"
|
||||||
|
| "vim::PushHelixPrevious"
|
||||||
| "vim::Number"
|
| "vim::Number"
|
||||||
| "vim::SelectRegister"
|
| "vim::SelectRegister"
|
||||||
| "git::StageAndNext"
|
| "git::StageAndNext"
|
||||||
|
|
|
@ -9,3 +9,7 @@ For a guide on Vim-related features that are also available in Helix mode, pleas
|
||||||
To check the current status of Helix mode, or to request a missing Helix feature, checkout out the ["Are we Helix yet?" discussion](https://github.com/zed-industries/zed/discussions/33580).
|
To check the current status of Helix mode, or to request a missing Helix feature, checkout out the ["Are we Helix yet?" discussion](https://github.com/zed-industries/zed/discussions/33580).
|
||||||
|
|
||||||
For a detailed list of Helix's default keybindings, please visit the [official Helix documentation](https://docs.helix-editor.com/keymap.html).
|
For a detailed list of Helix's default keybindings, please visit the [official Helix documentation](https://docs.helix-editor.com/keymap.html).
|
||||||
|
|
||||||
|
## Core differences
|
||||||
|
|
||||||
|
Text object motions like `mi|` work, even if the cursor is on the `|` itself. Also, any text object that work with `mi` or `ma` also works with `]` and `[`, so for example `](` selects the next pair of parentheses after the cursor.
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue