
Before this change we didn't explicitly handle vim's exclusive-linewise edgecase (https://neovim.io/doc/user/motion.html#exclusive). Instead we had hard-coded workarounds in a few places to make our tests pass. The most pernicious of these workarounds was that we represented a visual line selection as including the trailing newline (or leading newline for files that end with no newline), which other code had to undo to get back to what the user indended. Closes #21440 Updates #6900 Release Notes: - vim: Fixed `d]}` to not delete the closing brace - vim: Fixed `d}` from the start of the line to not delete the paragraph separator - vim: Fixed `d}` from the middle of the line to not delete the final newline
3566 lines
113 KiB
Rust
3566 lines
113 KiB
Rust
use editor::{
|
|
display_map::{DisplayRow, DisplaySnapshot, FoldPoint, ToDisplayPoint},
|
|
movement::{
|
|
self, find_boundary, find_preceding_boundary_display_point, FindRange, TextLayoutDetails,
|
|
},
|
|
scroll::Autoscroll,
|
|
Anchor, Bias, DisplayPoint, Editor, RowExt, ToOffset, ToPoint,
|
|
};
|
|
use gpui::{action_with_deprecated_aliases, actions, impl_actions, px, Context, Window};
|
|
use language::{CharKind, Point, Selection, SelectionGoal};
|
|
use multi_buffer::MultiBufferRow;
|
|
use schemars::JsonSchema;
|
|
use serde::Deserialize;
|
|
use std::ops::Range;
|
|
use workspace::searchable::Direction;
|
|
|
|
use crate::{
|
|
normal::mark,
|
|
state::{Mode, Operator},
|
|
surrounds::SurroundsType,
|
|
Vim,
|
|
};
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
pub(crate) enum MotionKind {
|
|
Linewise,
|
|
Exclusive,
|
|
Inclusive,
|
|
}
|
|
|
|
impl MotionKind {
|
|
pub(crate) fn for_mode(mode: Mode) -> Self {
|
|
match mode {
|
|
Mode::VisualLine => MotionKind::Linewise,
|
|
_ => MotionKind::Exclusive,
|
|
}
|
|
}
|
|
|
|
pub(crate) fn linewise(&self) -> bool {
|
|
matches!(self, MotionKind::Linewise)
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub enum Motion {
|
|
Left,
|
|
WrappingLeft,
|
|
Down {
|
|
display_lines: bool,
|
|
},
|
|
Up {
|
|
display_lines: bool,
|
|
},
|
|
Right,
|
|
WrappingRight,
|
|
NextWordStart {
|
|
ignore_punctuation: bool,
|
|
},
|
|
NextWordEnd {
|
|
ignore_punctuation: bool,
|
|
},
|
|
PreviousWordStart {
|
|
ignore_punctuation: bool,
|
|
},
|
|
PreviousWordEnd {
|
|
ignore_punctuation: bool,
|
|
},
|
|
NextSubwordStart {
|
|
ignore_punctuation: bool,
|
|
},
|
|
NextSubwordEnd {
|
|
ignore_punctuation: bool,
|
|
},
|
|
PreviousSubwordStart {
|
|
ignore_punctuation: bool,
|
|
},
|
|
PreviousSubwordEnd {
|
|
ignore_punctuation: bool,
|
|
},
|
|
FirstNonWhitespace {
|
|
display_lines: bool,
|
|
},
|
|
CurrentLine,
|
|
StartOfLine {
|
|
display_lines: bool,
|
|
},
|
|
EndOfLine {
|
|
display_lines: bool,
|
|
},
|
|
SentenceBackward,
|
|
SentenceForward,
|
|
StartOfParagraph,
|
|
EndOfParagraph,
|
|
StartOfDocument,
|
|
EndOfDocument,
|
|
Matching,
|
|
GoToPercentage,
|
|
UnmatchedForward {
|
|
char: char,
|
|
},
|
|
UnmatchedBackward {
|
|
char: char,
|
|
},
|
|
FindForward {
|
|
before: bool,
|
|
char: char,
|
|
mode: FindRange,
|
|
smartcase: bool,
|
|
},
|
|
FindBackward {
|
|
after: bool,
|
|
char: char,
|
|
mode: FindRange,
|
|
smartcase: bool,
|
|
},
|
|
Sneak {
|
|
first_char: char,
|
|
second_char: char,
|
|
smartcase: bool,
|
|
},
|
|
SneakBackward {
|
|
first_char: char,
|
|
second_char: char,
|
|
smartcase: bool,
|
|
},
|
|
RepeatFind {
|
|
last_find: Box<Motion>,
|
|
},
|
|
RepeatFindReversed {
|
|
last_find: Box<Motion>,
|
|
},
|
|
NextLineStart,
|
|
PreviousLineStart,
|
|
StartOfLineDownward,
|
|
EndOfLineDownward,
|
|
GoToColumn,
|
|
WindowTop,
|
|
WindowMiddle,
|
|
WindowBottom,
|
|
NextSectionStart,
|
|
NextSectionEnd,
|
|
PreviousSectionStart,
|
|
PreviousSectionEnd,
|
|
NextMethodStart,
|
|
NextMethodEnd,
|
|
PreviousMethodStart,
|
|
PreviousMethodEnd,
|
|
NextComment,
|
|
PreviousComment,
|
|
|
|
// we don't have a good way to run a search synchronously, so
|
|
// we handle search motions by running the search async and then
|
|
// calling back into motion with this
|
|
ZedSearchResult {
|
|
prior_selections: Vec<Range<Anchor>>,
|
|
new_selections: Vec<Range<Anchor>>,
|
|
},
|
|
Jump {
|
|
anchor: Anchor,
|
|
line: bool,
|
|
},
|
|
}
|
|
|
|
#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
|
|
#[serde(deny_unknown_fields)]
|
|
struct NextWordStart {
|
|
#[serde(default)]
|
|
ignore_punctuation: bool,
|
|
}
|
|
|
|
#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
|
|
#[serde(deny_unknown_fields)]
|
|
struct NextWordEnd {
|
|
#[serde(default)]
|
|
ignore_punctuation: bool,
|
|
}
|
|
|
|
#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
|
|
#[serde(deny_unknown_fields)]
|
|
struct PreviousWordStart {
|
|
#[serde(default)]
|
|
ignore_punctuation: bool,
|
|
}
|
|
|
|
#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
|
|
#[serde(deny_unknown_fields)]
|
|
struct PreviousWordEnd {
|
|
#[serde(default)]
|
|
ignore_punctuation: bool,
|
|
}
|
|
|
|
#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub(crate) struct NextSubwordStart {
|
|
#[serde(default)]
|
|
pub(crate) ignore_punctuation: bool,
|
|
}
|
|
|
|
#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub(crate) struct NextSubwordEnd {
|
|
#[serde(default)]
|
|
pub(crate) ignore_punctuation: bool,
|
|
}
|
|
|
|
#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub(crate) struct PreviousSubwordStart {
|
|
#[serde(default)]
|
|
pub(crate) ignore_punctuation: bool,
|
|
}
|
|
|
|
#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub(crate) struct PreviousSubwordEnd {
|
|
#[serde(default)]
|
|
pub(crate) ignore_punctuation: bool,
|
|
}
|
|
|
|
#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub(crate) struct Up {
|
|
#[serde(default)]
|
|
pub(crate) display_lines: bool,
|
|
}
|
|
|
|
#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub(crate) struct Down {
|
|
#[serde(default)]
|
|
pub(crate) display_lines: bool,
|
|
}
|
|
|
|
#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
|
|
#[serde(deny_unknown_fields)]
|
|
struct FirstNonWhitespace {
|
|
#[serde(default)]
|
|
display_lines: bool,
|
|
}
|
|
|
|
#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
|
|
#[serde(deny_unknown_fields)]
|
|
struct EndOfLine {
|
|
#[serde(default)]
|
|
display_lines: bool,
|
|
}
|
|
|
|
#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct StartOfLine {
|
|
#[serde(default)]
|
|
pub(crate) display_lines: bool,
|
|
}
|
|
|
|
#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
|
|
#[serde(deny_unknown_fields)]
|
|
struct UnmatchedForward {
|
|
#[serde(default)]
|
|
char: char,
|
|
}
|
|
|
|
#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
|
|
#[serde(deny_unknown_fields)]
|
|
struct UnmatchedBackward {
|
|
#[serde(default)]
|
|
char: char,
|
|
}
|
|
|
|
impl_actions!(
|
|
vim,
|
|
[
|
|
StartOfLine,
|
|
EndOfLine,
|
|
FirstNonWhitespace,
|
|
Down,
|
|
Up,
|
|
NextWordStart,
|
|
NextWordEnd,
|
|
PreviousWordStart,
|
|
PreviousWordEnd,
|
|
NextSubwordStart,
|
|
NextSubwordEnd,
|
|
PreviousSubwordStart,
|
|
PreviousSubwordEnd,
|
|
UnmatchedForward,
|
|
UnmatchedBackward
|
|
]
|
|
);
|
|
|
|
actions!(
|
|
vim,
|
|
[
|
|
Left,
|
|
Backspace,
|
|
Right,
|
|
Space,
|
|
CurrentLine,
|
|
SentenceForward,
|
|
SentenceBackward,
|
|
StartOfParagraph,
|
|
EndOfParagraph,
|
|
StartOfDocument,
|
|
EndOfDocument,
|
|
Matching,
|
|
GoToPercentage,
|
|
NextLineStart,
|
|
PreviousLineStart,
|
|
StartOfLineDownward,
|
|
EndOfLineDownward,
|
|
GoToColumn,
|
|
RepeatFind,
|
|
RepeatFindReversed,
|
|
WindowTop,
|
|
WindowMiddle,
|
|
WindowBottom,
|
|
NextSectionStart,
|
|
NextSectionEnd,
|
|
PreviousSectionStart,
|
|
PreviousSectionEnd,
|
|
NextMethodStart,
|
|
NextMethodEnd,
|
|
PreviousMethodStart,
|
|
PreviousMethodEnd,
|
|
NextComment,
|
|
PreviousComment,
|
|
]
|
|
);
|
|
|
|
action_with_deprecated_aliases!(vim, WrappingLeft, ["vim::Backspace"]);
|
|
action_with_deprecated_aliases!(vim, WrappingRight, ["vim::Space"]);
|
|
|
|
pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
|
|
Vim::action(editor, cx, |vim, _: &Left, window, cx| {
|
|
vim.motion(Motion::Left, window, cx)
|
|
});
|
|
Vim::action(editor, cx, |vim, _: &WrappingLeft, window, cx| {
|
|
vim.motion(Motion::WrappingLeft, window, cx)
|
|
});
|
|
// Deprecated.
|
|
Vim::action(editor, cx, |vim, _: &Backspace, window, cx| {
|
|
vim.motion(Motion::WrappingLeft, window, cx)
|
|
});
|
|
Vim::action(editor, cx, |vim, action: &Down, window, cx| {
|
|
vim.motion(
|
|
Motion::Down {
|
|
display_lines: action.display_lines,
|
|
},
|
|
window,
|
|
cx,
|
|
)
|
|
});
|
|
Vim::action(editor, cx, |vim, action: &Up, window, cx| {
|
|
vim.motion(
|
|
Motion::Up {
|
|
display_lines: action.display_lines,
|
|
},
|
|
window,
|
|
cx,
|
|
)
|
|
});
|
|
Vim::action(editor, cx, |vim, _: &Right, window, cx| {
|
|
vim.motion(Motion::Right, window, cx)
|
|
});
|
|
Vim::action(editor, cx, |vim, _: &WrappingRight, window, cx| {
|
|
vim.motion(Motion::WrappingRight, window, cx)
|
|
});
|
|
// Deprecated.
|
|
Vim::action(editor, cx, |vim, _: &Space, window, cx| {
|
|
vim.motion(Motion::WrappingRight, window, cx)
|
|
});
|
|
Vim::action(
|
|
editor,
|
|
cx,
|
|
|vim, action: &FirstNonWhitespace, window, cx| {
|
|
vim.motion(
|
|
Motion::FirstNonWhitespace {
|
|
display_lines: action.display_lines,
|
|
},
|
|
window,
|
|
cx,
|
|
)
|
|
},
|
|
);
|
|
Vim::action(editor, cx, |vim, action: &StartOfLine, window, cx| {
|
|
vim.motion(
|
|
Motion::StartOfLine {
|
|
display_lines: action.display_lines,
|
|
},
|
|
window,
|
|
cx,
|
|
)
|
|
});
|
|
Vim::action(editor, cx, |vim, action: &EndOfLine, window, cx| {
|
|
vim.motion(
|
|
Motion::EndOfLine {
|
|
display_lines: action.display_lines,
|
|
},
|
|
window,
|
|
cx,
|
|
)
|
|
});
|
|
Vim::action(editor, cx, |vim, _: &CurrentLine, window, cx| {
|
|
vim.motion(Motion::CurrentLine, window, cx)
|
|
});
|
|
Vim::action(editor, cx, |vim, _: &StartOfParagraph, window, cx| {
|
|
vim.motion(Motion::StartOfParagraph, window, cx)
|
|
});
|
|
Vim::action(editor, cx, |vim, _: &EndOfParagraph, window, cx| {
|
|
vim.motion(Motion::EndOfParagraph, window, cx)
|
|
});
|
|
|
|
Vim::action(editor, cx, |vim, _: &SentenceForward, window, cx| {
|
|
vim.motion(Motion::SentenceForward, window, cx)
|
|
});
|
|
Vim::action(editor, cx, |vim, _: &SentenceBackward, window, cx| {
|
|
vim.motion(Motion::SentenceBackward, window, cx)
|
|
});
|
|
Vim::action(editor, cx, |vim, _: &StartOfDocument, window, cx| {
|
|
vim.motion(Motion::StartOfDocument, window, cx)
|
|
});
|
|
Vim::action(editor, cx, |vim, _: &EndOfDocument, window, cx| {
|
|
vim.motion(Motion::EndOfDocument, window, cx)
|
|
});
|
|
Vim::action(editor, cx, |vim, _: &Matching, window, cx| {
|
|
vim.motion(Motion::Matching, window, cx)
|
|
});
|
|
Vim::action(editor, cx, |vim, _: &GoToPercentage, window, cx| {
|
|
vim.motion(Motion::GoToPercentage, window, cx)
|
|
});
|
|
Vim::action(
|
|
editor,
|
|
cx,
|
|
|vim, &UnmatchedForward { char }: &UnmatchedForward, window, cx| {
|
|
vim.motion(Motion::UnmatchedForward { char }, window, cx)
|
|
},
|
|
);
|
|
Vim::action(
|
|
editor,
|
|
cx,
|
|
|vim, &UnmatchedBackward { char }: &UnmatchedBackward, window, cx| {
|
|
vim.motion(Motion::UnmatchedBackward { char }, window, cx)
|
|
},
|
|
);
|
|
Vim::action(
|
|
editor,
|
|
cx,
|
|
|vim, &NextWordStart { ignore_punctuation }: &NextWordStart, window, cx| {
|
|
vim.motion(Motion::NextWordStart { ignore_punctuation }, window, cx)
|
|
},
|
|
);
|
|
Vim::action(
|
|
editor,
|
|
cx,
|
|
|vim, &NextWordEnd { ignore_punctuation }: &NextWordEnd, window, cx| {
|
|
vim.motion(Motion::NextWordEnd { ignore_punctuation }, window, cx)
|
|
},
|
|
);
|
|
Vim::action(
|
|
editor,
|
|
cx,
|
|
|vim, &PreviousWordStart { ignore_punctuation }: &PreviousWordStart, window, cx| {
|
|
vim.motion(Motion::PreviousWordStart { ignore_punctuation }, window, cx)
|
|
},
|
|
);
|
|
Vim::action(
|
|
editor,
|
|
cx,
|
|
|vim, &PreviousWordEnd { ignore_punctuation }, window, cx| {
|
|
vim.motion(Motion::PreviousWordEnd { ignore_punctuation }, window, cx)
|
|
},
|
|
);
|
|
Vim::action(
|
|
editor,
|
|
cx,
|
|
|vim, &NextSubwordStart { ignore_punctuation }: &NextSubwordStart, window, cx| {
|
|
vim.motion(Motion::NextSubwordStart { ignore_punctuation }, window, cx)
|
|
},
|
|
);
|
|
Vim::action(
|
|
editor,
|
|
cx,
|
|
|vim, &NextSubwordEnd { ignore_punctuation }: &NextSubwordEnd, window, cx| {
|
|
vim.motion(Motion::NextSubwordEnd { ignore_punctuation }, window, cx)
|
|
},
|
|
);
|
|
Vim::action(
|
|
editor,
|
|
cx,
|
|
|vim, &PreviousSubwordStart { ignore_punctuation }: &PreviousSubwordStart, window, cx| {
|
|
vim.motion(
|
|
Motion::PreviousSubwordStart { ignore_punctuation },
|
|
window,
|
|
cx,
|
|
)
|
|
},
|
|
);
|
|
Vim::action(
|
|
editor,
|
|
cx,
|
|
|vim, &PreviousSubwordEnd { ignore_punctuation }, window, cx| {
|
|
vim.motion(
|
|
Motion::PreviousSubwordEnd { ignore_punctuation },
|
|
window,
|
|
cx,
|
|
)
|
|
},
|
|
);
|
|
Vim::action(editor, cx, |vim, &NextLineStart, window, cx| {
|
|
vim.motion(Motion::NextLineStart, window, cx)
|
|
});
|
|
Vim::action(editor, cx, |vim, &PreviousLineStart, window, cx| {
|
|
vim.motion(Motion::PreviousLineStart, window, cx)
|
|
});
|
|
Vim::action(editor, cx, |vim, &StartOfLineDownward, window, cx| {
|
|
vim.motion(Motion::StartOfLineDownward, window, cx)
|
|
});
|
|
Vim::action(editor, cx, |vim, &EndOfLineDownward, window, cx| {
|
|
vim.motion(Motion::EndOfLineDownward, window, cx)
|
|
});
|
|
Vim::action(editor, cx, |vim, &GoToColumn, window, cx| {
|
|
vim.motion(Motion::GoToColumn, window, cx)
|
|
});
|
|
|
|
Vim::action(editor, cx, |vim, _: &RepeatFind, window, cx| {
|
|
if let Some(last_find) = Vim::globals(cx).last_find.clone().map(Box::new) {
|
|
vim.motion(Motion::RepeatFind { last_find }, window, cx);
|
|
}
|
|
});
|
|
|
|
Vim::action(editor, cx, |vim, _: &RepeatFindReversed, window, cx| {
|
|
if let Some(last_find) = Vim::globals(cx).last_find.clone().map(Box::new) {
|
|
vim.motion(Motion::RepeatFindReversed { last_find }, window, cx);
|
|
}
|
|
});
|
|
Vim::action(editor, cx, |vim, &WindowTop, window, cx| {
|
|
vim.motion(Motion::WindowTop, window, cx)
|
|
});
|
|
Vim::action(editor, cx, |vim, &WindowMiddle, window, cx| {
|
|
vim.motion(Motion::WindowMiddle, window, cx)
|
|
});
|
|
Vim::action(editor, cx, |vim, &WindowBottom, window, cx| {
|
|
vim.motion(Motion::WindowBottom, window, cx)
|
|
});
|
|
|
|
Vim::action(editor, cx, |vim, &PreviousSectionStart, window, cx| {
|
|
vim.motion(Motion::PreviousSectionStart, window, cx)
|
|
});
|
|
Vim::action(editor, cx, |vim, &NextSectionStart, window, cx| {
|
|
vim.motion(Motion::NextSectionStart, window, cx)
|
|
});
|
|
Vim::action(editor, cx, |vim, &PreviousSectionEnd, window, cx| {
|
|
vim.motion(Motion::PreviousSectionEnd, window, cx)
|
|
});
|
|
Vim::action(editor, cx, |vim, &NextSectionEnd, window, cx| {
|
|
vim.motion(Motion::NextSectionEnd, window, cx)
|
|
});
|
|
Vim::action(editor, cx, |vim, &PreviousMethodStart, window, cx| {
|
|
vim.motion(Motion::PreviousMethodStart, window, cx)
|
|
});
|
|
Vim::action(editor, cx, |vim, &NextMethodStart, window, cx| {
|
|
vim.motion(Motion::NextMethodStart, window, cx)
|
|
});
|
|
Vim::action(editor, cx, |vim, &PreviousMethodEnd, window, cx| {
|
|
vim.motion(Motion::PreviousMethodEnd, window, cx)
|
|
});
|
|
Vim::action(editor, cx, |vim, &NextMethodEnd, window, cx| {
|
|
vim.motion(Motion::NextMethodEnd, window, cx)
|
|
});
|
|
Vim::action(editor, cx, |vim, &NextComment, window, cx| {
|
|
vim.motion(Motion::NextComment, window, cx)
|
|
});
|
|
Vim::action(editor, cx, |vim, &PreviousComment, window, cx| {
|
|
vim.motion(Motion::PreviousComment, window, cx)
|
|
});
|
|
}
|
|
|
|
impl Vim {
|
|
pub(crate) fn search_motion(&mut self, m: Motion, window: &mut Window, cx: &mut Context<Self>) {
|
|
if let Motion::ZedSearchResult {
|
|
prior_selections, ..
|
|
} = &m
|
|
{
|
|
match self.mode {
|
|
Mode::Visual | Mode::VisualLine | Mode::VisualBlock => {
|
|
if !prior_selections.is_empty() {
|
|
self.update_editor(window, cx, |_, editor, window, cx| {
|
|
editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
|
|
s.select_ranges(prior_selections.iter().cloned())
|
|
})
|
|
});
|
|
}
|
|
}
|
|
Mode::Normal | Mode::Replace | Mode::Insert => {
|
|
if self.active_operator().is_none() {
|
|
return;
|
|
}
|
|
}
|
|
|
|
Mode::HelixNormal => {}
|
|
}
|
|
}
|
|
|
|
self.motion(m, window, cx)
|
|
}
|
|
|
|
pub(crate) fn motion(&mut self, motion: Motion, window: &mut Window, cx: &mut Context<Self>) {
|
|
if let Some(Operator::FindForward { .. })
|
|
| Some(Operator::Sneak { .. })
|
|
| Some(Operator::SneakBackward { .. })
|
|
| Some(Operator::FindBackward { .. }) = self.active_operator()
|
|
{
|
|
self.pop_operator(window, cx);
|
|
}
|
|
|
|
let count = Vim::take_count(cx);
|
|
let active_operator = self.active_operator();
|
|
let mut waiting_operator: Option<Operator> = None;
|
|
match self.mode {
|
|
Mode::Normal | Mode::Replace | Mode::Insert => {
|
|
if active_operator == Some(Operator::AddSurrounds { target: None }) {
|
|
waiting_operator = Some(Operator::AddSurrounds {
|
|
target: Some(SurroundsType::Motion(motion)),
|
|
});
|
|
} else {
|
|
self.normal_motion(motion.clone(), active_operator.clone(), count, window, cx)
|
|
}
|
|
}
|
|
Mode::Visual | Mode::VisualLine | Mode::VisualBlock => {
|
|
self.visual_motion(motion.clone(), count, window, cx)
|
|
}
|
|
|
|
Mode::HelixNormal => self.helix_normal_motion(motion.clone(), count, window, cx),
|
|
}
|
|
self.clear_operator(window, cx);
|
|
if let Some(operator) = waiting_operator {
|
|
self.push_operator(operator, window, cx);
|
|
Vim::globals(cx).pre_count = count
|
|
}
|
|
}
|
|
}
|
|
|
|
// Motion handling is specified here:
|
|
// https://github.com/vim/vim/blob/master/runtime/doc/motion.txt
|
|
impl Motion {
|
|
fn default_kind(&self) -> MotionKind {
|
|
use Motion::*;
|
|
match self {
|
|
Down { .. }
|
|
| Up { .. }
|
|
| StartOfDocument
|
|
| EndOfDocument
|
|
| CurrentLine
|
|
| NextLineStart
|
|
| PreviousLineStart
|
|
| StartOfLineDownward
|
|
| WindowTop
|
|
| WindowMiddle
|
|
| WindowBottom
|
|
| NextSectionStart
|
|
| NextSectionEnd
|
|
| PreviousSectionStart
|
|
| PreviousSectionEnd
|
|
| NextMethodStart
|
|
| NextMethodEnd
|
|
| PreviousMethodStart
|
|
| PreviousMethodEnd
|
|
| NextComment
|
|
| PreviousComment
|
|
| GoToPercentage
|
|
| Jump { line: true, .. } => MotionKind::Linewise,
|
|
EndOfLine { .. }
|
|
| EndOfLineDownward
|
|
| Matching
|
|
| FindForward { .. }
|
|
| NextWordEnd { .. }
|
|
| PreviousWordEnd { .. }
|
|
| NextSubwordEnd { .. }
|
|
| PreviousSubwordEnd { .. } => MotionKind::Inclusive,
|
|
Left
|
|
| WrappingLeft
|
|
| Right
|
|
| WrappingRight
|
|
| StartOfLine { .. }
|
|
| StartOfParagraph
|
|
| EndOfParagraph
|
|
| SentenceBackward
|
|
| SentenceForward
|
|
| GoToColumn
|
|
| UnmatchedForward { .. }
|
|
| UnmatchedBackward { .. }
|
|
| NextWordStart { .. }
|
|
| PreviousWordStart { .. }
|
|
| NextSubwordStart { .. }
|
|
| PreviousSubwordStart { .. }
|
|
| FirstNonWhitespace { .. }
|
|
| FindBackward { .. }
|
|
| Sneak { .. }
|
|
| SneakBackward { .. }
|
|
| Jump { .. }
|
|
| ZedSearchResult { .. } => MotionKind::Exclusive,
|
|
RepeatFind { last_find: motion } | RepeatFindReversed { last_find: motion } => {
|
|
motion.default_kind()
|
|
}
|
|
}
|
|
}
|
|
|
|
fn skip_exclusive_special_case(&self) -> bool {
|
|
match self {
|
|
Motion::WrappingLeft | Motion::WrappingRight => true,
|
|
_ => false,
|
|
}
|
|
}
|
|
|
|
pub fn infallible(&self) -> bool {
|
|
use Motion::*;
|
|
match self {
|
|
StartOfDocument | EndOfDocument | CurrentLine => true,
|
|
Down { .. }
|
|
| Up { .. }
|
|
| EndOfLine { .. }
|
|
| Matching
|
|
| UnmatchedForward { .. }
|
|
| UnmatchedBackward { .. }
|
|
| FindForward { .. }
|
|
| RepeatFind { .. }
|
|
| Left
|
|
| WrappingLeft
|
|
| Right
|
|
| WrappingRight
|
|
| StartOfLine { .. }
|
|
| StartOfParagraph
|
|
| EndOfParagraph
|
|
| SentenceBackward
|
|
| SentenceForward
|
|
| StartOfLineDownward
|
|
| EndOfLineDownward
|
|
| GoToColumn
|
|
| GoToPercentage
|
|
| NextWordStart { .. }
|
|
| NextWordEnd { .. }
|
|
| PreviousWordStart { .. }
|
|
| PreviousWordEnd { .. }
|
|
| NextSubwordStart { .. }
|
|
| NextSubwordEnd { .. }
|
|
| PreviousSubwordStart { .. }
|
|
| PreviousSubwordEnd { .. }
|
|
| FirstNonWhitespace { .. }
|
|
| FindBackward { .. }
|
|
| Sneak { .. }
|
|
| SneakBackward { .. }
|
|
| RepeatFindReversed { .. }
|
|
| WindowTop
|
|
| WindowMiddle
|
|
| WindowBottom
|
|
| NextLineStart
|
|
| PreviousLineStart
|
|
| ZedSearchResult { .. }
|
|
| NextSectionStart
|
|
| NextSectionEnd
|
|
| PreviousSectionStart
|
|
| PreviousSectionEnd
|
|
| NextMethodStart
|
|
| NextMethodEnd
|
|
| PreviousMethodStart
|
|
| PreviousMethodEnd
|
|
| NextComment
|
|
| PreviousComment
|
|
| Jump { .. } => false,
|
|
}
|
|
}
|
|
|
|
pub fn move_point(
|
|
&self,
|
|
map: &DisplaySnapshot,
|
|
point: DisplayPoint,
|
|
goal: SelectionGoal,
|
|
maybe_times: Option<usize>,
|
|
text_layout_details: &TextLayoutDetails,
|
|
) -> Option<(DisplayPoint, SelectionGoal)> {
|
|
let times = maybe_times.unwrap_or(1);
|
|
use Motion::*;
|
|
let infallible = self.infallible();
|
|
let (new_point, goal) = match self {
|
|
Left => (left(map, point, times), SelectionGoal::None),
|
|
WrappingLeft => (wrapping_left(map, point, times), SelectionGoal::None),
|
|
Down {
|
|
display_lines: false,
|
|
} => up_down_buffer_rows(map, point, goal, times as isize, text_layout_details),
|
|
Down {
|
|
display_lines: true,
|
|
} => down_display(map, point, goal, times, text_layout_details),
|
|
Up {
|
|
display_lines: false,
|
|
} => up_down_buffer_rows(map, point, goal, 0 - times as isize, text_layout_details),
|
|
Up {
|
|
display_lines: true,
|
|
} => up_display(map, point, goal, times, text_layout_details),
|
|
Right => (right(map, point, times), SelectionGoal::None),
|
|
WrappingRight => (wrapping_right(map, point, times), SelectionGoal::None),
|
|
NextWordStart { ignore_punctuation } => (
|
|
next_word_start(map, point, *ignore_punctuation, times),
|
|
SelectionGoal::None,
|
|
),
|
|
NextWordEnd { ignore_punctuation } => (
|
|
next_word_end(map, point, *ignore_punctuation, times, true),
|
|
SelectionGoal::None,
|
|
),
|
|
PreviousWordStart { ignore_punctuation } => (
|
|
previous_word_start(map, point, *ignore_punctuation, times),
|
|
SelectionGoal::None,
|
|
),
|
|
PreviousWordEnd { ignore_punctuation } => (
|
|
previous_word_end(map, point, *ignore_punctuation, times),
|
|
SelectionGoal::None,
|
|
),
|
|
NextSubwordStart { ignore_punctuation } => (
|
|
next_subword_start(map, point, *ignore_punctuation, times),
|
|
SelectionGoal::None,
|
|
),
|
|
NextSubwordEnd { ignore_punctuation } => (
|
|
next_subword_end(map, point, *ignore_punctuation, times, true),
|
|
SelectionGoal::None,
|
|
),
|
|
PreviousSubwordStart { ignore_punctuation } => (
|
|
previous_subword_start(map, point, *ignore_punctuation, times),
|
|
SelectionGoal::None,
|
|
),
|
|
PreviousSubwordEnd { ignore_punctuation } => (
|
|
previous_subword_end(map, point, *ignore_punctuation, times),
|
|
SelectionGoal::None,
|
|
),
|
|
FirstNonWhitespace { display_lines } => (
|
|
first_non_whitespace(map, *display_lines, point),
|
|
SelectionGoal::None,
|
|
),
|
|
StartOfLine { display_lines } => (
|
|
start_of_line(map, *display_lines, point),
|
|
SelectionGoal::None,
|
|
),
|
|
EndOfLine { display_lines } => (
|
|
end_of_line(map, *display_lines, point, times),
|
|
SelectionGoal::None,
|
|
),
|
|
SentenceBackward => (sentence_backwards(map, point, times), SelectionGoal::None),
|
|
SentenceForward => (sentence_forwards(map, point, times), SelectionGoal::None),
|
|
StartOfParagraph => (
|
|
movement::start_of_paragraph(map, point, times),
|
|
SelectionGoal::None,
|
|
),
|
|
EndOfParagraph => (
|
|
map.clip_at_line_end(movement::end_of_paragraph(map, point, times)),
|
|
SelectionGoal::None,
|
|
),
|
|
CurrentLine => (next_line_end(map, point, times), SelectionGoal::None),
|
|
StartOfDocument => (
|
|
start_of_document(map, point, maybe_times),
|
|
SelectionGoal::None,
|
|
),
|
|
EndOfDocument => (
|
|
end_of_document(map, point, maybe_times),
|
|
SelectionGoal::None,
|
|
),
|
|
Matching => (matching(map, point), SelectionGoal::None),
|
|
GoToPercentage => (go_to_percentage(map, point, times), SelectionGoal::None),
|
|
UnmatchedForward { char } => (
|
|
unmatched_forward(map, point, *char, times),
|
|
SelectionGoal::None,
|
|
),
|
|
UnmatchedBackward { char } => (
|
|
unmatched_backward(map, point, *char, times),
|
|
SelectionGoal::None,
|
|
),
|
|
// t f
|
|
FindForward {
|
|
before,
|
|
char,
|
|
mode,
|
|
smartcase,
|
|
} => {
|
|
return find_forward(map, point, *before, *char, times, *mode, *smartcase)
|
|
.map(|new_point| (new_point, SelectionGoal::None))
|
|
}
|
|
// T F
|
|
FindBackward {
|
|
after,
|
|
char,
|
|
mode,
|
|
smartcase,
|
|
} => (
|
|
find_backward(map, point, *after, *char, times, *mode, *smartcase),
|
|
SelectionGoal::None,
|
|
),
|
|
Sneak {
|
|
first_char,
|
|
second_char,
|
|
smartcase,
|
|
} => {
|
|
return sneak(map, point, *first_char, *second_char, times, *smartcase)
|
|
.map(|new_point| (new_point, SelectionGoal::None));
|
|
}
|
|
SneakBackward {
|
|
first_char,
|
|
second_char,
|
|
smartcase,
|
|
} => {
|
|
return sneak_backward(map, point, *first_char, *second_char, times, *smartcase)
|
|
.map(|new_point| (new_point, SelectionGoal::None));
|
|
}
|
|
// ; -- repeat the last find done with t, f, T, F
|
|
RepeatFind { last_find } => match **last_find {
|
|
Motion::FindForward {
|
|
before,
|
|
char,
|
|
mode,
|
|
smartcase,
|
|
} => {
|
|
let mut new_point =
|
|
find_forward(map, point, before, char, times, mode, smartcase);
|
|
if new_point == Some(point) {
|
|
new_point =
|
|
find_forward(map, point, before, char, times + 1, mode, smartcase);
|
|
}
|
|
|
|
return new_point.map(|new_point| (new_point, SelectionGoal::None));
|
|
}
|
|
|
|
Motion::FindBackward {
|
|
after,
|
|
char,
|
|
mode,
|
|
smartcase,
|
|
} => {
|
|
let mut new_point =
|
|
find_backward(map, point, after, char, times, mode, smartcase);
|
|
if new_point == point {
|
|
new_point =
|
|
find_backward(map, point, after, char, times + 1, mode, smartcase);
|
|
}
|
|
|
|
(new_point, SelectionGoal::None)
|
|
}
|
|
Motion::Sneak {
|
|
first_char,
|
|
second_char,
|
|
smartcase,
|
|
} => {
|
|
let mut new_point =
|
|
sneak(map, point, first_char, second_char, times, smartcase);
|
|
if new_point == Some(point) {
|
|
new_point =
|
|
sneak(map, point, first_char, second_char, times + 1, smartcase);
|
|
}
|
|
|
|
return new_point.map(|new_point| (new_point, SelectionGoal::None));
|
|
}
|
|
|
|
Motion::SneakBackward {
|
|
first_char,
|
|
second_char,
|
|
smartcase,
|
|
} => {
|
|
let mut new_point =
|
|
sneak_backward(map, point, first_char, second_char, times, smartcase);
|
|
if new_point == Some(point) {
|
|
new_point = sneak_backward(
|
|
map,
|
|
point,
|
|
first_char,
|
|
second_char,
|
|
times + 1,
|
|
smartcase,
|
|
);
|
|
}
|
|
|
|
return new_point.map(|new_point| (new_point, SelectionGoal::None));
|
|
}
|
|
_ => return None,
|
|
},
|
|
// , -- repeat the last find done with t, f, T, F, s, S, in opposite direction
|
|
RepeatFindReversed { last_find } => match **last_find {
|
|
Motion::FindForward {
|
|
before,
|
|
char,
|
|
mode,
|
|
smartcase,
|
|
} => {
|
|
let mut new_point =
|
|
find_backward(map, point, before, char, times, mode, smartcase);
|
|
if new_point == point {
|
|
new_point =
|
|
find_backward(map, point, before, char, times + 1, mode, smartcase);
|
|
}
|
|
|
|
(new_point, SelectionGoal::None)
|
|
}
|
|
|
|
Motion::FindBackward {
|
|
after,
|
|
char,
|
|
mode,
|
|
smartcase,
|
|
} => {
|
|
let mut new_point =
|
|
find_forward(map, point, after, char, times, mode, smartcase);
|
|
if new_point == Some(point) {
|
|
new_point =
|
|
find_forward(map, point, after, char, times + 1, mode, smartcase);
|
|
}
|
|
|
|
return new_point.map(|new_point| (new_point, SelectionGoal::None));
|
|
}
|
|
|
|
Motion::Sneak {
|
|
first_char,
|
|
second_char,
|
|
smartcase,
|
|
} => {
|
|
let mut new_point =
|
|
sneak_backward(map, point, first_char, second_char, times, smartcase);
|
|
if new_point == Some(point) {
|
|
new_point = sneak_backward(
|
|
map,
|
|
point,
|
|
first_char,
|
|
second_char,
|
|
times + 1,
|
|
smartcase,
|
|
);
|
|
}
|
|
|
|
return new_point.map(|new_point| (new_point, SelectionGoal::None));
|
|
}
|
|
|
|
Motion::SneakBackward {
|
|
first_char,
|
|
second_char,
|
|
smartcase,
|
|
} => {
|
|
let mut new_point =
|
|
sneak(map, point, first_char, second_char, times, smartcase);
|
|
if new_point == Some(point) {
|
|
new_point =
|
|
sneak(map, point, first_char, second_char, times + 1, smartcase);
|
|
}
|
|
|
|
return new_point.map(|new_point| (new_point, SelectionGoal::None));
|
|
}
|
|
_ => return None,
|
|
},
|
|
NextLineStart => (next_line_start(map, point, times), SelectionGoal::None),
|
|
PreviousLineStart => (previous_line_start(map, point, times), SelectionGoal::None),
|
|
StartOfLineDownward => (next_line_start(map, point, times - 1), SelectionGoal::None),
|
|
EndOfLineDownward => (last_non_whitespace(map, point, times), SelectionGoal::None),
|
|
GoToColumn => (go_to_column(map, point, times), SelectionGoal::None),
|
|
WindowTop => window_top(map, point, text_layout_details, times - 1),
|
|
WindowMiddle => window_middle(map, point, text_layout_details),
|
|
WindowBottom => window_bottom(map, point, text_layout_details, times - 1),
|
|
Jump { line, anchor } => mark::jump_motion(map, *anchor, *line),
|
|
ZedSearchResult { new_selections, .. } => {
|
|
// There will be only one selection, as
|
|
// Search::SelectNextMatch selects a single match.
|
|
if let Some(new_selection) = new_selections.first() {
|
|
(
|
|
new_selection.start.to_display_point(map),
|
|
SelectionGoal::None,
|
|
)
|
|
} else {
|
|
return None;
|
|
}
|
|
}
|
|
NextSectionStart => (
|
|
section_motion(map, point, times, Direction::Next, true),
|
|
SelectionGoal::None,
|
|
),
|
|
NextSectionEnd => (
|
|
section_motion(map, point, times, Direction::Next, false),
|
|
SelectionGoal::None,
|
|
),
|
|
PreviousSectionStart => (
|
|
section_motion(map, point, times, Direction::Prev, true),
|
|
SelectionGoal::None,
|
|
),
|
|
PreviousSectionEnd => (
|
|
section_motion(map, point, times, Direction::Prev, false),
|
|
SelectionGoal::None,
|
|
),
|
|
|
|
NextMethodStart => (
|
|
method_motion(map, point, times, Direction::Next, true),
|
|
SelectionGoal::None,
|
|
),
|
|
NextMethodEnd => (
|
|
method_motion(map, point, times, Direction::Next, false),
|
|
SelectionGoal::None,
|
|
),
|
|
PreviousMethodStart => (
|
|
method_motion(map, point, times, Direction::Prev, true),
|
|
SelectionGoal::None,
|
|
),
|
|
PreviousMethodEnd => (
|
|
method_motion(map, point, times, Direction::Prev, false),
|
|
SelectionGoal::None,
|
|
),
|
|
NextComment => (
|
|
comment_motion(map, point, times, Direction::Next),
|
|
SelectionGoal::None,
|
|
),
|
|
PreviousComment => (
|
|
comment_motion(map, point, times, Direction::Prev),
|
|
SelectionGoal::None,
|
|
),
|
|
};
|
|
|
|
(new_point != point || infallible).then_some((new_point, goal))
|
|
}
|
|
|
|
// Get the range value after self is applied to the specified selection.
|
|
pub fn range(
|
|
&self,
|
|
map: &DisplaySnapshot,
|
|
selection: Selection<DisplayPoint>,
|
|
times: Option<usize>,
|
|
text_layout_details: &TextLayoutDetails,
|
|
) -> Option<(Range<DisplayPoint>, MotionKind)> {
|
|
if let Motion::ZedSearchResult {
|
|
prior_selections,
|
|
new_selections,
|
|
} = self
|
|
{
|
|
if let Some((prior_selection, new_selection)) =
|
|
prior_selections.first().zip(new_selections.first())
|
|
{
|
|
let start = prior_selection
|
|
.start
|
|
.to_display_point(map)
|
|
.min(new_selection.start.to_display_point(map));
|
|
let end = new_selection
|
|
.end
|
|
.to_display_point(map)
|
|
.max(prior_selection.end.to_display_point(map));
|
|
|
|
if start < end {
|
|
return Some((start..end, MotionKind::Exclusive));
|
|
} else {
|
|
return Some((end..start, MotionKind::Exclusive));
|
|
}
|
|
} else {
|
|
return None;
|
|
}
|
|
}
|
|
|
|
let (new_head, goal) = self.move_point(
|
|
map,
|
|
selection.head(),
|
|
selection.goal,
|
|
times,
|
|
text_layout_details,
|
|
)?;
|
|
let mut selection = selection.clone();
|
|
selection.set_head(new_head, goal);
|
|
|
|
let mut kind = self.default_kind();
|
|
|
|
if let Motion::NextWordStart {
|
|
ignore_punctuation: _,
|
|
} = self
|
|
{
|
|
// Another special case: When using the "w" motion in combination with an
|
|
// operator and the last word moved over is at the end of a line, the end of
|
|
// that word becomes the end of the operated text, not the first word in the
|
|
// next line.
|
|
let start = selection.start.to_point(map);
|
|
let end = selection.end.to_point(map);
|
|
let start_row = MultiBufferRow(selection.start.to_point(map).row);
|
|
if end.row > start.row {
|
|
selection.end = Point::new(start_row.0, map.buffer_snapshot.line_len(start_row))
|
|
.to_display_point(map);
|
|
|
|
// a bit of a hack, we need `cw` on a blank line to not delete the newline,
|
|
// but dw on a blank line should. The `Linewise` returned from this method
|
|
// causes the `d` operator to include the trailing newline.
|
|
if selection.start == selection.end {
|
|
return Some((selection.start..selection.end, MotionKind::Linewise));
|
|
}
|
|
}
|
|
} else if kind == MotionKind::Exclusive && !self.skip_exclusive_special_case() {
|
|
let start_point = selection.start.to_point(map);
|
|
let mut end_point = selection.end.to_point(map);
|
|
|
|
if end_point.row > start_point.row {
|
|
let first_non_blank_of_start_row = map
|
|
.line_indent_for_buffer_row(MultiBufferRow(start_point.row))
|
|
.raw_len();
|
|
// https://github.com/neovim/neovim/blob/ee143aaf65a0e662c42c636aa4a959682858b3e7/src/nvim/ops.c#L6178-L6203
|
|
if end_point.column == 0 {
|
|
// If the motion is exclusive and the end of the motion is in column 1, the
|
|
// end of the motion is moved to the end of the previous line and the motion
|
|
// becomes inclusive. Example: "}" moves to the first line after a paragraph,
|
|
// but "d}" will not include that line.
|
|
//
|
|
// If the motion is exclusive, the end of the motion is in column 1 and the
|
|
// start of the motion was at or before the first non-blank in the line, the
|
|
// motion becomes linewise. Example: If a paragraph begins with some blanks
|
|
// and you do "d}" while standing on the first non-blank, all the lines of
|
|
// the paragraph are deleted, including the blanks.
|
|
if start_point.column <= first_non_blank_of_start_row {
|
|
kind = MotionKind::Linewise;
|
|
} else {
|
|
kind = MotionKind::Inclusive;
|
|
}
|
|
end_point.row -= 1;
|
|
end_point.column = 0;
|
|
selection.end = map.clip_point(map.next_line_boundary(end_point).1, Bias::Left);
|
|
}
|
|
}
|
|
} else if kind == MotionKind::Inclusive {
|
|
selection.end = movement::saturating_right(map, selection.end)
|
|
}
|
|
|
|
if kind == MotionKind::Linewise {
|
|
selection.start = map.prev_line_boundary(selection.start.to_point(map)).1;
|
|
selection.end = map.next_line_boundary(selection.end.to_point(map)).1;
|
|
}
|
|
Some((selection.start..selection.end, kind))
|
|
}
|
|
|
|
// Expands a selection using self for an operator
|
|
pub fn expand_selection(
|
|
&self,
|
|
map: &DisplaySnapshot,
|
|
selection: &mut Selection<DisplayPoint>,
|
|
times: Option<usize>,
|
|
text_layout_details: &TextLayoutDetails,
|
|
) -> Option<MotionKind> {
|
|
let (range, kind) = self.range(map, selection.clone(), times, text_layout_details)?;
|
|
selection.start = range.start;
|
|
selection.end = range.end;
|
|
Some(kind)
|
|
}
|
|
}
|
|
|
|
fn left(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
|
|
for _ in 0..times {
|
|
point = movement::saturating_left(map, point);
|
|
if point.column() == 0 {
|
|
break;
|
|
}
|
|
}
|
|
point
|
|
}
|
|
|
|
pub(crate) fn wrapping_left(
|
|
map: &DisplaySnapshot,
|
|
mut point: DisplayPoint,
|
|
times: usize,
|
|
) -> DisplayPoint {
|
|
for _ in 0..times {
|
|
point = movement::left(map, point);
|
|
if point.is_zero() {
|
|
break;
|
|
}
|
|
}
|
|
point
|
|
}
|
|
|
|
fn wrapping_right(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
|
|
for _ in 0..times {
|
|
point = wrapping_right_single(map, point);
|
|
if point == map.max_point() {
|
|
break;
|
|
}
|
|
}
|
|
point
|
|
}
|
|
|
|
fn wrapping_right_single(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
|
|
let max_column = map.line_len(point.row()).saturating_sub(1);
|
|
if point.column() < max_column {
|
|
*point.column_mut() += 1;
|
|
point = map.clip_point(point, Bias::Right);
|
|
} else if point.row() < map.max_point().row() {
|
|
*point.row_mut() += 1;
|
|
*point.column_mut() = 0;
|
|
}
|
|
point
|
|
}
|
|
|
|
pub(crate) fn start_of_relative_buffer_row(
|
|
map: &DisplaySnapshot,
|
|
point: DisplayPoint,
|
|
times: isize,
|
|
) -> DisplayPoint {
|
|
let start = map.display_point_to_fold_point(point, Bias::Left);
|
|
let target = start.row() as isize + times;
|
|
let new_row = (target.max(0) as u32).min(map.fold_snapshot.max_point().row());
|
|
|
|
map.clip_point(
|
|
map.fold_point_to_display_point(
|
|
map.fold_snapshot
|
|
.clip_point(FoldPoint::new(new_row, 0), Bias::Right),
|
|
),
|
|
Bias::Right,
|
|
)
|
|
}
|
|
|
|
fn up_down_buffer_rows(
|
|
map: &DisplaySnapshot,
|
|
mut point: DisplayPoint,
|
|
mut goal: SelectionGoal,
|
|
mut times: isize,
|
|
text_layout_details: &TextLayoutDetails,
|
|
) -> (DisplayPoint, SelectionGoal) {
|
|
let bias = if times < 0 { Bias::Left } else { Bias::Right };
|
|
|
|
while map.is_folded_buffer_header(point.row()) {
|
|
if times < 0 {
|
|
(point, _) = movement::up(map, point, goal, true, text_layout_details);
|
|
times += 1;
|
|
} else if times > 0 {
|
|
(point, _) = movement::down(map, point, goal, true, text_layout_details);
|
|
times -= 1;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
let start = map.display_point_to_fold_point(point, Bias::Left);
|
|
let begin_folded_line = map.fold_point_to_display_point(
|
|
map.fold_snapshot
|
|
.clip_point(FoldPoint::new(start.row(), 0), Bias::Left),
|
|
);
|
|
let select_nth_wrapped_row = point.row().0 - begin_folded_line.row().0;
|
|
|
|
let (goal_wrap, goal_x) = match goal {
|
|
SelectionGoal::WrappedHorizontalPosition((row, x)) => (row, x),
|
|
SelectionGoal::HorizontalRange { end, .. } => (select_nth_wrapped_row, end),
|
|
SelectionGoal::HorizontalPosition(x) => (select_nth_wrapped_row, x),
|
|
_ => {
|
|
let x = map.x_for_display_point(point, text_layout_details);
|
|
goal = SelectionGoal::WrappedHorizontalPosition((select_nth_wrapped_row, x.0));
|
|
(select_nth_wrapped_row, x.0)
|
|
}
|
|
};
|
|
|
|
let target = start.row() as isize + times;
|
|
let new_row = (target.max(0) as u32).min(map.fold_snapshot.max_point().row());
|
|
|
|
let mut begin_folded_line = map.fold_point_to_display_point(
|
|
map.fold_snapshot
|
|
.clip_point(FoldPoint::new(new_row, 0), bias),
|
|
);
|
|
|
|
let mut i = 0;
|
|
while i < goal_wrap && begin_folded_line.row() < map.max_point().row() {
|
|
let next_folded_line = DisplayPoint::new(begin_folded_line.row().next_row(), 0);
|
|
if map
|
|
.display_point_to_fold_point(next_folded_line, bias)
|
|
.row()
|
|
== new_row
|
|
{
|
|
i += 1;
|
|
begin_folded_line = next_folded_line;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
let new_col = if i == goal_wrap {
|
|
map.display_column_for_x(begin_folded_line.row(), px(goal_x), text_layout_details)
|
|
} else {
|
|
map.line_len(begin_folded_line.row())
|
|
};
|
|
|
|
(
|
|
map.clip_point(DisplayPoint::new(begin_folded_line.row(), new_col), bias),
|
|
goal,
|
|
)
|
|
}
|
|
|
|
fn down_display(
|
|
map: &DisplaySnapshot,
|
|
mut point: DisplayPoint,
|
|
mut goal: SelectionGoal,
|
|
times: usize,
|
|
text_layout_details: &TextLayoutDetails,
|
|
) -> (DisplayPoint, SelectionGoal) {
|
|
for _ in 0..times {
|
|
(point, goal) = movement::down(map, point, goal, true, text_layout_details);
|
|
}
|
|
|
|
(point, goal)
|
|
}
|
|
|
|
fn up_display(
|
|
map: &DisplaySnapshot,
|
|
mut point: DisplayPoint,
|
|
mut goal: SelectionGoal,
|
|
times: usize,
|
|
text_layout_details: &TextLayoutDetails,
|
|
) -> (DisplayPoint, SelectionGoal) {
|
|
for _ in 0..times {
|
|
(point, goal) = movement::up(map, point, goal, true, text_layout_details);
|
|
}
|
|
|
|
(point, goal)
|
|
}
|
|
|
|
pub(crate) fn right(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
|
|
for _ in 0..times {
|
|
let new_point = movement::saturating_right(map, point);
|
|
if point == new_point {
|
|
break;
|
|
}
|
|
point = new_point;
|
|
}
|
|
point
|
|
}
|
|
|
|
pub(crate) fn next_char(
|
|
map: &DisplaySnapshot,
|
|
point: DisplayPoint,
|
|
allow_cross_newline: bool,
|
|
) -> DisplayPoint {
|
|
let mut new_point = point;
|
|
let mut max_column = map.line_len(new_point.row());
|
|
if !allow_cross_newline {
|
|
max_column -= 1;
|
|
}
|
|
if new_point.column() < max_column {
|
|
*new_point.column_mut() += 1;
|
|
} else if new_point < map.max_point() && allow_cross_newline {
|
|
*new_point.row_mut() += 1;
|
|
*new_point.column_mut() = 0;
|
|
}
|
|
map.clip_ignoring_line_ends(new_point, Bias::Right)
|
|
}
|
|
|
|
pub(crate) fn next_word_start(
|
|
map: &DisplaySnapshot,
|
|
mut point: DisplayPoint,
|
|
ignore_punctuation: bool,
|
|
times: usize,
|
|
) -> DisplayPoint {
|
|
let classifier = map
|
|
.buffer_snapshot
|
|
.char_classifier_at(point.to_point(map))
|
|
.ignore_punctuation(ignore_punctuation);
|
|
for _ in 0..times {
|
|
let mut crossed_newline = false;
|
|
let new_point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
|
|
let left_kind = classifier.kind(left);
|
|
let right_kind = classifier.kind(right);
|
|
let at_newline = right == '\n';
|
|
|
|
let found = (left_kind != right_kind && right_kind != CharKind::Whitespace)
|
|
|| at_newline && crossed_newline
|
|
|| at_newline && left == '\n'; // Prevents skipping repeated empty lines
|
|
|
|
crossed_newline |= at_newline;
|
|
found
|
|
});
|
|
if point == new_point {
|
|
break;
|
|
}
|
|
point = new_point;
|
|
}
|
|
point
|
|
}
|
|
|
|
pub(crate) fn next_word_end(
|
|
map: &DisplaySnapshot,
|
|
mut point: DisplayPoint,
|
|
ignore_punctuation: bool,
|
|
times: usize,
|
|
allow_cross_newline: bool,
|
|
) -> DisplayPoint {
|
|
let classifier = map
|
|
.buffer_snapshot
|
|
.char_classifier_at(point.to_point(map))
|
|
.ignore_punctuation(ignore_punctuation);
|
|
for _ in 0..times {
|
|
let new_point = next_char(map, point, allow_cross_newline);
|
|
let mut need_next_char = false;
|
|
let new_point = movement::find_boundary_exclusive(
|
|
map,
|
|
new_point,
|
|
FindRange::MultiLine,
|
|
|left, right| {
|
|
let left_kind = classifier.kind(left);
|
|
let right_kind = classifier.kind(right);
|
|
let at_newline = right == '\n';
|
|
|
|
if !allow_cross_newline && at_newline {
|
|
need_next_char = true;
|
|
return true;
|
|
}
|
|
|
|
left_kind != right_kind && left_kind != CharKind::Whitespace
|
|
},
|
|
);
|
|
let new_point = if need_next_char {
|
|
next_char(map, new_point, true)
|
|
} else {
|
|
new_point
|
|
};
|
|
let new_point = map.clip_point(new_point, Bias::Left);
|
|
if point == new_point {
|
|
break;
|
|
}
|
|
point = new_point;
|
|
}
|
|
point
|
|
}
|
|
|
|
fn previous_word_start(
|
|
map: &DisplaySnapshot,
|
|
mut point: DisplayPoint,
|
|
ignore_punctuation: bool,
|
|
times: usize,
|
|
) -> DisplayPoint {
|
|
let classifier = map
|
|
.buffer_snapshot
|
|
.char_classifier_at(point.to_point(map))
|
|
.ignore_punctuation(ignore_punctuation);
|
|
for _ in 0..times {
|
|
// This works even though find_preceding_boundary is called for every character in the line containing
|
|
// cursor because the newline is checked only once.
|
|
let new_point = movement::find_preceding_boundary_display_point(
|
|
map,
|
|
point,
|
|
FindRange::MultiLine,
|
|
|left, right| {
|
|
let left_kind = classifier.kind(left);
|
|
let right_kind = classifier.kind(right);
|
|
|
|
(left_kind != right_kind && !right.is_whitespace()) || left == '\n'
|
|
},
|
|
);
|
|
if point == new_point {
|
|
break;
|
|
}
|
|
point = new_point;
|
|
}
|
|
point
|
|
}
|
|
|
|
fn previous_word_end(
|
|
map: &DisplaySnapshot,
|
|
point: DisplayPoint,
|
|
ignore_punctuation: bool,
|
|
times: usize,
|
|
) -> DisplayPoint {
|
|
let classifier = map
|
|
.buffer_snapshot
|
|
.char_classifier_at(point.to_point(map))
|
|
.ignore_punctuation(ignore_punctuation);
|
|
let mut point = point.to_point(map);
|
|
|
|
if point.column < map.buffer_snapshot.line_len(MultiBufferRow(point.row)) {
|
|
point.column += 1;
|
|
}
|
|
for _ in 0..times {
|
|
let new_point = movement::find_preceding_boundary_point(
|
|
&map.buffer_snapshot,
|
|
point,
|
|
FindRange::MultiLine,
|
|
|left, right| {
|
|
let left_kind = classifier.kind(left);
|
|
let right_kind = classifier.kind(right);
|
|
match (left_kind, right_kind) {
|
|
(CharKind::Punctuation, CharKind::Whitespace)
|
|
| (CharKind::Punctuation, CharKind::Word)
|
|
| (CharKind::Word, CharKind::Whitespace)
|
|
| (CharKind::Word, CharKind::Punctuation) => true,
|
|
(CharKind::Whitespace, CharKind::Whitespace) => left == '\n' && right == '\n',
|
|
_ => false,
|
|
}
|
|
},
|
|
);
|
|
if new_point == point {
|
|
break;
|
|
}
|
|
point = new_point;
|
|
}
|
|
movement::saturating_left(map, point.to_display_point(map))
|
|
}
|
|
|
|
fn next_subword_start(
|
|
map: &DisplaySnapshot,
|
|
mut point: DisplayPoint,
|
|
ignore_punctuation: bool,
|
|
times: usize,
|
|
) -> DisplayPoint {
|
|
let classifier = map
|
|
.buffer_snapshot
|
|
.char_classifier_at(point.to_point(map))
|
|
.ignore_punctuation(ignore_punctuation);
|
|
for _ in 0..times {
|
|
let mut crossed_newline = false;
|
|
let new_point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
|
|
let left_kind = classifier.kind(left);
|
|
let right_kind = classifier.kind(right);
|
|
let at_newline = right == '\n';
|
|
|
|
let is_word_start = (left_kind != right_kind) && !left.is_alphanumeric();
|
|
let is_subword_start =
|
|
left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
|
|
|
|
let found = (!right.is_whitespace() && (is_word_start || is_subword_start))
|
|
|| at_newline && crossed_newline
|
|
|| at_newline && left == '\n'; // Prevents skipping repeated empty lines
|
|
|
|
crossed_newline |= at_newline;
|
|
found
|
|
});
|
|
if point == new_point {
|
|
break;
|
|
}
|
|
point = new_point;
|
|
}
|
|
point
|
|
}
|
|
|
|
pub(crate) fn next_subword_end(
|
|
map: &DisplaySnapshot,
|
|
mut point: DisplayPoint,
|
|
ignore_punctuation: bool,
|
|
times: usize,
|
|
allow_cross_newline: bool,
|
|
) -> DisplayPoint {
|
|
let classifier = map
|
|
.buffer_snapshot
|
|
.char_classifier_at(point.to_point(map))
|
|
.ignore_punctuation(ignore_punctuation);
|
|
for _ in 0..times {
|
|
let new_point = next_char(map, point, allow_cross_newline);
|
|
|
|
let mut crossed_newline = false;
|
|
let mut need_backtrack = false;
|
|
let new_point =
|
|
movement::find_boundary(map, new_point, FindRange::MultiLine, |left, right| {
|
|
let left_kind = classifier.kind(left);
|
|
let right_kind = classifier.kind(right);
|
|
let at_newline = right == '\n';
|
|
|
|
if !allow_cross_newline && at_newline {
|
|
return true;
|
|
}
|
|
|
|
let is_word_end = (left_kind != right_kind) && !right.is_alphanumeric();
|
|
let is_subword_end =
|
|
left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
|
|
|
|
let found = !left.is_whitespace() && !at_newline && (is_word_end || is_subword_end);
|
|
|
|
if found && (is_word_end || is_subword_end) {
|
|
need_backtrack = true;
|
|
}
|
|
|
|
crossed_newline |= at_newline;
|
|
found
|
|
});
|
|
let mut new_point = map.clip_point(new_point, Bias::Left);
|
|
if need_backtrack {
|
|
*new_point.column_mut() -= 1;
|
|
}
|
|
let new_point = map.clip_point(new_point, Bias::Left);
|
|
if point == new_point {
|
|
break;
|
|
}
|
|
point = new_point;
|
|
}
|
|
point
|
|
}
|
|
|
|
fn previous_subword_start(
|
|
map: &DisplaySnapshot,
|
|
mut point: DisplayPoint,
|
|
ignore_punctuation: bool,
|
|
times: usize,
|
|
) -> DisplayPoint {
|
|
let classifier = map
|
|
.buffer_snapshot
|
|
.char_classifier_at(point.to_point(map))
|
|
.ignore_punctuation(ignore_punctuation);
|
|
for _ in 0..times {
|
|
let mut crossed_newline = false;
|
|
// This works even though find_preceding_boundary is called for every character in the line containing
|
|
// cursor because the newline is checked only once.
|
|
let new_point = movement::find_preceding_boundary_display_point(
|
|
map,
|
|
point,
|
|
FindRange::MultiLine,
|
|
|left, right| {
|
|
let left_kind = classifier.kind(left);
|
|
let right_kind = classifier.kind(right);
|
|
let at_newline = right == '\n';
|
|
|
|
let is_word_start = (left_kind != right_kind) && !left.is_alphanumeric();
|
|
let is_subword_start =
|
|
left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
|
|
|
|
let found = (!right.is_whitespace() && (is_word_start || is_subword_start))
|
|
|| at_newline && crossed_newline
|
|
|| at_newline && left == '\n'; // Prevents skipping repeated empty lines
|
|
|
|
crossed_newline |= at_newline;
|
|
|
|
found
|
|
},
|
|
);
|
|
if point == new_point {
|
|
break;
|
|
}
|
|
point = new_point;
|
|
}
|
|
point
|
|
}
|
|
|
|
fn previous_subword_end(
|
|
map: &DisplaySnapshot,
|
|
point: DisplayPoint,
|
|
ignore_punctuation: bool,
|
|
times: usize,
|
|
) -> DisplayPoint {
|
|
let classifier = map
|
|
.buffer_snapshot
|
|
.char_classifier_at(point.to_point(map))
|
|
.ignore_punctuation(ignore_punctuation);
|
|
let mut point = point.to_point(map);
|
|
|
|
if point.column < map.buffer_snapshot.line_len(MultiBufferRow(point.row)) {
|
|
point.column += 1;
|
|
}
|
|
for _ in 0..times {
|
|
let new_point = movement::find_preceding_boundary_point(
|
|
&map.buffer_snapshot,
|
|
point,
|
|
FindRange::MultiLine,
|
|
|left, right| {
|
|
let left_kind = classifier.kind(left);
|
|
let right_kind = classifier.kind(right);
|
|
|
|
let is_subword_end =
|
|
left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
|
|
|
|
if is_subword_end {
|
|
return true;
|
|
}
|
|
|
|
match (left_kind, right_kind) {
|
|
(CharKind::Word, CharKind::Whitespace)
|
|
| (CharKind::Word, CharKind::Punctuation) => true,
|
|
(CharKind::Whitespace, CharKind::Whitespace) => left == '\n' && right == '\n',
|
|
_ => false,
|
|
}
|
|
},
|
|
);
|
|
if new_point == point {
|
|
break;
|
|
}
|
|
point = new_point;
|
|
}
|
|
movement::saturating_left(map, point.to_display_point(map))
|
|
}
|
|
|
|
pub(crate) fn first_non_whitespace(
|
|
map: &DisplaySnapshot,
|
|
display_lines: bool,
|
|
from: DisplayPoint,
|
|
) -> DisplayPoint {
|
|
let mut start_offset = start_of_line(map, display_lines, from).to_offset(map, Bias::Left);
|
|
let classifier = map.buffer_snapshot.char_classifier_at(from.to_point(map));
|
|
for (ch, offset) in map.buffer_chars_at(start_offset) {
|
|
if ch == '\n' {
|
|
return from;
|
|
}
|
|
|
|
start_offset = offset;
|
|
|
|
if classifier.kind(ch) != CharKind::Whitespace {
|
|
break;
|
|
}
|
|
}
|
|
|
|
start_offset.to_display_point(map)
|
|
}
|
|
|
|
pub(crate) fn last_non_whitespace(
|
|
map: &DisplaySnapshot,
|
|
from: DisplayPoint,
|
|
count: usize,
|
|
) -> DisplayPoint {
|
|
let mut end_of_line = end_of_line(map, false, from, count).to_offset(map, Bias::Left);
|
|
let classifier = map.buffer_snapshot.char_classifier_at(from.to_point(map));
|
|
|
|
// NOTE: depending on clip_at_line_end we may already be one char back from the end.
|
|
if let Some((ch, _)) = map.buffer_chars_at(end_of_line).next() {
|
|
if classifier.kind(ch) != CharKind::Whitespace {
|
|
return end_of_line.to_display_point(map);
|
|
}
|
|
}
|
|
|
|
for (ch, offset) in map.reverse_buffer_chars_at(end_of_line) {
|
|
if ch == '\n' {
|
|
break;
|
|
}
|
|
end_of_line = offset;
|
|
if classifier.kind(ch) != CharKind::Whitespace || ch == '\n' {
|
|
break;
|
|
}
|
|
}
|
|
|
|
end_of_line.to_display_point(map)
|
|
}
|
|
|
|
pub(crate) fn start_of_line(
|
|
map: &DisplaySnapshot,
|
|
display_lines: bool,
|
|
point: DisplayPoint,
|
|
) -> DisplayPoint {
|
|
if display_lines {
|
|
map.clip_point(DisplayPoint::new(point.row(), 0), Bias::Right)
|
|
} else {
|
|
map.prev_line_boundary(point.to_point(map)).1
|
|
}
|
|
}
|
|
|
|
pub(crate) fn end_of_line(
|
|
map: &DisplaySnapshot,
|
|
display_lines: bool,
|
|
mut point: DisplayPoint,
|
|
times: usize,
|
|
) -> DisplayPoint {
|
|
if times > 1 {
|
|
point = start_of_relative_buffer_row(map, point, times as isize - 1);
|
|
}
|
|
if display_lines {
|
|
map.clip_point(
|
|
DisplayPoint::new(point.row(), map.line_len(point.row())),
|
|
Bias::Left,
|
|
)
|
|
} else {
|
|
map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left)
|
|
}
|
|
}
|
|
|
|
pub(crate) fn sentence_backwards(
|
|
map: &DisplaySnapshot,
|
|
point: DisplayPoint,
|
|
mut times: usize,
|
|
) -> DisplayPoint {
|
|
let mut start = point.to_point(map).to_offset(&map.buffer_snapshot);
|
|
let mut chars = map.reverse_buffer_chars_at(start).peekable();
|
|
|
|
let mut was_newline = map
|
|
.buffer_chars_at(start)
|
|
.next()
|
|
.is_some_and(|(c, _)| c == '\n');
|
|
|
|
while let Some((ch, offset)) = chars.next() {
|
|
let start_of_next_sentence = if was_newline && ch == '\n' {
|
|
Some(offset + ch.len_utf8())
|
|
} else if ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n') {
|
|
Some(next_non_blank(map, offset + ch.len_utf8()))
|
|
} else if ch == '.' || ch == '?' || ch == '!' {
|
|
start_of_next_sentence(map, offset + ch.len_utf8())
|
|
} else {
|
|
None
|
|
};
|
|
|
|
if let Some(start_of_next_sentence) = start_of_next_sentence {
|
|
if start_of_next_sentence < start {
|
|
times = times.saturating_sub(1);
|
|
}
|
|
if times == 0 || offset == 0 {
|
|
return map.clip_point(
|
|
start_of_next_sentence
|
|
.to_offset(&map.buffer_snapshot)
|
|
.to_display_point(map),
|
|
Bias::Left,
|
|
);
|
|
}
|
|
}
|
|
if was_newline {
|
|
start = offset;
|
|
}
|
|
was_newline = ch == '\n';
|
|
}
|
|
|
|
DisplayPoint::zero()
|
|
}
|
|
|
|
pub(crate) fn sentence_forwards(
|
|
map: &DisplaySnapshot,
|
|
point: DisplayPoint,
|
|
mut times: usize,
|
|
) -> DisplayPoint {
|
|
let start = point.to_point(map).to_offset(&map.buffer_snapshot);
|
|
let mut chars = map.buffer_chars_at(start).peekable();
|
|
|
|
let mut was_newline = map
|
|
.reverse_buffer_chars_at(start)
|
|
.next()
|
|
.is_some_and(|(c, _)| c == '\n')
|
|
&& chars.peek().is_some_and(|(c, _)| *c == '\n');
|
|
|
|
while let Some((ch, offset)) = chars.next() {
|
|
if was_newline && ch == '\n' {
|
|
continue;
|
|
}
|
|
let start_of_next_sentence = if was_newline {
|
|
Some(next_non_blank(map, offset))
|
|
} else if ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n') {
|
|
Some(next_non_blank(map, offset + ch.len_utf8()))
|
|
} else if ch == '.' || ch == '?' || ch == '!' {
|
|
start_of_next_sentence(map, offset + ch.len_utf8())
|
|
} else {
|
|
None
|
|
};
|
|
|
|
if let Some(start_of_next_sentence) = start_of_next_sentence {
|
|
times = times.saturating_sub(1);
|
|
if times == 0 {
|
|
return map.clip_point(
|
|
start_of_next_sentence
|
|
.to_offset(&map.buffer_snapshot)
|
|
.to_display_point(map),
|
|
Bias::Right,
|
|
);
|
|
}
|
|
}
|
|
|
|
was_newline = ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n');
|
|
}
|
|
|
|
map.max_point()
|
|
}
|
|
|
|
fn next_non_blank(map: &DisplaySnapshot, start: usize) -> usize {
|
|
for (c, o) in map.buffer_chars_at(start) {
|
|
if c == '\n' || !c.is_whitespace() {
|
|
return o;
|
|
}
|
|
}
|
|
|
|
map.buffer_snapshot.len()
|
|
}
|
|
|
|
// given the offset after a ., !, or ? find the start of the next sentence.
|
|
// if this is not a sentence boundary, returns None.
|
|
fn start_of_next_sentence(map: &DisplaySnapshot, end_of_sentence: usize) -> Option<usize> {
|
|
let chars = map.buffer_chars_at(end_of_sentence);
|
|
let mut seen_space = false;
|
|
|
|
for (char, offset) in chars {
|
|
if !seen_space && (char == ')' || char == ']' || char == '"' || char == '\'') {
|
|
continue;
|
|
}
|
|
|
|
if char == '\n' && seen_space {
|
|
return Some(offset);
|
|
} else if char.is_whitespace() {
|
|
seen_space = true;
|
|
} else if seen_space {
|
|
return Some(offset);
|
|
} else {
|
|
return None;
|
|
}
|
|
}
|
|
|
|
Some(map.buffer_snapshot.len())
|
|
}
|
|
|
|
fn go_to_line(map: &DisplaySnapshot, display_point: DisplayPoint, line: usize) -> DisplayPoint {
|
|
let point = map.display_point_to_point(display_point, Bias::Left);
|
|
let Some(mut excerpt) = map.buffer_snapshot.excerpt_containing(point..point) else {
|
|
return display_point;
|
|
};
|
|
let offset = excerpt.buffer().point_to_offset(
|
|
excerpt
|
|
.buffer()
|
|
.clip_point(Point::new((line - 1) as u32, point.column), Bias::Left),
|
|
);
|
|
let buffer_range = excerpt.buffer_range();
|
|
if offset >= buffer_range.start && offset <= buffer_range.end {
|
|
let point = map
|
|
.buffer_snapshot
|
|
.offset_to_point(excerpt.map_offset_from_buffer(offset));
|
|
return map.clip_point(map.point_to_display_point(point, Bias::Left), Bias::Left);
|
|
}
|
|
let mut last_position = None;
|
|
for (excerpt, buffer, range) in map.buffer_snapshot.excerpts() {
|
|
let excerpt_range = language::ToOffset::to_offset(&range.context.start, &buffer)
|
|
..language::ToOffset::to_offset(&range.context.end, &buffer);
|
|
if offset >= excerpt_range.start && offset <= excerpt_range.end {
|
|
let text_anchor = buffer.anchor_after(offset);
|
|
let anchor = Anchor::in_buffer(excerpt, buffer.remote_id(), text_anchor);
|
|
return anchor.to_display_point(map);
|
|
} else if offset <= excerpt_range.start {
|
|
let anchor = Anchor::in_buffer(excerpt, buffer.remote_id(), range.context.start);
|
|
return anchor.to_display_point(map);
|
|
} else {
|
|
last_position = Some(Anchor::in_buffer(
|
|
excerpt,
|
|
buffer.remote_id(),
|
|
range.context.end,
|
|
));
|
|
}
|
|
}
|
|
|
|
let mut last_point = last_position.unwrap().to_point(&map.buffer_snapshot);
|
|
last_point.column = point.column;
|
|
|
|
map.clip_point(
|
|
map.point_to_display_point(
|
|
map.buffer_snapshot.clip_point(point, Bias::Left),
|
|
Bias::Left,
|
|
),
|
|
Bias::Left,
|
|
)
|
|
}
|
|
|
|
fn start_of_document(
|
|
map: &DisplaySnapshot,
|
|
display_point: DisplayPoint,
|
|
maybe_times: Option<usize>,
|
|
) -> DisplayPoint {
|
|
if let Some(times) = maybe_times {
|
|
return go_to_line(map, display_point, times);
|
|
}
|
|
|
|
let point = map.display_point_to_point(display_point, Bias::Left);
|
|
let mut first_point = Point::zero();
|
|
first_point.column = point.column;
|
|
|
|
map.clip_point(
|
|
map.point_to_display_point(
|
|
map.buffer_snapshot.clip_point(first_point, Bias::Left),
|
|
Bias::Left,
|
|
),
|
|
Bias::Left,
|
|
)
|
|
}
|
|
|
|
fn end_of_document(
|
|
map: &DisplaySnapshot,
|
|
display_point: DisplayPoint,
|
|
maybe_times: Option<usize>,
|
|
) -> DisplayPoint {
|
|
if let Some(times) = maybe_times {
|
|
return go_to_line(map, display_point, times);
|
|
};
|
|
let point = map.display_point_to_point(display_point, Bias::Left);
|
|
let mut last_point = map.buffer_snapshot.max_point();
|
|
last_point.column = point.column;
|
|
|
|
map.clip_point(
|
|
map.point_to_display_point(
|
|
map.buffer_snapshot.clip_point(last_point, Bias::Left),
|
|
Bias::Left,
|
|
),
|
|
Bias::Left,
|
|
)
|
|
}
|
|
|
|
fn matching_tag(map: &DisplaySnapshot, head: DisplayPoint) -> Option<DisplayPoint> {
|
|
let inner = crate::object::surrounding_html_tag(map, head, head..head, false)?;
|
|
let outer = crate::object::surrounding_html_tag(map, head, head..head, true)?;
|
|
|
|
if head > outer.start && head < inner.start {
|
|
let mut offset = inner.end.to_offset(map, Bias::Left);
|
|
for c in map.buffer_snapshot.chars_at(offset) {
|
|
if c == '/' || c == '\n' || c == '>' {
|
|
return Some(offset.to_display_point(map));
|
|
}
|
|
offset += c.len_utf8();
|
|
}
|
|
} else {
|
|
let mut offset = outer.start.to_offset(map, Bias::Left);
|
|
for c in map.buffer_snapshot.chars_at(offset) {
|
|
offset += c.len_utf8();
|
|
if c == '<' || c == '\n' {
|
|
return Some(offset.to_display_point(map));
|
|
}
|
|
}
|
|
}
|
|
|
|
return None;
|
|
}
|
|
|
|
fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
|
|
// https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1200
|
|
let display_point = map.clip_at_line_end(display_point);
|
|
let point = display_point.to_point(map);
|
|
let offset = point.to_offset(&map.buffer_snapshot);
|
|
|
|
// Ensure the range is contained by the current line.
|
|
let mut line_end = map.next_line_boundary(point).0;
|
|
if line_end == point {
|
|
line_end = map.max_point().to_point(map);
|
|
}
|
|
|
|
let line_range = map.prev_line_boundary(point).0..line_end;
|
|
let visible_line_range =
|
|
line_range.start..Point::new(line_range.end.row, line_range.end.column.saturating_sub(1));
|
|
let ranges = map
|
|
.buffer_snapshot
|
|
.bracket_ranges(visible_line_range.clone());
|
|
if let Some(ranges) = ranges {
|
|
let line_range = line_range.start.to_offset(&map.buffer_snapshot)
|
|
..line_range.end.to_offset(&map.buffer_snapshot);
|
|
let mut closest_pair_destination = None;
|
|
let mut closest_distance = usize::MAX;
|
|
|
|
for (open_range, close_range) in ranges {
|
|
if map.buffer_snapshot.chars_at(open_range.start).next() == Some('<') {
|
|
if offset > open_range.start && offset < close_range.start {
|
|
let mut chars = map.buffer_snapshot.chars_at(close_range.start);
|
|
if (Some('/'), Some('>')) == (chars.next(), chars.next()) {
|
|
return display_point;
|
|
}
|
|
if let Some(tag) = matching_tag(map, display_point) {
|
|
return tag;
|
|
}
|
|
} else if close_range.contains(&offset) {
|
|
return open_range.start.to_display_point(map);
|
|
} else if open_range.contains(&offset) {
|
|
return (close_range.end - 1).to_display_point(map);
|
|
}
|
|
}
|
|
|
|
if (open_range.contains(&offset) || open_range.start >= offset)
|
|
&& line_range.contains(&open_range.start)
|
|
{
|
|
let distance = open_range.start.saturating_sub(offset);
|
|
if distance < closest_distance {
|
|
closest_pair_destination = Some(close_range.start);
|
|
closest_distance = distance;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (close_range.contains(&offset) || close_range.start >= offset)
|
|
&& line_range.contains(&close_range.start)
|
|
{
|
|
let distance = close_range.start.saturating_sub(offset);
|
|
if distance < closest_distance {
|
|
closest_pair_destination = Some(open_range.start);
|
|
closest_distance = distance;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
closest_pair_destination
|
|
.map(|destination| destination.to_display_point(map))
|
|
.unwrap_or(display_point)
|
|
} else {
|
|
display_point
|
|
}
|
|
}
|
|
|
|
// Go to {count} percentage in the file, on the first
|
|
// non-blank in the line linewise. To compute the new
|
|
// line number this formula is used:
|
|
// ({count} * number-of-lines + 99) / 100
|
|
//
|
|
// https://neovim.io/doc/user/motion.html#N%25
|
|
fn go_to_percentage(map: &DisplaySnapshot, point: DisplayPoint, count: usize) -> DisplayPoint {
|
|
let total_lines = map.buffer_snapshot.max_point().row + 1;
|
|
let target_line = (count * total_lines as usize + 99) / 100;
|
|
let target_point = DisplayPoint::new(
|
|
DisplayRow(target_line.saturating_sub(1) as u32),
|
|
point.column(),
|
|
);
|
|
map.clip_point(target_point, Bias::Left)
|
|
}
|
|
|
|
fn unmatched_forward(
|
|
map: &DisplaySnapshot,
|
|
mut display_point: DisplayPoint,
|
|
char: char,
|
|
times: usize,
|
|
) -> DisplayPoint {
|
|
for _ in 0..times {
|
|
// https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1245
|
|
let point = display_point.to_point(map);
|
|
let offset = point.to_offset(&map.buffer_snapshot);
|
|
|
|
let ranges = map.buffer_snapshot.enclosing_bracket_ranges(point..point);
|
|
let Some(ranges) = ranges else { break };
|
|
let mut closest_closing_destination = None;
|
|
let mut closest_distance = usize::MAX;
|
|
|
|
for (_, close_range) in ranges {
|
|
if close_range.start > offset {
|
|
let mut chars = map.buffer_snapshot.chars_at(close_range.start);
|
|
if Some(char) == chars.next() {
|
|
let distance = close_range.start - offset;
|
|
if distance < closest_distance {
|
|
closest_closing_destination = Some(close_range.start);
|
|
closest_distance = distance;
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
let new_point = closest_closing_destination
|
|
.map(|destination| destination.to_display_point(map))
|
|
.unwrap_or(display_point);
|
|
if new_point == display_point {
|
|
break;
|
|
}
|
|
display_point = new_point;
|
|
}
|
|
return display_point;
|
|
}
|
|
|
|
fn unmatched_backward(
|
|
map: &DisplaySnapshot,
|
|
mut display_point: DisplayPoint,
|
|
char: char,
|
|
times: usize,
|
|
) -> DisplayPoint {
|
|
for _ in 0..times {
|
|
// https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1239
|
|
let point = display_point.to_point(map);
|
|
let offset = point.to_offset(&map.buffer_snapshot);
|
|
|
|
let ranges = map.buffer_snapshot.enclosing_bracket_ranges(point..point);
|
|
let Some(ranges) = ranges else {
|
|
break;
|
|
};
|
|
|
|
let mut closest_starting_destination = None;
|
|
let mut closest_distance = usize::MAX;
|
|
|
|
for (start_range, _) in ranges {
|
|
if start_range.start < offset {
|
|
let mut chars = map.buffer_snapshot.chars_at(start_range.start);
|
|
if Some(char) == chars.next() {
|
|
let distance = offset - start_range.start;
|
|
if distance < closest_distance {
|
|
closest_starting_destination = Some(start_range.start);
|
|
closest_distance = distance;
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
let new_point = closest_starting_destination
|
|
.map(|destination| destination.to_display_point(map))
|
|
.unwrap_or(display_point);
|
|
if new_point == display_point {
|
|
break;
|
|
} else {
|
|
display_point = new_point;
|
|
}
|
|
}
|
|
display_point
|
|
}
|
|
|
|
fn find_forward(
|
|
map: &DisplaySnapshot,
|
|
from: DisplayPoint,
|
|
before: bool,
|
|
target: char,
|
|
times: usize,
|
|
mode: FindRange,
|
|
smartcase: bool,
|
|
) -> Option<DisplayPoint> {
|
|
let mut to = from;
|
|
let mut found = false;
|
|
|
|
for _ in 0..times {
|
|
found = false;
|
|
let new_to = find_boundary(map, to, mode, |_, right| {
|
|
found = is_character_match(target, right, smartcase);
|
|
found
|
|
});
|
|
if to == new_to {
|
|
break;
|
|
}
|
|
to = new_to;
|
|
}
|
|
|
|
if found {
|
|
if before && to.column() > 0 {
|
|
*to.column_mut() -= 1;
|
|
Some(map.clip_point(to, Bias::Left))
|
|
} else {
|
|
Some(to)
|
|
}
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
fn find_backward(
|
|
map: &DisplaySnapshot,
|
|
from: DisplayPoint,
|
|
after: bool,
|
|
target: char,
|
|
times: usize,
|
|
mode: FindRange,
|
|
smartcase: bool,
|
|
) -> DisplayPoint {
|
|
let mut to = from;
|
|
|
|
for _ in 0..times {
|
|
let new_to = find_preceding_boundary_display_point(map, to, mode, |_, right| {
|
|
is_character_match(target, right, smartcase)
|
|
});
|
|
if to == new_to {
|
|
break;
|
|
}
|
|
to = new_to;
|
|
}
|
|
|
|
let next = map.buffer_snapshot.chars_at(to.to_point(map)).next();
|
|
if next.is_some() && is_character_match(target, next.unwrap(), smartcase) {
|
|
if after {
|
|
*to.column_mut() += 1;
|
|
map.clip_point(to, Bias::Right)
|
|
} else {
|
|
to
|
|
}
|
|
} else {
|
|
from
|
|
}
|
|
}
|
|
|
|
fn is_character_match(target: char, other: char, smartcase: bool) -> bool {
|
|
if smartcase {
|
|
if target.is_uppercase() {
|
|
target == other
|
|
} else {
|
|
target == other.to_ascii_lowercase()
|
|
}
|
|
} else {
|
|
target == other
|
|
}
|
|
}
|
|
|
|
fn sneak(
|
|
map: &DisplaySnapshot,
|
|
from: DisplayPoint,
|
|
first_target: char,
|
|
second_target: char,
|
|
times: usize,
|
|
smartcase: bool,
|
|
) -> Option<DisplayPoint> {
|
|
let mut to = from;
|
|
let mut found = false;
|
|
|
|
for _ in 0..times {
|
|
found = false;
|
|
let new_to = find_boundary(
|
|
map,
|
|
movement::right(map, to),
|
|
FindRange::MultiLine,
|
|
|left, right| {
|
|
found = is_character_match(first_target, left, smartcase)
|
|
&& is_character_match(second_target, right, smartcase);
|
|
found
|
|
},
|
|
);
|
|
if to == new_to {
|
|
break;
|
|
}
|
|
to = new_to;
|
|
}
|
|
|
|
if found {
|
|
Some(movement::left(map, to))
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
fn sneak_backward(
|
|
map: &DisplaySnapshot,
|
|
from: DisplayPoint,
|
|
first_target: char,
|
|
second_target: char,
|
|
times: usize,
|
|
smartcase: bool,
|
|
) -> Option<DisplayPoint> {
|
|
let mut to = from;
|
|
let mut found = false;
|
|
|
|
for _ in 0..times {
|
|
found = false;
|
|
let new_to =
|
|
find_preceding_boundary_display_point(map, to, FindRange::MultiLine, |left, right| {
|
|
found = is_character_match(first_target, left, smartcase)
|
|
&& is_character_match(second_target, right, smartcase);
|
|
found
|
|
});
|
|
if to == new_to {
|
|
break;
|
|
}
|
|
to = new_to;
|
|
}
|
|
|
|
if found {
|
|
Some(movement::left(map, to))
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
|
|
let correct_line = start_of_relative_buffer_row(map, point, times as isize);
|
|
first_non_whitespace(map, false, correct_line)
|
|
}
|
|
|
|
fn previous_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
|
|
let correct_line = start_of_relative_buffer_row(map, point, -(times as isize));
|
|
first_non_whitespace(map, false, correct_line)
|
|
}
|
|
|
|
fn go_to_column(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
|
|
let correct_line = start_of_relative_buffer_row(map, point, 0);
|
|
right(map, correct_line, times.saturating_sub(1))
|
|
}
|
|
|
|
pub(crate) fn next_line_end(
|
|
map: &DisplaySnapshot,
|
|
mut point: DisplayPoint,
|
|
times: usize,
|
|
) -> DisplayPoint {
|
|
if times > 1 {
|
|
point = start_of_relative_buffer_row(map, point, times as isize - 1);
|
|
}
|
|
end_of_line(map, false, point, 1)
|
|
}
|
|
|
|
fn window_top(
|
|
map: &DisplaySnapshot,
|
|
point: DisplayPoint,
|
|
text_layout_details: &TextLayoutDetails,
|
|
mut times: usize,
|
|
) -> (DisplayPoint, SelectionGoal) {
|
|
let first_visible_line = text_layout_details
|
|
.scroll_anchor
|
|
.anchor
|
|
.to_display_point(map);
|
|
|
|
if first_visible_line.row() != DisplayRow(0)
|
|
&& text_layout_details.vertical_scroll_margin as usize > times
|
|
{
|
|
times = text_layout_details.vertical_scroll_margin.ceil() as usize;
|
|
}
|
|
|
|
if let Some(visible_rows) = text_layout_details.visible_rows {
|
|
let bottom_row = first_visible_line.row().0 + visible_rows as u32;
|
|
let new_row = (first_visible_line.row().0 + (times as u32))
|
|
.min(bottom_row)
|
|
.min(map.max_point().row().0);
|
|
let new_col = point.column().min(map.line_len(first_visible_line.row()));
|
|
|
|
let new_point = DisplayPoint::new(DisplayRow(new_row), new_col);
|
|
(map.clip_point(new_point, Bias::Left), SelectionGoal::None)
|
|
} else {
|
|
let new_row =
|
|
DisplayRow((first_visible_line.row().0 + (times as u32)).min(map.max_point().row().0));
|
|
let new_col = point.column().min(map.line_len(first_visible_line.row()));
|
|
|
|
let new_point = DisplayPoint::new(new_row, new_col);
|
|
(map.clip_point(new_point, Bias::Left), SelectionGoal::None)
|
|
}
|
|
}
|
|
|
|
fn window_middle(
|
|
map: &DisplaySnapshot,
|
|
point: DisplayPoint,
|
|
text_layout_details: &TextLayoutDetails,
|
|
) -> (DisplayPoint, SelectionGoal) {
|
|
if let Some(visible_rows) = text_layout_details.visible_rows {
|
|
let first_visible_line = text_layout_details
|
|
.scroll_anchor
|
|
.anchor
|
|
.to_display_point(map);
|
|
|
|
let max_visible_rows =
|
|
(visible_rows as u32).min(map.max_point().row().0 - first_visible_line.row().0);
|
|
|
|
let new_row =
|
|
(first_visible_line.row().0 + (max_visible_rows / 2)).min(map.max_point().row().0);
|
|
let new_row = DisplayRow(new_row);
|
|
let new_col = point.column().min(map.line_len(new_row));
|
|
let new_point = DisplayPoint::new(new_row, new_col);
|
|
(map.clip_point(new_point, Bias::Left), SelectionGoal::None)
|
|
} else {
|
|
(point, SelectionGoal::None)
|
|
}
|
|
}
|
|
|
|
fn window_bottom(
|
|
map: &DisplaySnapshot,
|
|
point: DisplayPoint,
|
|
text_layout_details: &TextLayoutDetails,
|
|
mut times: usize,
|
|
) -> (DisplayPoint, SelectionGoal) {
|
|
if let Some(visible_rows) = text_layout_details.visible_rows {
|
|
let first_visible_line = text_layout_details
|
|
.scroll_anchor
|
|
.anchor
|
|
.to_display_point(map);
|
|
let bottom_row = first_visible_line.row().0
|
|
+ (visible_rows + text_layout_details.scroll_anchor.offset.y - 1.).floor() as u32;
|
|
if bottom_row < map.max_point().row().0
|
|
&& text_layout_details.vertical_scroll_margin as usize > times
|
|
{
|
|
times = text_layout_details.vertical_scroll_margin.ceil() as usize;
|
|
}
|
|
let bottom_row_capped = bottom_row.min(map.max_point().row().0);
|
|
let new_row = if bottom_row_capped.saturating_sub(times as u32) < first_visible_line.row().0
|
|
{
|
|
first_visible_line.row()
|
|
} else {
|
|
DisplayRow(bottom_row_capped.saturating_sub(times as u32))
|
|
};
|
|
let new_col = point.column().min(map.line_len(new_row));
|
|
let new_point = DisplayPoint::new(new_row, new_col);
|
|
(map.clip_point(new_point, Bias::Left), SelectionGoal::None)
|
|
} else {
|
|
(point, SelectionGoal::None)
|
|
}
|
|
}
|
|
|
|
fn method_motion(
|
|
map: &DisplaySnapshot,
|
|
mut display_point: DisplayPoint,
|
|
times: usize,
|
|
direction: Direction,
|
|
is_start: bool,
|
|
) -> DisplayPoint {
|
|
let Some((_, _, buffer)) = map.buffer_snapshot.as_singleton() else {
|
|
return display_point;
|
|
};
|
|
|
|
for _ in 0..times {
|
|
let point = map.display_point_to_point(display_point, Bias::Left);
|
|
let offset = point.to_offset(&map.buffer_snapshot);
|
|
let range = if direction == Direction::Prev {
|
|
0..offset
|
|
} else {
|
|
offset..buffer.len()
|
|
};
|
|
|
|
let possibilities = buffer
|
|
.text_object_ranges(range, language::TreeSitterOptions::max_start_depth(4))
|
|
.filter_map(|(range, object)| {
|
|
if !matches!(object, language::TextObject::AroundFunction) {
|
|
return None;
|
|
}
|
|
|
|
let relevant = if is_start { range.start } else { range.end };
|
|
if direction == Direction::Prev && relevant < offset {
|
|
Some(relevant)
|
|
} else if direction == Direction::Next && relevant > offset + 1 {
|
|
Some(relevant)
|
|
} else {
|
|
None
|
|
}
|
|
});
|
|
|
|
let dest = if direction == Direction::Prev {
|
|
possibilities.max().unwrap_or(offset)
|
|
} else {
|
|
possibilities.min().unwrap_or(offset)
|
|
};
|
|
let new_point = map.clip_point(dest.to_display_point(&map), Bias::Left);
|
|
if new_point == display_point {
|
|
break;
|
|
}
|
|
display_point = new_point;
|
|
}
|
|
display_point
|
|
}
|
|
|
|
fn comment_motion(
|
|
map: &DisplaySnapshot,
|
|
mut display_point: DisplayPoint,
|
|
times: usize,
|
|
direction: Direction,
|
|
) -> DisplayPoint {
|
|
let Some((_, _, buffer)) = map.buffer_snapshot.as_singleton() else {
|
|
return display_point;
|
|
};
|
|
|
|
for _ in 0..times {
|
|
let point = map.display_point_to_point(display_point, Bias::Left);
|
|
let offset = point.to_offset(&map.buffer_snapshot);
|
|
let range = if direction == Direction::Prev {
|
|
0..offset
|
|
} else {
|
|
offset..buffer.len()
|
|
};
|
|
|
|
let possibilities = buffer
|
|
.text_object_ranges(range, language::TreeSitterOptions::max_start_depth(6))
|
|
.filter_map(|(range, object)| {
|
|
if !matches!(object, language::TextObject::AroundComment) {
|
|
return None;
|
|
}
|
|
|
|
let relevant = if direction == Direction::Prev {
|
|
range.start
|
|
} else {
|
|
range.end
|
|
};
|
|
if direction == Direction::Prev && relevant < offset {
|
|
Some(relevant)
|
|
} else if direction == Direction::Next && relevant > offset + 1 {
|
|
Some(relevant)
|
|
} else {
|
|
None
|
|
}
|
|
});
|
|
|
|
let dest = if direction == Direction::Prev {
|
|
possibilities.max().unwrap_or(offset)
|
|
} else {
|
|
possibilities.min().unwrap_or(offset)
|
|
};
|
|
let new_point = map.clip_point(dest.to_display_point(&map), Bias::Left);
|
|
if new_point == display_point {
|
|
break;
|
|
}
|
|
display_point = new_point;
|
|
}
|
|
|
|
display_point
|
|
}
|
|
|
|
fn section_motion(
|
|
map: &DisplaySnapshot,
|
|
mut display_point: DisplayPoint,
|
|
times: usize,
|
|
direction: Direction,
|
|
is_start: bool,
|
|
) -> DisplayPoint {
|
|
if map.buffer_snapshot.as_singleton().is_some() {
|
|
for _ in 0..times {
|
|
let offset = map
|
|
.display_point_to_point(display_point, Bias::Left)
|
|
.to_offset(&map.buffer_snapshot);
|
|
let range = if direction == Direction::Prev {
|
|
0..offset
|
|
} else {
|
|
offset..map.buffer_snapshot.len()
|
|
};
|
|
|
|
// we set a max start depth here because we want a section to only be "top level"
|
|
// similar to vim's default of '{' in the first column.
|
|
// (and without it, ]] at the start of editor.rs is -very- slow)
|
|
let mut possibilities = map
|
|
.buffer_snapshot
|
|
.text_object_ranges(range, language::TreeSitterOptions::max_start_depth(3))
|
|
.filter(|(_, object)| {
|
|
matches!(
|
|
object,
|
|
language::TextObject::AroundClass | language::TextObject::AroundFunction
|
|
)
|
|
})
|
|
.collect::<Vec<_>>();
|
|
possibilities.sort_by_key(|(range_a, _)| range_a.start);
|
|
let mut prev_end = None;
|
|
let possibilities = possibilities.into_iter().filter_map(|(range, t)| {
|
|
if t == language::TextObject::AroundFunction
|
|
&& prev_end.is_some_and(|prev_end| prev_end > range.start)
|
|
{
|
|
return None;
|
|
}
|
|
prev_end = Some(range.end);
|
|
|
|
let relevant = if is_start { range.start } else { range.end };
|
|
if direction == Direction::Prev && relevant < offset {
|
|
Some(relevant)
|
|
} else if direction == Direction::Next && relevant > offset + 1 {
|
|
Some(relevant)
|
|
} else {
|
|
None
|
|
}
|
|
});
|
|
|
|
let offset = if direction == Direction::Prev {
|
|
possibilities.max().unwrap_or(0)
|
|
} else {
|
|
possibilities.min().unwrap_or(map.buffer_snapshot.len())
|
|
};
|
|
|
|
let new_point = map.clip_point(offset.to_display_point(&map), Bias::Left);
|
|
if new_point == display_point {
|
|
break;
|
|
}
|
|
display_point = new_point;
|
|
}
|
|
return display_point;
|
|
};
|
|
|
|
for _ in 0..times {
|
|
let next_point = if is_start {
|
|
movement::start_of_excerpt(map, display_point, direction)
|
|
} else {
|
|
movement::end_of_excerpt(map, display_point, direction)
|
|
};
|
|
if next_point == display_point {
|
|
break;
|
|
}
|
|
display_point = next_point;
|
|
}
|
|
|
|
display_point
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
|
|
use crate::{
|
|
state::Mode,
|
|
test::{NeovimBackedTestContext, VimTestContext},
|
|
};
|
|
use editor::display_map::Inlay;
|
|
use indoc::indoc;
|
|
use language::Point;
|
|
use multi_buffer::MultiBufferRow;
|
|
|
|
#[gpui::test]
|
|
async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
|
|
|
let initial_state = indoc! {r"ˇabc
|
|
def
|
|
|
|
paragraph
|
|
the second
|
|
|
|
|
|
|
|
third and
|
|
final"};
|
|
|
|
// goes down once
|
|
cx.set_shared_state(initial_state).await;
|
|
cx.simulate_shared_keystrokes("}").await;
|
|
cx.shared_state().await.assert_eq(indoc! {r"abc
|
|
def
|
|
ˇ
|
|
paragraph
|
|
the second
|
|
|
|
|
|
|
|
third and
|
|
final"});
|
|
|
|
// goes up once
|
|
cx.simulate_shared_keystrokes("{").await;
|
|
cx.shared_state().await.assert_eq(initial_state);
|
|
|
|
// goes down twice
|
|
cx.simulate_shared_keystrokes("2 }").await;
|
|
cx.shared_state().await.assert_eq(indoc! {r"abc
|
|
def
|
|
|
|
paragraph
|
|
the second
|
|
ˇ
|
|
|
|
|
|
third and
|
|
final"});
|
|
|
|
// goes down over multiple blanks
|
|
cx.simulate_shared_keystrokes("}").await;
|
|
cx.shared_state().await.assert_eq(indoc! {r"abc
|
|
def
|
|
|
|
paragraph
|
|
the second
|
|
|
|
|
|
|
|
third and
|
|
finaˇl"});
|
|
|
|
// goes up twice
|
|
cx.simulate_shared_keystrokes("2 {").await;
|
|
cx.shared_state().await.assert_eq(indoc! {r"abc
|
|
def
|
|
ˇ
|
|
paragraph
|
|
the second
|
|
|
|
|
|
|
|
third and
|
|
final"});
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_matching(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
|
|
|
cx.set_shared_state(indoc! {r"func ˇ(a string) {
|
|
do(something(with<Types>.and_arrays[0, 2]))
|
|
}"})
|
|
.await;
|
|
cx.simulate_shared_keystrokes("%").await;
|
|
cx.shared_state()
|
|
.await
|
|
.assert_eq(indoc! {r"func (a stringˇ) {
|
|
do(something(with<Types>.and_arrays[0, 2]))
|
|
}"});
|
|
|
|
// test it works on the last character of the line
|
|
cx.set_shared_state(indoc! {r"func (a string) ˇ{
|
|
do(something(with<Types>.and_arrays[0, 2]))
|
|
}"})
|
|
.await;
|
|
cx.simulate_shared_keystrokes("%").await;
|
|
cx.shared_state()
|
|
.await
|
|
.assert_eq(indoc! {r"func (a string) {
|
|
do(something(with<Types>.and_arrays[0, 2]))
|
|
ˇ}"});
|
|
|
|
// test it works on immediate nesting
|
|
cx.set_shared_state("ˇ{()}").await;
|
|
cx.simulate_shared_keystrokes("%").await;
|
|
cx.shared_state().await.assert_eq("{()ˇ}");
|
|
cx.simulate_shared_keystrokes("%").await;
|
|
cx.shared_state().await.assert_eq("ˇ{()}");
|
|
|
|
// test it works on immediate nesting inside braces
|
|
cx.set_shared_state("{\n ˇ{()}\n}").await;
|
|
cx.simulate_shared_keystrokes("%").await;
|
|
cx.shared_state().await.assert_eq("{\n {()ˇ}\n}");
|
|
|
|
// test it jumps to the next paren on a line
|
|
cx.set_shared_state("func ˇboop() {\n}").await;
|
|
cx.simulate_shared_keystrokes("%").await;
|
|
cx.shared_state().await.assert_eq("func boop(ˇ) {\n}");
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_unmatched_forward(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
|
|
|
// test it works with curly braces
|
|
cx.set_shared_state(indoc! {r"func (a string) {
|
|
do(something(with<Types>.anˇd_arrays[0, 2]))
|
|
}"})
|
|
.await;
|
|
cx.simulate_shared_keystrokes("] }").await;
|
|
cx.shared_state()
|
|
.await
|
|
.assert_eq(indoc! {r"func (a string) {
|
|
do(something(with<Types>.and_arrays[0, 2]))
|
|
ˇ}"});
|
|
|
|
// test it works with brackets
|
|
cx.set_shared_state(indoc! {r"func (a string) {
|
|
do(somethiˇng(with<Types>.and_arrays[0, 2]))
|
|
}"})
|
|
.await;
|
|
cx.simulate_shared_keystrokes("] )").await;
|
|
cx.shared_state()
|
|
.await
|
|
.assert_eq(indoc! {r"func (a string) {
|
|
do(something(with<Types>.and_arrays[0, 2])ˇ)
|
|
}"});
|
|
|
|
cx.set_shared_state(indoc! {r"func (a string) { a((b, cˇ))}"})
|
|
.await;
|
|
cx.simulate_shared_keystrokes("] )").await;
|
|
cx.shared_state()
|
|
.await
|
|
.assert_eq(indoc! {r"func (a string) { a((b, c)ˇ)}"});
|
|
|
|
// test it works on immediate nesting
|
|
cx.set_shared_state("{ˇ {}{}}").await;
|
|
cx.simulate_shared_keystrokes("] }").await;
|
|
cx.shared_state().await.assert_eq("{ {}{}ˇ}");
|
|
cx.set_shared_state("(ˇ ()())").await;
|
|
cx.simulate_shared_keystrokes("] )").await;
|
|
cx.shared_state().await.assert_eq("( ()()ˇ)");
|
|
|
|
// test it works on immediate nesting inside braces
|
|
cx.set_shared_state("{\n ˇ {()}\n}").await;
|
|
cx.simulate_shared_keystrokes("] }").await;
|
|
cx.shared_state().await.assert_eq("{\n {()}\nˇ}");
|
|
cx.set_shared_state("(\n ˇ {()}\n)").await;
|
|
cx.simulate_shared_keystrokes("] )").await;
|
|
cx.shared_state().await.assert_eq("(\n {()}\nˇ)");
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_unmatched_backward(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
|
|
|
// test it works with curly braces
|
|
cx.set_shared_state(indoc! {r"func (a string) {
|
|
do(something(with<Types>.anˇd_arrays[0, 2]))
|
|
}"})
|
|
.await;
|
|
cx.simulate_shared_keystrokes("[ {").await;
|
|
cx.shared_state()
|
|
.await
|
|
.assert_eq(indoc! {r"func (a string) ˇ{
|
|
do(something(with<Types>.and_arrays[0, 2]))
|
|
}"});
|
|
|
|
// test it works with brackets
|
|
cx.set_shared_state(indoc! {r"func (a string) {
|
|
do(somethiˇng(with<Types>.and_arrays[0, 2]))
|
|
}"})
|
|
.await;
|
|
cx.simulate_shared_keystrokes("[ (").await;
|
|
cx.shared_state()
|
|
.await
|
|
.assert_eq(indoc! {r"func (a string) {
|
|
doˇ(something(with<Types>.and_arrays[0, 2]))
|
|
}"});
|
|
|
|
// test it works on immediate nesting
|
|
cx.set_shared_state("{{}{} ˇ }").await;
|
|
cx.simulate_shared_keystrokes("[ {").await;
|
|
cx.shared_state().await.assert_eq("ˇ{{}{} }");
|
|
cx.set_shared_state("(()() ˇ )").await;
|
|
cx.simulate_shared_keystrokes("[ (").await;
|
|
cx.shared_state().await.assert_eq("ˇ(()() )");
|
|
|
|
// test it works on immediate nesting inside braces
|
|
cx.set_shared_state("{\n {()} ˇ\n}").await;
|
|
cx.simulate_shared_keystrokes("[ {").await;
|
|
cx.shared_state().await.assert_eq("ˇ{\n {()} \n}");
|
|
cx.set_shared_state("(\n {()} ˇ\n)").await;
|
|
cx.simulate_shared_keystrokes("[ (").await;
|
|
cx.shared_state().await.assert_eq("ˇ(\n {()} \n)");
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_matching_tags(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = NeovimBackedTestContext::new_html(cx).await;
|
|
|
|
cx.neovim.exec("set filetype=html").await;
|
|
|
|
cx.set_shared_state(indoc! {r"<bˇody></body>"}).await;
|
|
cx.simulate_shared_keystrokes("%").await;
|
|
cx.shared_state()
|
|
.await
|
|
.assert_eq(indoc! {r"<body><ˇ/body>"});
|
|
cx.simulate_shared_keystrokes("%").await;
|
|
|
|
// test jumping backwards
|
|
cx.shared_state()
|
|
.await
|
|
.assert_eq(indoc! {r"<ˇbody></body>"});
|
|
|
|
// test self-closing tags
|
|
cx.set_shared_state(indoc! {r"<a><bˇr/></a>"}).await;
|
|
cx.simulate_shared_keystrokes("%").await;
|
|
cx.shared_state().await.assert_eq(indoc! {r"<a><bˇr/></a>"});
|
|
|
|
// test tag with attributes
|
|
cx.set_shared_state(indoc! {r"<div class='test' ˇid='main'>
|
|
</div>
|
|
"})
|
|
.await;
|
|
cx.simulate_shared_keystrokes("%").await;
|
|
cx.shared_state()
|
|
.await
|
|
.assert_eq(indoc! {r"<div class='test' id='main'>
|
|
<ˇ/div>
|
|
"});
|
|
|
|
// test multi-line self-closing tag
|
|
cx.set_shared_state(indoc! {r#"<a>
|
|
<br
|
|
test = "test"
|
|
/ˇ>
|
|
</a>"#})
|
|
.await;
|
|
cx.simulate_shared_keystrokes("%").await;
|
|
cx.shared_state().await.assert_eq(indoc! {r#"<a>
|
|
ˇ<br
|
|
test = "test"
|
|
/>
|
|
</a>"#});
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
|
|
|
// f and F
|
|
cx.set_shared_state("ˇone two three four").await;
|
|
cx.simulate_shared_keystrokes("f o").await;
|
|
cx.shared_state().await.assert_eq("one twˇo three four");
|
|
cx.simulate_shared_keystrokes(",").await;
|
|
cx.shared_state().await.assert_eq("ˇone two three four");
|
|
cx.simulate_shared_keystrokes("2 ;").await;
|
|
cx.shared_state().await.assert_eq("one two three fˇour");
|
|
cx.simulate_shared_keystrokes("shift-f e").await;
|
|
cx.shared_state().await.assert_eq("one two threˇe four");
|
|
cx.simulate_shared_keystrokes("2 ;").await;
|
|
cx.shared_state().await.assert_eq("onˇe two three four");
|
|
cx.simulate_shared_keystrokes(",").await;
|
|
cx.shared_state().await.assert_eq("one two thrˇee four");
|
|
|
|
// t and T
|
|
cx.set_shared_state("ˇone two three four").await;
|
|
cx.simulate_shared_keystrokes("t o").await;
|
|
cx.shared_state().await.assert_eq("one tˇwo three four");
|
|
cx.simulate_shared_keystrokes(",").await;
|
|
cx.shared_state().await.assert_eq("oˇne two three four");
|
|
cx.simulate_shared_keystrokes("2 ;").await;
|
|
cx.shared_state().await.assert_eq("one two three ˇfour");
|
|
cx.simulate_shared_keystrokes("shift-t e").await;
|
|
cx.shared_state().await.assert_eq("one two threeˇ four");
|
|
cx.simulate_shared_keystrokes("3 ;").await;
|
|
cx.shared_state().await.assert_eq("oneˇ two three four");
|
|
cx.simulate_shared_keystrokes(",").await;
|
|
cx.shared_state().await.assert_eq("one two thˇree four");
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_next_word_end_newline_last_char(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
|
let initial_state = indoc! {r"something(ˇfoo)"};
|
|
cx.set_shared_state(initial_state).await;
|
|
cx.simulate_shared_keystrokes("}").await;
|
|
cx.shared_state().await.assert_eq("something(fooˇ)");
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_next_line_start(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
|
cx.set_shared_state("ˇone\n two\nthree").await;
|
|
cx.simulate_shared_keystrokes("enter").await;
|
|
cx.shared_state().await.assert_eq("one\n ˇtwo\nthree");
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_end_of_line_downward(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
|
cx.set_shared_state("ˇ one\n two \nthree").await;
|
|
cx.simulate_shared_keystrokes("g _").await;
|
|
cx.shared_state().await.assert_eq(" onˇe\n two \nthree");
|
|
|
|
cx.set_shared_state("ˇ one \n two \nthree").await;
|
|
cx.simulate_shared_keystrokes("g _").await;
|
|
cx.shared_state().await.assert_eq(" onˇe \n two \nthree");
|
|
cx.simulate_shared_keystrokes("2 g _").await;
|
|
cx.shared_state().await.assert_eq(" one \n twˇo \nthree");
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_window_top(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
|
let initial_state = indoc! {r"abc
|
|
def
|
|
paragraph
|
|
the second
|
|
third ˇand
|
|
final"};
|
|
|
|
cx.set_shared_state(initial_state).await;
|
|
cx.simulate_shared_keystrokes("shift-h").await;
|
|
cx.shared_state().await.assert_eq(indoc! {r"abˇc
|
|
def
|
|
paragraph
|
|
the second
|
|
third and
|
|
final"});
|
|
|
|
// clip point
|
|
cx.set_shared_state(indoc! {r"
|
|
1 2 3
|
|
4 5 6
|
|
7 8 ˇ9
|
|
"})
|
|
.await;
|
|
cx.simulate_shared_keystrokes("shift-h").await;
|
|
cx.shared_state().await.assert_eq(indoc! {"
|
|
1 2 ˇ3
|
|
4 5 6
|
|
7 8 9
|
|
"});
|
|
|
|
cx.set_shared_state(indoc! {r"
|
|
1 2 3
|
|
4 5 6
|
|
ˇ7 8 9
|
|
"})
|
|
.await;
|
|
cx.simulate_shared_keystrokes("shift-h").await;
|
|
cx.shared_state().await.assert_eq(indoc! {"
|
|
ˇ1 2 3
|
|
4 5 6
|
|
7 8 9
|
|
"});
|
|
|
|
cx.set_shared_state(indoc! {r"
|
|
1 2 3
|
|
4 5 ˇ6
|
|
7 8 9"})
|
|
.await;
|
|
cx.simulate_shared_keystrokes("9 shift-h").await;
|
|
cx.shared_state().await.assert_eq(indoc! {"
|
|
1 2 3
|
|
4 5 6
|
|
7 8 ˇ9"});
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_window_middle(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
|
let initial_state = indoc! {r"abˇc
|
|
def
|
|
paragraph
|
|
the second
|
|
third and
|
|
final"};
|
|
|
|
cx.set_shared_state(initial_state).await;
|
|
cx.simulate_shared_keystrokes("shift-m").await;
|
|
cx.shared_state().await.assert_eq(indoc! {r"abc
|
|
def
|
|
paˇragraph
|
|
the second
|
|
third and
|
|
final"});
|
|
|
|
cx.set_shared_state(indoc! {r"
|
|
1 2 3
|
|
4 5 6
|
|
7 8 ˇ9
|
|
"})
|
|
.await;
|
|
cx.simulate_shared_keystrokes("shift-m").await;
|
|
cx.shared_state().await.assert_eq(indoc! {"
|
|
1 2 3
|
|
4 5 ˇ6
|
|
7 8 9
|
|
"});
|
|
cx.set_shared_state(indoc! {r"
|
|
1 2 3
|
|
4 5 6
|
|
ˇ7 8 9
|
|
"})
|
|
.await;
|
|
cx.simulate_shared_keystrokes("shift-m").await;
|
|
cx.shared_state().await.assert_eq(indoc! {"
|
|
1 2 3
|
|
ˇ4 5 6
|
|
7 8 9
|
|
"});
|
|
cx.set_shared_state(indoc! {r"
|
|
ˇ1 2 3
|
|
4 5 6
|
|
7 8 9
|
|
"})
|
|
.await;
|
|
cx.simulate_shared_keystrokes("shift-m").await;
|
|
cx.shared_state().await.assert_eq(indoc! {"
|
|
1 2 3
|
|
ˇ4 5 6
|
|
7 8 9
|
|
"});
|
|
cx.set_shared_state(indoc! {r"
|
|
1 2 3
|
|
ˇ4 5 6
|
|
7 8 9
|
|
"})
|
|
.await;
|
|
cx.simulate_shared_keystrokes("shift-m").await;
|
|
cx.shared_state().await.assert_eq(indoc! {"
|
|
1 2 3
|
|
ˇ4 5 6
|
|
7 8 9
|
|
"});
|
|
cx.set_shared_state(indoc! {r"
|
|
1 2 3
|
|
4 5 ˇ6
|
|
7 8 9
|
|
"})
|
|
.await;
|
|
cx.simulate_shared_keystrokes("shift-m").await;
|
|
cx.shared_state().await.assert_eq(indoc! {"
|
|
1 2 3
|
|
4 5 ˇ6
|
|
7 8 9
|
|
"});
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_window_bottom(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
|
let initial_state = indoc! {r"abc
|
|
deˇf
|
|
paragraph
|
|
the second
|
|
third and
|
|
final"};
|
|
|
|
cx.set_shared_state(initial_state).await;
|
|
cx.simulate_shared_keystrokes("shift-l").await;
|
|
cx.shared_state().await.assert_eq(indoc! {r"abc
|
|
def
|
|
paragraph
|
|
the second
|
|
third and
|
|
fiˇnal"});
|
|
|
|
cx.set_shared_state(indoc! {r"
|
|
1 2 3
|
|
4 5 ˇ6
|
|
7 8 9
|
|
"})
|
|
.await;
|
|
cx.simulate_shared_keystrokes("shift-l").await;
|
|
cx.shared_state().await.assert_eq(indoc! {"
|
|
1 2 3
|
|
4 5 6
|
|
7 8 9
|
|
ˇ"});
|
|
|
|
cx.set_shared_state(indoc! {r"
|
|
1 2 3
|
|
ˇ4 5 6
|
|
7 8 9
|
|
"})
|
|
.await;
|
|
cx.simulate_shared_keystrokes("shift-l").await;
|
|
cx.shared_state().await.assert_eq(indoc! {"
|
|
1 2 3
|
|
4 5 6
|
|
7 8 9
|
|
ˇ"});
|
|
|
|
cx.set_shared_state(indoc! {r"
|
|
1 2 ˇ3
|
|
4 5 6
|
|
7 8 9
|
|
"})
|
|
.await;
|
|
cx.simulate_shared_keystrokes("shift-l").await;
|
|
cx.shared_state().await.assert_eq(indoc! {"
|
|
1 2 3
|
|
4 5 6
|
|
7 8 9
|
|
ˇ"});
|
|
|
|
cx.set_shared_state(indoc! {r"
|
|
ˇ1 2 3
|
|
4 5 6
|
|
7 8 9
|
|
"})
|
|
.await;
|
|
cx.simulate_shared_keystrokes("shift-l").await;
|
|
cx.shared_state().await.assert_eq(indoc! {"
|
|
1 2 3
|
|
4 5 6
|
|
7 8 9
|
|
ˇ"});
|
|
|
|
cx.set_shared_state(indoc! {r"
|
|
1 2 3
|
|
4 5 ˇ6
|
|
7 8 9
|
|
"})
|
|
.await;
|
|
cx.simulate_shared_keystrokes("9 shift-l").await;
|
|
cx.shared_state().await.assert_eq(indoc! {"
|
|
1 2 ˇ3
|
|
4 5 6
|
|
7 8 9
|
|
"});
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_previous_word_end(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
|
cx.set_shared_state(indoc! {r"
|
|
456 5ˇ67 678
|
|
"})
|
|
.await;
|
|
cx.simulate_shared_keystrokes("g e").await;
|
|
cx.shared_state().await.assert_eq(indoc! {"
|
|
45ˇ6 567 678
|
|
"});
|
|
|
|
// Test times
|
|
cx.set_shared_state(indoc! {r"
|
|
123 234 345
|
|
456 5ˇ67 678
|
|
"})
|
|
.await;
|
|
cx.simulate_shared_keystrokes("4 g e").await;
|
|
cx.shared_state().await.assert_eq(indoc! {"
|
|
12ˇ3 234 345
|
|
456 567 678
|
|
"});
|
|
|
|
// With punctuation
|
|
cx.set_shared_state(indoc! {r"
|
|
123 234 345
|
|
4;5.6 5ˇ67 678
|
|
789 890 901
|
|
"})
|
|
.await;
|
|
cx.simulate_shared_keystrokes("g e").await;
|
|
cx.shared_state().await.assert_eq(indoc! {"
|
|
123 234 345
|
|
4;5.ˇ6 567 678
|
|
789 890 901
|
|
"});
|
|
|
|
// With punctuation and count
|
|
cx.set_shared_state(indoc! {r"
|
|
123 234 345
|
|
4;5.6 5ˇ67 678
|
|
789 890 901
|
|
"})
|
|
.await;
|
|
cx.simulate_shared_keystrokes("5 g e").await;
|
|
cx.shared_state().await.assert_eq(indoc! {"
|
|
123 234 345
|
|
ˇ4;5.6 567 678
|
|
789 890 901
|
|
"});
|
|
|
|
// newlines
|
|
cx.set_shared_state(indoc! {r"
|
|
123 234 345
|
|
|
|
78ˇ9 890 901
|
|
"})
|
|
.await;
|
|
cx.simulate_shared_keystrokes("g e").await;
|
|
cx.shared_state().await.assert_eq(indoc! {"
|
|
123 234 345
|
|
ˇ
|
|
789 890 901
|
|
"});
|
|
cx.simulate_shared_keystrokes("g e").await;
|
|
cx.shared_state().await.assert_eq(indoc! {"
|
|
123 234 34ˇ5
|
|
|
|
789 890 901
|
|
"});
|
|
|
|
// With punctuation
|
|
cx.set_shared_state(indoc! {r"
|
|
123 234 345
|
|
4;5.ˇ6 567 678
|
|
789 890 901
|
|
"})
|
|
.await;
|
|
cx.simulate_shared_keystrokes("g shift-e").await;
|
|
cx.shared_state().await.assert_eq(indoc! {"
|
|
123 234 34ˇ5
|
|
4;5.6 567 678
|
|
789 890 901
|
|
"});
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_visual_match_eol(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
|
|
|
cx.set_shared_state(indoc! {"
|
|
fn aˇ() {
|
|
return
|
|
}
|
|
"})
|
|
.await;
|
|
cx.simulate_shared_keystrokes("v $ %").await;
|
|
cx.shared_state().await.assert_eq(indoc! {"
|
|
fn a«() {
|
|
return
|
|
}ˇ»
|
|
"});
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_clipping_with_inlay_hints(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = VimTestContext::new(cx, true).await;
|
|
|
|
cx.set_state(
|
|
indoc! {"
|
|
struct Foo {
|
|
ˇ
|
|
}
|
|
"},
|
|
Mode::Normal,
|
|
);
|
|
|
|
cx.update_editor(|editor, _window, cx| {
|
|
let range = editor.selections.newest_anchor().range();
|
|
let inlay_text = " field: int,\n field2: string\n field3: float";
|
|
let inlay = Inlay::inline_completion(1, range.start, inlay_text);
|
|
editor.splice_inlays(&[], vec![inlay], cx);
|
|
});
|
|
|
|
cx.simulate_keystrokes("j");
|
|
cx.assert_state(
|
|
indoc! {"
|
|
struct Foo {
|
|
|
|
ˇ}
|
|
"},
|
|
Mode::Normal,
|
|
);
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_clipping_with_inlay_hints_end_of_line(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = VimTestContext::new(cx, true).await;
|
|
|
|
cx.set_state(
|
|
indoc! {"
|
|
ˇstruct Foo {
|
|
|
|
}
|
|
"},
|
|
Mode::Normal,
|
|
);
|
|
cx.update_editor(|editor, _window, cx| {
|
|
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
|
let end_of_line =
|
|
snapshot.anchor_after(Point::new(0, snapshot.line_len(MultiBufferRow(0))));
|
|
let inlay_text = " hint";
|
|
let inlay = Inlay::inline_completion(1, end_of_line, inlay_text);
|
|
editor.splice_inlays(&[], vec![inlay], cx);
|
|
});
|
|
cx.simulate_keystrokes("$");
|
|
cx.assert_state(
|
|
indoc! {"
|
|
struct Foo ˇ{
|
|
|
|
}
|
|
"},
|
|
Mode::Normal,
|
|
);
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_go_to_percentage(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
|
// Normal mode
|
|
cx.set_shared_state(indoc! {"
|
|
The ˇquick brown
|
|
fox jumps over
|
|
the lazy dog
|
|
The quick brown
|
|
fox jumps over
|
|
the lazy dog
|
|
The quick brown
|
|
fox jumps over
|
|
the lazy dog"})
|
|
.await;
|
|
cx.simulate_shared_keystrokes("2 0 %").await;
|
|
cx.shared_state().await.assert_eq(indoc! {"
|
|
The quick brown
|
|
fox ˇjumps over
|
|
the lazy dog
|
|
The quick brown
|
|
fox jumps over
|
|
the lazy dog
|
|
The quick brown
|
|
fox jumps over
|
|
the lazy dog"});
|
|
|
|
cx.simulate_shared_keystrokes("2 5 %").await;
|
|
cx.shared_state().await.assert_eq(indoc! {"
|
|
The quick brown
|
|
fox jumps over
|
|
the ˇlazy dog
|
|
The quick brown
|
|
fox jumps over
|
|
the lazy dog
|
|
The quick brown
|
|
fox jumps over
|
|
the lazy dog"});
|
|
|
|
cx.simulate_shared_keystrokes("7 5 %").await;
|
|
cx.shared_state().await.assert_eq(indoc! {"
|
|
The quick brown
|
|
fox jumps over
|
|
the lazy dog
|
|
The quick brown
|
|
fox jumps over
|
|
the lazy dog
|
|
The ˇquick brown
|
|
fox jumps over
|
|
the lazy dog"});
|
|
|
|
// Visual mode
|
|
cx.set_shared_state(indoc! {"
|
|
The ˇquick brown
|
|
fox jumps over
|
|
the lazy dog
|
|
The quick brown
|
|
fox jumps over
|
|
the lazy dog
|
|
The quick brown
|
|
fox jumps over
|
|
the lazy dog"})
|
|
.await;
|
|
cx.simulate_shared_keystrokes("v 5 0 %").await;
|
|
cx.shared_state().await.assert_eq(indoc! {"
|
|
The «quick brown
|
|
fox jumps over
|
|
the lazy dog
|
|
The quick brown
|
|
fox jˇ»umps over
|
|
the lazy dog
|
|
The quick brown
|
|
fox jumps over
|
|
the lazy dog"});
|
|
|
|
cx.set_shared_state(indoc! {"
|
|
The ˇquick brown
|
|
fox jumps over
|
|
the lazy dog
|
|
The quick brown
|
|
fox jumps over
|
|
the lazy dog
|
|
The quick brown
|
|
fox jumps over
|
|
the lazy dog"})
|
|
.await;
|
|
cx.simulate_shared_keystrokes("v 1 0 0 %").await;
|
|
cx.shared_state().await.assert_eq(indoc! {"
|
|
The «quick brown
|
|
fox jumps over
|
|
the lazy dog
|
|
The quick brown
|
|
fox jumps over
|
|
the lazy dog
|
|
The quick brown
|
|
fox jumps over
|
|
the lˇ»azy dog"});
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_space_non_ascii(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
|
|
|
cx.set_shared_state("ˇπππππ").await;
|
|
cx.simulate_shared_keystrokes("3 space").await;
|
|
cx.shared_state().await.assert_eq("πππˇππ");
|
|
}
|
|
}
|