# Objective - Preserve Helix mode selections after surround-related commands such as adding, deleting, and replacing surrounds. - Add Helix-compatible closest-pair behavior for `m i m` / `m a m` and surround commands that target `m`. - Avoid long stalls in Helix text-object boundary scans by removing repeated syntax-scope classification during textual scans. - Keep workflows like selecting text with `m i w`, applying `m s`, and continuing with `i`, `a`, or another surround command aligned with Helix behavior. ## Solution - Add selection anchors that preserve each pre-edit selection range and direction through surround edits. - Update Helix surround add so the resulting selection includes the added delimiters, allowing surround commands to compose and insert/append actions to land outside the pair. - Apply preserved selections to Helix surround delete and replace so non-empty selections are not collapsed to the cursor after the edit. - Add Helix-specific text-object bindings so `m` selects the closest surrounding pair, `t` selects types/classes, `c` selects comments, and `x` selects XML/HTML elements in Helix mode. - Match the current closest pair through language bracket queries, so delimiters inside strings or comments do not count as surrounding pairs. - Resolve the textual boundary scanner's `CharClassifier` once per scan and pass it by reference instead of recomputing the same classifier for every scanned character. Word and subword boundaries still clone it only to apply the `ignore_punctuation` flag. ## Performance The textual boundary scanner previously called `char_classifier_at(from.0)` inside the per-character comparison closures for the four `ImmediateBoundary` scan methods and `FuzzyBoundary::to_boundary`. That lookup resolves the language scope through the syntax layers, and the value was loop-invariant because every iteration used the scan's starting offset. Local debug benchmark, using `] m` in a roughly 540 KB buffer with no pair characters so the scan runs to EOF across all pair boundaries: | | `] m` | | ------ | -----: | | Before | 36.3 s | | After | 210 ms | This speeds up the remaining textual scanner paths, including `m i (` / `m i "`-style pair objects, word/sentence/paragraph objects, `]` / `[` text-object navigation, and `] m` / `[ m`. The current-pair `m i m`, `m a m`, `m d m`, and `m r m` paths use the tree-sitter closest-pair path instead. ## Testing - Ran `cargo test -p vim test_helix_surround -- --nocapture`. - The focused Helix surround test filter passed: 10 passed, 0 failed. - Added coverage for closest-pair selection, Helix-specific text-object keys, and surround selection preservation. ## Self-Review Checklist: - [x] I've reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments - [x] The content adheres to Zed's UI standards ([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) and [icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md) guidelines) - [x] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable --- Release Notes: - Added Helix mode support for closest-pair text objects with `m i m` and `m a m`. - Fixed Helix mode surround commands to preserve selections after edits and avoid slow textual boundary scans.
4114 lines
133 KiB
Rust
4114 lines
133 KiB
Rust
use std::ops::Range;
|
|
|
|
use crate::{
|
|
Vim,
|
|
motion::{is_subword_end, is_subword_start, right},
|
|
state::{Mode, Operator},
|
|
surrounds::{BRACKET_PAIRS, QUOTE_PAIRS, SURROUND_PAIRS, SurroundPair},
|
|
};
|
|
use editor::{
|
|
Bias, BufferOffset, DisplayPoint, Editor, MultiBufferOffset, ToOffset,
|
|
display_map::{DisplaySnapshot, ToDisplayPoint},
|
|
movement::{self, FindRange},
|
|
};
|
|
use gpui::{Action, Window, actions};
|
|
use itertools::Itertools;
|
|
use language::{BufferSnapshot, CharKind, Point, Selection, TextObject, TreeSitterOptions};
|
|
use multi_buffer::MultiBufferRow;
|
|
use schemars::JsonSchema;
|
|
use serde::Deserialize;
|
|
use ui::Context;
|
|
|
|
#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize, JsonSchema)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum Object {
|
|
Word { ignore_punctuation: bool },
|
|
Subword { ignore_punctuation: bool },
|
|
Sentence,
|
|
Paragraph,
|
|
Quotes,
|
|
BackQuotes,
|
|
AnyQuotes,
|
|
MiniQuotes,
|
|
DoubleQuotes,
|
|
VerticalBars,
|
|
AnyBrackets,
|
|
MiniBrackets,
|
|
AnyPair,
|
|
Parentheses,
|
|
SquareBrackets,
|
|
CurlyBrackets,
|
|
AngleBrackets,
|
|
Argument,
|
|
IndentObj { include_below: bool },
|
|
Tag,
|
|
Method,
|
|
Class,
|
|
Comment,
|
|
EntireFile,
|
|
}
|
|
|
|
/// Selects a word text object.
|
|
#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
|
|
#[action(namespace = vim)]
|
|
#[serde(deny_unknown_fields)]
|
|
struct Word {
|
|
#[serde(default)]
|
|
ignore_punctuation: bool,
|
|
}
|
|
|
|
/// Selects a subword text object.
|
|
#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
|
|
#[action(namespace = vim)]
|
|
#[serde(deny_unknown_fields)]
|
|
struct Subword {
|
|
#[serde(default)]
|
|
ignore_punctuation: bool,
|
|
}
|
|
/// Selects text at the same indentation level.
|
|
#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
|
|
#[action(namespace = vim)]
|
|
#[serde(deny_unknown_fields)]
|
|
struct IndentObj {
|
|
#[serde(default)]
|
|
include_below: bool,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct CandidateRange {
|
|
pub start: DisplayPoint,
|
|
pub end: DisplayPoint,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct CandidateWithRanges {
|
|
candidate: CandidateRange,
|
|
open_range: Range<MultiBufferOffset>,
|
|
close_range: Range<MultiBufferOffset>,
|
|
}
|
|
|
|
/// Operates on text within or around parentheses `()`.
|
|
#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
|
|
#[action(namespace = vim)]
|
|
#[serde(deny_unknown_fields)]
|
|
struct Parentheses {
|
|
#[serde(default)]
|
|
opening: bool,
|
|
}
|
|
|
|
/// Operates on text within or around square brackets `[]`.
|
|
#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
|
|
#[action(namespace = vim)]
|
|
#[serde(deny_unknown_fields)]
|
|
struct SquareBrackets {
|
|
#[serde(default)]
|
|
opening: bool,
|
|
}
|
|
|
|
/// Operates on text within or around angle brackets `<>`.
|
|
#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
|
|
#[action(namespace = vim)]
|
|
#[serde(deny_unknown_fields)]
|
|
struct AngleBrackets {
|
|
#[serde(default)]
|
|
opening: bool,
|
|
}
|
|
|
|
/// Operates on text within or around curly brackets `{}`.
|
|
#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
|
|
#[action(namespace = vim)]
|
|
#[serde(deny_unknown_fields)]
|
|
struct CurlyBrackets {
|
|
#[serde(default)]
|
|
opening: bool,
|
|
}
|
|
|
|
fn cover_or_next<I: Iterator<Item = (Range<MultiBufferOffset>, Range<MultiBufferOffset>)>>(
|
|
candidates: Option<I>,
|
|
caret: DisplayPoint,
|
|
map: &DisplaySnapshot,
|
|
) -> Option<CandidateWithRanges> {
|
|
let caret_offset = caret.to_offset(map, Bias::Left);
|
|
let mut covering = vec![];
|
|
let mut next_ones = vec![];
|
|
let snapshot = map.buffer_snapshot();
|
|
|
|
if let Some(ranges) = candidates {
|
|
for (open_range, close_range) in ranges {
|
|
let start_off = open_range.start;
|
|
let end_off = close_range.end;
|
|
let candidate = CandidateWithRanges {
|
|
candidate: CandidateRange {
|
|
start: start_off.to_display_point(map),
|
|
end: end_off.to_display_point(map),
|
|
},
|
|
open_range: open_range.clone(),
|
|
close_range: close_range.clone(),
|
|
};
|
|
|
|
if open_range
|
|
.start
|
|
.to_offset(snapshot)
|
|
.to_display_point(map)
|
|
.row()
|
|
== caret_offset.to_display_point(map).row()
|
|
{
|
|
if start_off <= caret_offset && caret_offset < end_off {
|
|
covering.push(candidate);
|
|
} else if start_off >= caret_offset {
|
|
next_ones.push(candidate);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 1) covering -> smallest width
|
|
if !covering.is_empty() {
|
|
return covering.into_iter().min_by_key(|r| {
|
|
r.candidate.end.to_offset(map, Bias::Right)
|
|
- r.candidate.start.to_offset(map, Bias::Left)
|
|
});
|
|
}
|
|
|
|
// 2) next -> closest by start
|
|
if !next_ones.is_empty() {
|
|
return next_ones.into_iter().min_by_key(|r| {
|
|
let start = r.candidate.start.to_offset(map, Bias::Left);
|
|
(start.0 as isize - caret_offset.0 as isize).abs()
|
|
});
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
type DelimiterPredicate = dyn Fn(&BufferSnapshot, usize, usize) -> bool;
|
|
|
|
pub(crate) struct DelimiterRange {
|
|
pub(crate) open: Range<MultiBufferOffset>,
|
|
pub(crate) close: Range<MultiBufferOffset>,
|
|
}
|
|
|
|
impl DelimiterRange {
|
|
pub(crate) fn to_display_range(
|
|
&self,
|
|
map: &DisplaySnapshot,
|
|
around: bool,
|
|
) -> Range<DisplayPoint> {
|
|
if around {
|
|
self.open.start.to_display_point(map)..self.close.end.to_display_point(map)
|
|
} else {
|
|
self.open.end.to_display_point(map)..self.close.start.to_display_point(map)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The innermost pair from the language's bracket queries that surrounds
|
|
/// `relative_to` and whose delimiters form a known single-character surround
|
|
/// pair, like Helix's tree-sitter based closest-pair matching. Returns None
|
|
/// in buffers without bracket queries.
|
|
pub(crate) fn innermost_surrounding_pair(
|
|
map: &DisplaySnapshot,
|
|
relative_to: Range<DisplayPoint>,
|
|
) -> Option<DelimiterRange> {
|
|
innermost_surrounding_pair_impl(map, relative_to, PairMatchMode::Any)
|
|
}
|
|
|
|
pub(crate) fn innermost_surrounding_pair_for_text_object(
|
|
map: &DisplaySnapshot,
|
|
relative_to: Range<DisplayPoint>,
|
|
around: bool,
|
|
) -> Option<DelimiterRange> {
|
|
innermost_surrounding_pair_impl(map, relative_to, PairMatchMode::ExpandSelection { around })
|
|
}
|
|
|
|
#[derive(Clone, Copy)]
|
|
enum PairMatchMode {
|
|
Any,
|
|
ExpandSelection { around: bool },
|
|
}
|
|
|
|
fn innermost_surrounding_pair_impl(
|
|
map: &DisplaySnapshot,
|
|
relative_to: Range<DisplayPoint>,
|
|
match_mode: PairMatchMode,
|
|
) -> Option<DelimiterRange> {
|
|
let snapshot = map.buffer_snapshot();
|
|
let offset_range =
|
|
relative_to.start.to_offset(map, Bias::Left)..relative_to.end.to_offset(map, Bias::Left);
|
|
|
|
let results = snapshot.map_excerpt_ranges(offset_range, |buffer, _, input_range| {
|
|
let filter = |open: Range<usize>, close: Range<usize>| {
|
|
if let PairMatchMode::ExpandSelection { around } = match_mode {
|
|
let input_range = input_range.start.0..input_range.end.0;
|
|
let selects_around = input_range == (open.start..close.end);
|
|
let selects_inside = input_range == (open.end..close.start);
|
|
// `mam` after `mim` changes the form of the current pair;
|
|
// every other exact match expands to the enclosing pair.
|
|
if selects_around || (selects_inside && !around) {
|
|
return false;
|
|
}
|
|
}
|
|
is_surround_pair_delimiter(buffer, open, close)
|
|
};
|
|
let Some((open, close)) = buffer.innermost_enclosing_bracket_ranges(
|
|
input_range.start.0..input_range.end.0,
|
|
Some(&filter),
|
|
) else {
|
|
return vec![];
|
|
};
|
|
vec![
|
|
(BufferOffset(open.start)..BufferOffset(open.end), ()),
|
|
(BufferOffset(close.start)..BufferOffset(close.end), ()),
|
|
]
|
|
})?;
|
|
|
|
if results.len() < 2 {
|
|
return None;
|
|
}
|
|
|
|
Some(DelimiterRange {
|
|
open: results[0].0.clone(),
|
|
close: results[1].0.clone(),
|
|
})
|
|
}
|
|
|
|
/// Bracket queries can also match multi-character delimiters (e.g. HTML
|
|
/// element tags), which Helix's pair matching rejects.
|
|
fn is_surround_pair_delimiter(
|
|
buffer: &BufferSnapshot,
|
|
open: Range<usize>,
|
|
close: Range<usize>,
|
|
) -> bool {
|
|
if open.len() != 1 || close.len() != 1 {
|
|
return false;
|
|
}
|
|
match (
|
|
buffer.chars_at(open.start).next(),
|
|
buffer.chars_at(close.start).next(),
|
|
) {
|
|
(Some(open_char), Some(close_char)) => SURROUND_PAIRS
|
|
.iter()
|
|
.any(|pair| pair.open == open_char && pair.close == close_char),
|
|
_ => false,
|
|
}
|
|
}
|
|
|
|
fn find_mini_delimiters(
|
|
map: &DisplaySnapshot,
|
|
display_point: DisplayPoint,
|
|
around: bool,
|
|
is_valid_delimiter: &DelimiterPredicate,
|
|
) -> Option<Range<DisplayPoint>> {
|
|
let point = map.clip_at_line_end(display_point).to_point(map);
|
|
let offset = map.buffer_snapshot().point_to_offset(point);
|
|
|
|
let line_range = get_line_range(map, point);
|
|
let visible_line_range = get_visible_line_range(&line_range);
|
|
|
|
let snapshot = &map.buffer_snapshot();
|
|
|
|
let ranges = map
|
|
.buffer_snapshot()
|
|
.bracket_ranges(visible_line_range)
|
|
.map(|ranges| {
|
|
ranges.filter_map(|(open, close)| {
|
|
let (buffer, buffer_open) =
|
|
snapshot.range_to_buffer_range::<MultiBufferOffset>(open.clone())?;
|
|
let (_, buffer_close) =
|
|
snapshot.range_to_buffer_range::<MultiBufferOffset>(close.clone())?;
|
|
|
|
if is_valid_delimiter(buffer, buffer_open.start, buffer_close.start) {
|
|
Some((open, close))
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
});
|
|
|
|
if let Some(candidate) = cover_or_next(ranges, display_point, map) {
|
|
return Some(
|
|
DelimiterRange {
|
|
open: candidate.open_range,
|
|
close: candidate.close_range,
|
|
}
|
|
.to_display_range(map, around),
|
|
);
|
|
}
|
|
|
|
let results = snapshot.map_excerpt_ranges(offset..offset, |buffer, _, input_range| {
|
|
let buffer_offset = input_range.start.0;
|
|
let bracket_filter = |open: Range<usize>, close: Range<usize>| {
|
|
is_valid_delimiter(buffer, open.start, close.start)
|
|
};
|
|
let Some((open, close)) = buffer.innermost_enclosing_bracket_ranges(
|
|
buffer_offset..buffer_offset,
|
|
Some(&bracket_filter),
|
|
) else {
|
|
return vec![];
|
|
};
|
|
vec![
|
|
(BufferOffset(open.start)..BufferOffset(open.end), ()),
|
|
(BufferOffset(close.start)..BufferOffset(close.end), ()),
|
|
]
|
|
})?;
|
|
|
|
if results.len() < 2 {
|
|
return None;
|
|
}
|
|
|
|
Some(
|
|
DelimiterRange {
|
|
open: results[0].0.clone(),
|
|
close: results[1].0.clone(),
|
|
}
|
|
.to_display_range(map, around),
|
|
)
|
|
}
|
|
|
|
fn get_line_range(map: &DisplaySnapshot, point: Point) -> Range<Point> {
|
|
let (start, mut end) = (
|
|
map.prev_line_boundary(point).0,
|
|
map.next_line_boundary(point).0,
|
|
);
|
|
|
|
if end == point {
|
|
end = map.max_point().to_point(map);
|
|
}
|
|
|
|
start..end
|
|
}
|
|
|
|
fn get_visible_line_range(line_range: &Range<Point>) -> Range<Point> {
|
|
let end_column = line_range.end.column.saturating_sub(1);
|
|
line_range.start..Point::new(line_range.end.row, end_column)
|
|
}
|
|
|
|
fn is_quote_delimiter(buffer: &BufferSnapshot, _start: usize, end: usize) -> bool {
|
|
matches!(buffer.chars_at(end).next(), Some('\'' | '"' | '`'))
|
|
}
|
|
|
|
fn is_bracket_delimiter(buffer: &BufferSnapshot, start: usize, _end: usize) -> bool {
|
|
matches!(
|
|
buffer.chars_at(start).next(),
|
|
Some('(' | '[' | '{' | '<' | '|')
|
|
)
|
|
}
|
|
|
|
fn find_mini_quotes(
|
|
map: &DisplaySnapshot,
|
|
display_point: DisplayPoint,
|
|
around: bool,
|
|
) -> Option<Range<DisplayPoint>> {
|
|
find_mini_delimiters(map, display_point, around, &is_quote_delimiter)
|
|
}
|
|
|
|
fn find_mini_brackets(
|
|
map: &DisplaySnapshot,
|
|
display_point: DisplayPoint,
|
|
around: bool,
|
|
) -> Option<Range<DisplayPoint>> {
|
|
find_mini_delimiters(map, display_point, around, &is_bracket_delimiter)
|
|
}
|
|
|
|
actions!(
|
|
vim,
|
|
[
|
|
/// Selects a sentence text object.
|
|
Sentence,
|
|
/// Selects a paragraph text object.
|
|
Paragraph,
|
|
/// Selects text within single quotes.
|
|
Quotes,
|
|
/// Selects text within backticks.
|
|
BackQuotes,
|
|
/// Selects text within the nearest quotes (single or double).
|
|
MiniQuotes,
|
|
/// Selects text within any type of quotes.
|
|
AnyQuotes,
|
|
/// Selects text within double quotes.
|
|
DoubleQuotes,
|
|
/// Selects text within vertical bars (pipes).
|
|
VerticalBars,
|
|
/// Selects text within the nearest brackets.
|
|
MiniBrackets,
|
|
/// Selects text within any type of brackets.
|
|
AnyBrackets,
|
|
/// Selects text within the closest surrounding pair of any type
|
|
/// (brackets, quotes, backticks, or vertical bars), based on the
|
|
/// language's bracket queries, like Helix's `m` text object.
|
|
AnyPair,
|
|
/// Selects a function argument.
|
|
Argument,
|
|
/// Selects an HTML/XML tag.
|
|
Tag,
|
|
/// Selects a method or function.
|
|
Method,
|
|
/// Selects a class definition.
|
|
Class,
|
|
/// Selects a comment block.
|
|
Comment,
|
|
/// Selects the entire file.
|
|
EntireFile
|
|
]
|
|
);
|
|
|
|
pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
|
|
Vim::action(
|
|
editor,
|
|
cx,
|
|
|vim, &Word { ignore_punctuation }: &Word, window, cx| {
|
|
vim.object(Object::Word { ignore_punctuation }, window, cx)
|
|
},
|
|
);
|
|
Vim::action(
|
|
editor,
|
|
cx,
|
|
|vim, &Subword { ignore_punctuation }: &Subword, window, cx| {
|
|
vim.object(Object::Subword { ignore_punctuation }, window, cx)
|
|
},
|
|
);
|
|
Vim::action(editor, cx, |vim, _: &Tag, window, cx| {
|
|
vim.object(Object::Tag, window, cx)
|
|
});
|
|
Vim::action(editor, cx, |vim, _: &Sentence, window, cx| {
|
|
vim.object(Object::Sentence, window, cx)
|
|
});
|
|
Vim::action(editor, cx, |vim, _: &Paragraph, window, cx| {
|
|
vim.object(Object::Paragraph, window, cx)
|
|
});
|
|
Vim::action(editor, cx, |vim, _: &Quotes, window, cx| {
|
|
vim.object(Object::Quotes, window, cx)
|
|
});
|
|
Vim::action(editor, cx, |vim, _: &BackQuotes, window, cx| {
|
|
vim.object(Object::BackQuotes, window, cx)
|
|
});
|
|
Vim::action(editor, cx, |vim, _: &MiniQuotes, window, cx| {
|
|
vim.object(Object::MiniQuotes, window, cx)
|
|
});
|
|
Vim::action(editor, cx, |vim, _: &MiniBrackets, window, cx| {
|
|
vim.object(Object::MiniBrackets, window, cx)
|
|
});
|
|
Vim::action(editor, cx, |vim, _: &AnyQuotes, window, cx| {
|
|
vim.object(Object::AnyQuotes, window, cx)
|
|
});
|
|
Vim::action(editor, cx, |vim, _: &AnyBrackets, window, cx| {
|
|
vim.object(Object::AnyBrackets, window, cx)
|
|
});
|
|
Vim::action(editor, cx, |vim, _: &AnyPair, window, cx| {
|
|
vim.object(Object::AnyPair, window, cx)
|
|
});
|
|
Vim::action(editor, cx, |vim, _: &BackQuotes, window, cx| {
|
|
vim.object(Object::BackQuotes, window, cx)
|
|
});
|
|
Vim::action(editor, cx, |vim, _: &DoubleQuotes, window, cx| {
|
|
vim.object(Object::DoubleQuotes, window, cx)
|
|
});
|
|
Vim::action(editor, cx, |vim, action: &Parentheses, window, cx| {
|
|
vim.object_impl(Object::Parentheses, action.opening, window, cx)
|
|
});
|
|
Vim::action(editor, cx, |vim, action: &SquareBrackets, window, cx| {
|
|
vim.object_impl(Object::SquareBrackets, action.opening, window, cx)
|
|
});
|
|
Vim::action(editor, cx, |vim, action: &CurlyBrackets, window, cx| {
|
|
vim.object_impl(Object::CurlyBrackets, action.opening, window, cx)
|
|
});
|
|
Vim::action(editor, cx, |vim, action: &AngleBrackets, window, cx| {
|
|
vim.object_impl(Object::AngleBrackets, action.opening, window, cx)
|
|
});
|
|
Vim::action(editor, cx, |vim, _: &VerticalBars, window, cx| {
|
|
vim.object(Object::VerticalBars, window, cx)
|
|
});
|
|
Vim::action(editor, cx, |vim, _: &Argument, window, cx| {
|
|
vim.object(Object::Argument, window, cx)
|
|
});
|
|
Vim::action(editor, cx, |vim, _: &Method, window, cx| {
|
|
vim.object(Object::Method, window, cx)
|
|
});
|
|
Vim::action(editor, cx, |vim, _: &Class, window, cx| {
|
|
vim.object(Object::Class, window, cx)
|
|
});
|
|
Vim::action(editor, cx, |vim, _: &EntireFile, window, cx| {
|
|
vim.object(Object::EntireFile, window, cx)
|
|
});
|
|
Vim::action(editor, cx, |vim, _: &Comment, window, cx| {
|
|
if !matches!(vim.active_operator(), Some(Operator::Object { .. })) {
|
|
vim.push_operator(Operator::Object { around: true }, window, cx);
|
|
}
|
|
vim.object(Object::Comment, window, cx)
|
|
});
|
|
Vim::action(
|
|
editor,
|
|
cx,
|
|
|vim, &IndentObj { include_below }: &IndentObj, window, cx| {
|
|
vim.object(Object::IndentObj { include_below }, window, cx)
|
|
},
|
|
);
|
|
}
|
|
|
|
impl Vim {
|
|
fn object(&mut self, object: Object, window: &mut Window, cx: &mut Context<Self>) {
|
|
self.object_impl(object, false, window, cx);
|
|
}
|
|
|
|
fn object_impl(
|
|
&mut self,
|
|
object: Object,
|
|
opening: bool,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
let count = Self::take_count(cx);
|
|
|
|
match self.mode {
|
|
Mode::Normal | Mode::HelixNormal => {
|
|
self.normal_object(object, count, opening, window, cx)
|
|
}
|
|
Mode::Visual | Mode::VisualLine | Mode::VisualBlock | Mode::HelixSelect => {
|
|
self.visual_object(object, count, window, cx)
|
|
}
|
|
Mode::Insert | Mode::Replace => {
|
|
// Shouldn't execute a text object in insert mode. Ignoring
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Object {
|
|
pub fn is_multiline(self) -> bool {
|
|
match self {
|
|
Object::Word { .. }
|
|
| Object::Subword { .. }
|
|
| Object::Quotes
|
|
| Object::BackQuotes
|
|
| Object::AnyQuotes
|
|
| Object::MiniQuotes
|
|
| Object::VerticalBars
|
|
| Object::DoubleQuotes => false,
|
|
Object::Sentence
|
|
| Object::Paragraph
|
|
| Object::AnyBrackets
|
|
| Object::MiniBrackets
|
|
| Object::AnyPair
|
|
| Object::Parentheses
|
|
| Object::Tag
|
|
| Object::AngleBrackets
|
|
| Object::CurlyBrackets
|
|
| Object::SquareBrackets
|
|
| Object::Argument
|
|
| Object::Method
|
|
| Object::Class
|
|
| Object::EntireFile
|
|
| Object::Comment
|
|
| Object::IndentObj { .. } => true,
|
|
}
|
|
}
|
|
|
|
pub fn always_expands_both_ways(self) -> bool {
|
|
match self {
|
|
Object::Word { .. }
|
|
| Object::Subword { .. }
|
|
| Object::Sentence
|
|
| Object::Paragraph
|
|
| Object::Argument
|
|
| Object::IndentObj { .. } => false,
|
|
Object::Quotes
|
|
| Object::BackQuotes
|
|
| Object::AnyQuotes
|
|
| Object::MiniQuotes
|
|
| Object::DoubleQuotes
|
|
| Object::VerticalBars
|
|
| Object::AnyBrackets
|
|
| Object::MiniBrackets
|
|
| Object::AnyPair
|
|
| Object::Parentheses
|
|
| Object::SquareBrackets
|
|
| Object::Tag
|
|
| Object::Method
|
|
| Object::Class
|
|
| Object::Comment
|
|
| Object::EntireFile
|
|
| Object::CurlyBrackets
|
|
| Object::AngleBrackets => true,
|
|
}
|
|
}
|
|
|
|
pub fn target_visual_mode(self, current_mode: Mode, around: bool) -> Mode {
|
|
match self {
|
|
Object::Word { .. }
|
|
| Object::Subword { .. }
|
|
| Object::Sentence
|
|
| Object::Quotes
|
|
| Object::AnyQuotes
|
|
| Object::MiniQuotes
|
|
| Object::BackQuotes
|
|
| Object::DoubleQuotes => {
|
|
if current_mode == Mode::VisualBlock {
|
|
Mode::VisualBlock
|
|
} else {
|
|
Mode::Visual
|
|
}
|
|
}
|
|
Object::Parentheses
|
|
| Object::AnyBrackets
|
|
| Object::MiniBrackets
|
|
| Object::AnyPair
|
|
| Object::SquareBrackets
|
|
| Object::CurlyBrackets
|
|
| Object::AngleBrackets
|
|
| Object::VerticalBars
|
|
| Object::Tag
|
|
| Object::Comment
|
|
| Object::Argument
|
|
| Object::IndentObj { .. } => Mode::Visual,
|
|
Object::Method | Object::Class => {
|
|
if around {
|
|
Mode::VisualLine
|
|
} else {
|
|
Mode::Visual
|
|
}
|
|
}
|
|
Object::Paragraph | Object::EntireFile => Mode::VisualLine,
|
|
}
|
|
}
|
|
|
|
pub fn range(
|
|
self,
|
|
map: &DisplaySnapshot,
|
|
selection: Selection<DisplayPoint>,
|
|
around: bool,
|
|
times: Option<usize>,
|
|
) -> Option<Range<DisplayPoint>> {
|
|
let relative_to = selection.head();
|
|
match self {
|
|
Object::Word { ignore_punctuation } => {
|
|
let count = times.unwrap_or(1);
|
|
if around {
|
|
around_word(map, relative_to, ignore_punctuation, count)
|
|
} else {
|
|
in_word(map, relative_to, ignore_punctuation, count).map(|range| {
|
|
// For iw with count > 1, vim includes trailing whitespace
|
|
if count > 1 {
|
|
let spans_multiple_lines = range.start.row() != range.end.row();
|
|
expand_to_include_whitespace(map, range, !spans_multiple_lines)
|
|
} else {
|
|
range
|
|
}
|
|
})
|
|
}
|
|
}
|
|
Object::Subword { ignore_punctuation } => {
|
|
if around {
|
|
around_subword(map, relative_to, ignore_punctuation)
|
|
} else {
|
|
in_subword(map, relative_to, ignore_punctuation)
|
|
}
|
|
}
|
|
Object::Sentence => sentence(map, relative_to, around),
|
|
//change others later
|
|
Object::Paragraph => paragraph(map, relative_to, around, times.unwrap_or(1)),
|
|
Object::Quotes => {
|
|
surrounding_markers(map, relative_to, around, self.is_multiline(), '\'', '\'')
|
|
}
|
|
Object::BackQuotes => {
|
|
surrounding_markers(map, relative_to, around, self.is_multiline(), '`', '`')
|
|
}
|
|
Object::AnyQuotes => {
|
|
let cursor_offset = relative_to.to_offset(map, Bias::Left);
|
|
|
|
// Find innermost range directly without collecting all ranges
|
|
let mut innermost = None;
|
|
let mut min_size = usize::MAX;
|
|
|
|
// First pass: find innermost enclosing range
|
|
for &SurroundPair { open, close } in QUOTE_PAIRS {
|
|
if let Some(range) = surrounding_markers(
|
|
map,
|
|
relative_to,
|
|
around,
|
|
self.is_multiline(),
|
|
open,
|
|
close,
|
|
) {
|
|
let start_offset = range.start.to_offset(map, Bias::Left);
|
|
let end_offset = range.end.to_offset(map, Bias::Right);
|
|
|
|
if cursor_offset >= start_offset && cursor_offset <= end_offset {
|
|
let size = end_offset - start_offset;
|
|
if size < min_size {
|
|
min_size = size;
|
|
innermost = Some(range);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if let Some(range) = innermost {
|
|
return Some(range);
|
|
}
|
|
|
|
// Fallback: find nearest pair if not inside any quotes
|
|
QUOTE_PAIRS
|
|
.iter()
|
|
.flat_map(|&SurroundPair { open, close }| {
|
|
surrounding_markers(
|
|
map,
|
|
relative_to,
|
|
around,
|
|
self.is_multiline(),
|
|
open,
|
|
close,
|
|
)
|
|
})
|
|
.min_by_key(|range| {
|
|
let start_offset = range.start.to_offset(map, Bias::Left);
|
|
let end_offset = range.end.to_offset(map, Bias::Right);
|
|
if cursor_offset < start_offset {
|
|
(start_offset - cursor_offset) as isize
|
|
} else if cursor_offset > end_offset {
|
|
(cursor_offset - end_offset) as isize
|
|
} else {
|
|
0
|
|
}
|
|
})
|
|
}
|
|
Object::MiniQuotes => find_mini_quotes(map, relative_to, around),
|
|
Object::DoubleQuotes => {
|
|
surrounding_markers(map, relative_to, around, self.is_multiline(), '"', '"')
|
|
}
|
|
Object::VerticalBars => {
|
|
surrounding_markers(map, relative_to, around, self.is_multiline(), '|', '|')
|
|
}
|
|
Object::Parentheses => {
|
|
surrounding_markers(map, relative_to, around, self.is_multiline(), '(', ')')
|
|
}
|
|
Object::Tag => {
|
|
let head = selection.head();
|
|
let range = selection.range();
|
|
surrounding_html_tag(map, head, range, around)
|
|
}
|
|
Object::AnyBrackets => {
|
|
let cursor_offset = relative_to.to_offset(map, Bias::Left);
|
|
|
|
// Find innermost enclosing bracket range
|
|
let mut innermost = None;
|
|
let mut min_size = usize::MAX;
|
|
|
|
for &SurroundPair { open, close } in BRACKET_PAIRS {
|
|
if let Some(range) = surrounding_markers(
|
|
map,
|
|
relative_to,
|
|
around,
|
|
self.is_multiline(),
|
|
open,
|
|
close,
|
|
) {
|
|
let start_offset = range.start.to_offset(map, Bias::Left);
|
|
let end_offset = range.end.to_offset(map, Bias::Right);
|
|
|
|
if cursor_offset >= start_offset && cursor_offset <= end_offset {
|
|
let size = end_offset - start_offset;
|
|
if size < min_size {
|
|
min_size = size;
|
|
innermost = Some(range);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if let Some(range) = innermost {
|
|
return Some(range);
|
|
}
|
|
|
|
// Fallback: find nearest bracket pair if not inside any
|
|
BRACKET_PAIRS
|
|
.iter()
|
|
.flat_map(|&SurroundPair { open, close }| {
|
|
surrounding_markers(
|
|
map,
|
|
relative_to,
|
|
around,
|
|
self.is_multiline(),
|
|
open,
|
|
close,
|
|
)
|
|
})
|
|
.min_by_key(|range| {
|
|
let start_offset = range.start.to_offset(map, Bias::Left);
|
|
let end_offset = range.end.to_offset(map, Bias::Right);
|
|
if cursor_offset < start_offset {
|
|
(start_offset - cursor_offset) as isize
|
|
} else if cursor_offset > end_offset {
|
|
(cursor_offset - end_offset) as isize
|
|
} else {
|
|
0
|
|
}
|
|
})
|
|
}
|
|
Object::MiniBrackets => find_mini_brackets(map, relative_to, around),
|
|
Object::AnyPair => {
|
|
let cursor = relative_to..movement::right(map, relative_to);
|
|
innermost_surrounding_pair(map, cursor)
|
|
.map(|pair| pair.to_display_range(map, around))
|
|
}
|
|
Object::SquareBrackets => {
|
|
surrounding_markers(map, relative_to, around, self.is_multiline(), '[', ']')
|
|
}
|
|
Object::CurlyBrackets => {
|
|
surrounding_markers(map, relative_to, around, self.is_multiline(), '{', '}')
|
|
}
|
|
Object::AngleBrackets => {
|
|
surrounding_markers(map, relative_to, around, self.is_multiline(), '<', '>')
|
|
}
|
|
Object::Method => text_object(
|
|
map,
|
|
relative_to,
|
|
if around {
|
|
TextObject::AroundFunction
|
|
} else {
|
|
TextObject::InsideFunction
|
|
},
|
|
),
|
|
Object::Comment => text_object(
|
|
map,
|
|
relative_to,
|
|
if around {
|
|
TextObject::AroundComment
|
|
} else {
|
|
TextObject::InsideComment
|
|
},
|
|
),
|
|
Object::Class => text_object(
|
|
map,
|
|
relative_to,
|
|
if around {
|
|
TextObject::AroundClass
|
|
} else {
|
|
TextObject::InsideClass
|
|
},
|
|
),
|
|
Object::Argument => argument(map, relative_to, around),
|
|
Object::IndentObj { include_below } => indent(map, relative_to, around, include_below),
|
|
Object::EntireFile => entire_file(map),
|
|
}
|
|
}
|
|
|
|
pub fn expand_selection(
|
|
self,
|
|
map: &DisplaySnapshot,
|
|
selection: &mut Selection<DisplayPoint>,
|
|
around: bool,
|
|
times: Option<usize>,
|
|
) -> bool {
|
|
if let Some(range) = self.range(map, *selection, around, times) {
|
|
selection.start = range.start;
|
|
selection.end = range.end;
|
|
true
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Returns a range that surrounds the word `relative_to` is in.
|
|
///
|
|
/// If `relative_to` is at the start of a word, return the word.
|
|
/// If `relative_to` is between words, return the space between.
|
|
/// If `times` > 1, extend to include additional words.
|
|
fn in_word(
|
|
map: &DisplaySnapshot,
|
|
relative_to: DisplayPoint,
|
|
ignore_punctuation: bool,
|
|
times: usize,
|
|
) -> Option<Range<DisplayPoint>> {
|
|
// Use motion::right so that we consider the character under the cursor when looking for the start
|
|
let classifier = map
|
|
.buffer_snapshot()
|
|
.char_classifier_at(relative_to.to_point(map))
|
|
.ignore_punctuation(ignore_punctuation);
|
|
let start = movement::find_preceding_boundary_display_point(
|
|
map,
|
|
right(map, relative_to, 1),
|
|
movement::FindRange::SingleLine,
|
|
&mut |left, right| classifier.kind(left) != classifier.kind(right),
|
|
);
|
|
|
|
let mut end = movement::find_boundary(
|
|
map,
|
|
relative_to,
|
|
FindRange::SingleLine,
|
|
&mut |left, right| classifier.kind(left) != classifier.kind(right),
|
|
);
|
|
|
|
let mut is_boundary = |left: char, right: char| classifier.kind(left) != classifier.kind(right);
|
|
|
|
for _ in 1..times {
|
|
let kind_at_end = map
|
|
.buffer_chars_at(end.to_offset(map, Bias::Right))
|
|
.next()
|
|
.map(|(c, _)| classifier.kind(c));
|
|
|
|
// Skip whitespace but not punctuation (punctuation is its own word unit).
|
|
let next_end = if kind_at_end == Some(CharKind::Whitespace) {
|
|
let after_whitespace =
|
|
movement::find_boundary(map, end, FindRange::MultiLine, &mut is_boundary);
|
|
movement::find_boundary(
|
|
map,
|
|
after_whitespace,
|
|
FindRange::MultiLine,
|
|
&mut is_boundary,
|
|
)
|
|
} else {
|
|
movement::find_boundary(map, end, FindRange::MultiLine, &mut is_boundary)
|
|
};
|
|
if next_end == end {
|
|
break;
|
|
}
|
|
end = next_end;
|
|
}
|
|
|
|
Some(start..end)
|
|
}
|
|
|
|
fn in_subword(
|
|
map: &DisplaySnapshot,
|
|
relative_to: DisplayPoint,
|
|
ignore_punctuation: bool,
|
|
) -> Option<Range<DisplayPoint>> {
|
|
let offset = relative_to.to_offset(map, Bias::Left);
|
|
// Use motion::right so that we consider the character under the cursor when looking for the start
|
|
let classifier = map
|
|
.buffer_snapshot()
|
|
.char_classifier_at(relative_to.to_point(map))
|
|
.ignore_punctuation(ignore_punctuation);
|
|
let in_subword = map
|
|
.buffer_chars_at(offset)
|
|
.next()
|
|
.map(|(c, _)| {
|
|
let is_separator = "._-".contains(c);
|
|
!classifier.is_whitespace(c) && !is_separator
|
|
})
|
|
.unwrap_or(false);
|
|
|
|
let start = if in_subword {
|
|
movement::find_preceding_boundary_display_point(
|
|
map,
|
|
right(map, relative_to, 1),
|
|
movement::FindRange::SingleLine,
|
|
&mut |left, right| {
|
|
let is_word_start = classifier.kind(left) != classifier.kind(right);
|
|
is_word_start || is_subword_start(left, right, "._-")
|
|
},
|
|
)
|
|
} else {
|
|
movement::find_boundary(
|
|
map,
|
|
relative_to,
|
|
FindRange::SingleLine,
|
|
&mut |left, right| {
|
|
let is_word_start = classifier.kind(left) != classifier.kind(right);
|
|
is_word_start || is_subword_start(left, right, "._-")
|
|
},
|
|
)
|
|
};
|
|
|
|
let end = movement::find_boundary(
|
|
map,
|
|
relative_to,
|
|
FindRange::SingleLine,
|
|
&mut |left, right| {
|
|
let is_word_end = classifier.kind(left) != classifier.kind(right);
|
|
is_word_end || is_subword_end(left, right, "._-")
|
|
},
|
|
);
|
|
|
|
Some(start..end)
|
|
}
|
|
|
|
pub fn surrounding_html_tag(
|
|
map: &DisplaySnapshot,
|
|
head: DisplayPoint,
|
|
range: Range<DisplayPoint>,
|
|
around: bool,
|
|
) -> Option<Range<DisplayPoint>> {
|
|
fn read_tag(chars: impl Iterator<Item = char>) -> String {
|
|
chars
|
|
.take_while(|c| c.is_alphanumeric() || *c == ':' || *c == '-' || *c == '_' || *c == '.')
|
|
.collect()
|
|
}
|
|
fn open_tag(mut chars: impl Iterator<Item = char>) -> Option<String> {
|
|
if Some('<') != chars.next() {
|
|
return None;
|
|
}
|
|
Some(read_tag(chars))
|
|
}
|
|
fn close_tag(mut chars: impl Iterator<Item = char>) -> Option<String> {
|
|
if (Some('<'), Some('/')) != (chars.next(), chars.next()) {
|
|
return None;
|
|
}
|
|
Some(read_tag(chars))
|
|
}
|
|
|
|
let snapshot = &map.buffer_snapshot();
|
|
let head_offset = head.to_offset(map, Bias::Left);
|
|
let range_start = range.start.to_offset(map, Bias::Left);
|
|
let range_end = range.end.to_offset(map, Bias::Left);
|
|
let head_is_start = head_offset <= range_start;
|
|
|
|
let results = snapshot.map_excerpt_ranges(
|
|
range_start..range_end,
|
|
|buffer, _excerpt_range, input_buffer_range| {
|
|
let buffer_offset = if head_is_start {
|
|
input_buffer_range.start
|
|
} else {
|
|
input_buffer_range.end
|
|
};
|
|
|
|
let Some(layer) = buffer.syntax_layer_at(buffer_offset) else {
|
|
return Vec::new();
|
|
};
|
|
let mut cursor = layer.node().walk();
|
|
let mut last_child_node = cursor.node();
|
|
while cursor.goto_first_child_for_byte(buffer_offset.0).is_some() {
|
|
last_child_node = cursor.node();
|
|
}
|
|
|
|
let mut last_child_node = Some(last_child_node);
|
|
while let Some(cur_node) = last_child_node {
|
|
if cur_node.child_count() >= 2 {
|
|
let first_child = cur_node.child(0);
|
|
let last_child = cur_node.child(cur_node.child_count() - 1);
|
|
if let (Some(first_child), Some(last_child)) = (first_child, last_child) {
|
|
let open_tag = open_tag(buffer.chars_for_range(first_child.byte_range()));
|
|
let close_tag = close_tag(buffer.chars_for_range(last_child.byte_range()));
|
|
let is_valid = if range_end.saturating_sub(range_start) <= 1 {
|
|
buffer_offset.0 <= last_child.end_byte()
|
|
} else {
|
|
input_buffer_range.start.0 >= first_child.start_byte()
|
|
&& input_buffer_range.end.0 <= last_child.start_byte() + 1
|
|
};
|
|
if open_tag.is_some() && open_tag == close_tag && is_valid {
|
|
let buffer_range = if around {
|
|
first_child.byte_range().start..last_child.byte_range().end
|
|
} else {
|
|
first_child.byte_range().end..last_child.byte_range().start
|
|
};
|
|
return vec![(
|
|
BufferOffset(buffer_range.start)..BufferOffset(buffer_range.end),
|
|
(),
|
|
)];
|
|
}
|
|
}
|
|
}
|
|
last_child_node = cur_node.parent();
|
|
}
|
|
Vec::new()
|
|
},
|
|
)?;
|
|
|
|
let (result, ()) = results.into_iter().next()?;
|
|
Some(result.start.to_display_point(map)..result.end.to_display_point(map))
|
|
}
|
|
|
|
/// Returns a range that surrounds the word and following whitespace
|
|
/// relative_to is in.
|
|
///
|
|
/// If `relative_to` is at the start of a word, return the word and following whitespace.
|
|
/// If `relative_to` is between words, return the whitespace back and the following word.
|
|
///
|
|
/// if in word
|
|
/// delete that word
|
|
/// if there is whitespace following the word, delete that as well
|
|
/// otherwise, delete any preceding whitespace
|
|
/// otherwise
|
|
/// delete whitespace around cursor
|
|
/// delete word following the cursor
|
|
/// If `times` > 1, extend to include additional words.
|
|
fn around_word(
|
|
map: &DisplaySnapshot,
|
|
relative_to: DisplayPoint,
|
|
ignore_punctuation: bool,
|
|
times: usize,
|
|
) -> Option<Range<DisplayPoint>> {
|
|
let offset = relative_to.to_offset(map, Bias::Left);
|
|
let classifier = map
|
|
.buffer_snapshot()
|
|
.char_classifier_at(offset)
|
|
.ignore_punctuation(ignore_punctuation);
|
|
let in_word = map
|
|
.buffer_chars_at(offset)
|
|
.next()
|
|
.map(|(c, _)| !classifier.is_whitespace(c))
|
|
.unwrap_or(false);
|
|
|
|
if in_word {
|
|
around_containing_word(map, relative_to, ignore_punctuation, times)
|
|
} else {
|
|
around_next_word(map, relative_to, ignore_punctuation, times)
|
|
}
|
|
}
|
|
|
|
fn around_subword(
|
|
map: &DisplaySnapshot,
|
|
relative_to: DisplayPoint,
|
|
ignore_punctuation: bool,
|
|
) -> Option<Range<DisplayPoint>> {
|
|
// Use motion::right so that we consider the character under the cursor when looking for the start
|
|
let classifier = map
|
|
.buffer_snapshot()
|
|
.char_classifier_at(relative_to.to_point(map))
|
|
.ignore_punctuation(ignore_punctuation);
|
|
let start = movement::find_preceding_boundary_display_point(
|
|
map,
|
|
right(map, relative_to, 1),
|
|
movement::FindRange::SingleLine,
|
|
&mut |left, right| {
|
|
let is_separator = |c: char| "._-".contains(c);
|
|
let is_word_start =
|
|
classifier.kind(left) != classifier.kind(right) && !is_separator(left);
|
|
is_word_start || is_subword_start(left, right, "._-")
|
|
},
|
|
);
|
|
|
|
let end = movement::find_boundary(
|
|
map,
|
|
relative_to,
|
|
FindRange::SingleLine,
|
|
&mut |left, right| {
|
|
let is_separator = |c: char| "._-".contains(c);
|
|
let is_word_end =
|
|
classifier.kind(left) != classifier.kind(right) && !is_separator(right);
|
|
is_word_end || is_subword_end(left, right, "._-")
|
|
},
|
|
);
|
|
|
|
Some(start..end).map(|range| expand_to_include_whitespace(map, range, true))
|
|
}
|
|
|
|
fn around_containing_word(
|
|
map: &DisplaySnapshot,
|
|
relative_to: DisplayPoint,
|
|
ignore_punctuation: bool,
|
|
times: usize,
|
|
) -> Option<Range<DisplayPoint>> {
|
|
in_word(map, relative_to, ignore_punctuation, times).map(|range| {
|
|
let spans_multiple_lines = range.start.row() != range.end.row();
|
|
let stop_at_newline = !spans_multiple_lines;
|
|
|
|
let line_start = DisplayPoint::new(range.start.row(), 0);
|
|
let is_first_word = map
|
|
.buffer_chars_at(line_start.to_offset(map, Bias::Left))
|
|
.take_while(|(ch, offset)| {
|
|
offset < &range.start.to_offset(map, Bias::Left) && ch.is_whitespace()
|
|
})
|
|
.count()
|
|
> 0;
|
|
|
|
if is_first_word {
|
|
// For first word on line, trim indentation
|
|
let mut expanded = expand_to_include_whitespace(map, range.clone(), stop_at_newline);
|
|
expanded.start = range.start;
|
|
expanded
|
|
} else {
|
|
expand_to_include_whitespace(map, range, stop_at_newline)
|
|
}
|
|
})
|
|
}
|
|
|
|
fn around_next_word(
|
|
map: &DisplaySnapshot,
|
|
relative_to: DisplayPoint,
|
|
ignore_punctuation: bool,
|
|
times: usize,
|
|
) -> Option<Range<DisplayPoint>> {
|
|
let classifier = map
|
|
.buffer_snapshot()
|
|
.char_classifier_at(relative_to.to_point(map))
|
|
.ignore_punctuation(ignore_punctuation);
|
|
let start = movement::find_preceding_boundary_display_point(
|
|
map,
|
|
right(map, relative_to, 1),
|
|
FindRange::SingleLine,
|
|
&mut |left, right| classifier.kind(left) != classifier.kind(right),
|
|
);
|
|
|
|
let mut word_found = false;
|
|
let mut end = movement::find_boundary(
|
|
map,
|
|
relative_to,
|
|
FindRange::MultiLine,
|
|
&mut |left, right| {
|
|
let left_kind = classifier.kind(left);
|
|
let right_kind = classifier.kind(right);
|
|
|
|
let found = (word_found && left_kind != right_kind) || right == '\n' && left == '\n';
|
|
|
|
if right_kind != CharKind::Whitespace {
|
|
word_found = true;
|
|
}
|
|
|
|
found
|
|
},
|
|
);
|
|
|
|
for _ in 1..times {
|
|
let next_end =
|
|
movement::find_boundary(map, end, FindRange::MultiLine, &mut |left, right| {
|
|
let left_kind = classifier.kind(left);
|
|
let right_kind = classifier.kind(right);
|
|
|
|
let in_word_unit = left_kind != CharKind::Whitespace;
|
|
(in_word_unit && left_kind != right_kind) || right == '\n' && left == '\n'
|
|
});
|
|
if next_end == end {
|
|
break;
|
|
}
|
|
end = next_end;
|
|
}
|
|
|
|
Some(start..end)
|
|
}
|
|
|
|
fn entire_file(map: &DisplaySnapshot) -> Option<Range<DisplayPoint>> {
|
|
Some(DisplayPoint::zero()..map.max_point())
|
|
}
|
|
|
|
fn text_object(
|
|
map: &DisplaySnapshot,
|
|
relative_to: DisplayPoint,
|
|
target: TextObject,
|
|
) -> Option<Range<DisplayPoint>> {
|
|
let snapshot = &map.buffer_snapshot();
|
|
let offset = relative_to.to_offset(map, Bias::Left);
|
|
|
|
let results =
|
|
snapshot.map_excerpt_ranges(offset..offset, |buffer, _excerpt_range, buffer_range| {
|
|
let buffer_offset = buffer_range.start;
|
|
|
|
let mut matches: Vec<Range<usize>> = buffer
|
|
.text_object_ranges(buffer_offset..buffer_offset, TreeSitterOptions::default())
|
|
.filter_map(|(r, m)| if m == target { Some(r) } else { None })
|
|
.collect();
|
|
matches.sort_by_key(|r| r.end - r.start);
|
|
if let Some(buffer_range) = matches.first() {
|
|
return vec![(
|
|
BufferOffset(buffer_range.start)..BufferOffset(buffer_range.end),
|
|
(),
|
|
)];
|
|
}
|
|
|
|
let Some(around) = target.around() else {
|
|
return vec![];
|
|
};
|
|
let mut matches: Vec<Range<usize>> = buffer
|
|
.text_object_ranges(buffer_offset..buffer_offset, TreeSitterOptions::default())
|
|
.filter_map(|(r, m)| if m == around { Some(r) } else { None })
|
|
.collect();
|
|
matches.sort_by_key(|r| r.end - r.start);
|
|
let Some(around_range) = matches.first() else {
|
|
return vec![];
|
|
};
|
|
|
|
let mut matches: Vec<Range<usize>> = buffer
|
|
.text_object_ranges(around_range.clone(), TreeSitterOptions::default())
|
|
.filter_map(|(r, m)| if m == target { Some(r) } else { None })
|
|
.collect();
|
|
matches.sort_by_key(|r| r.start);
|
|
if let Some(buffer_range) = matches.first()
|
|
&& !buffer_range.is_empty()
|
|
{
|
|
return vec![(
|
|
BufferOffset(buffer_range.start)..BufferOffset(buffer_range.end),
|
|
(),
|
|
)];
|
|
}
|
|
vec![(
|
|
BufferOffset(around_range.start)..BufferOffset(around_range.end),
|
|
(),
|
|
)]
|
|
})?;
|
|
|
|
let (range, ()) = results.into_iter().next()?;
|
|
Some(range.start.to_display_point(map)..range.end.to_display_point(map))
|
|
}
|
|
|
|
fn argument(
|
|
map: &DisplaySnapshot,
|
|
relative_to: DisplayPoint,
|
|
around: bool,
|
|
) -> Option<Range<DisplayPoint>> {
|
|
let snapshot = &map.buffer_snapshot();
|
|
let offset = relative_to.to_offset(map, Bias::Left);
|
|
|
|
fn comma_delimited_range_at(
|
|
buffer: &BufferSnapshot,
|
|
mut offset: BufferOffset,
|
|
include_comma: bool,
|
|
) -> Option<Range<BufferOffset>> {
|
|
offset += buffer
|
|
.chars_at(offset)
|
|
.take_while(|c| c.is_whitespace())
|
|
.map(char::len_utf8)
|
|
.sum::<usize>();
|
|
|
|
let bracket_filter = |open: Range<usize>, close: Range<usize>| {
|
|
if open.end == close.start {
|
|
return false;
|
|
}
|
|
|
|
if open.start == offset.0 || close.end == offset.0 {
|
|
return false;
|
|
}
|
|
|
|
matches!(
|
|
buffer.chars_at(open.start).next(),
|
|
Some('(' | '[' | '{' | '<' | '|')
|
|
)
|
|
};
|
|
|
|
let (open_bracket, close_bracket) =
|
|
buffer.innermost_enclosing_bracket_ranges(offset..offset, Some(&bracket_filter))?;
|
|
|
|
let inner_bracket_range = BufferOffset(open_bracket.end)..BufferOffset(close_bracket.start);
|
|
|
|
let layer = buffer.syntax_layer_at(offset)?;
|
|
let node = layer.node();
|
|
let mut cursor = node.walk();
|
|
|
|
let mut parent_covers_bracket_range = false;
|
|
loop {
|
|
let node = cursor.node();
|
|
let range = node.byte_range();
|
|
let covers_bracket_range =
|
|
range.start == open_bracket.start && range.end == close_bracket.end;
|
|
if parent_covers_bracket_range && !covers_bracket_range {
|
|
break;
|
|
}
|
|
parent_covers_bracket_range = covers_bracket_range;
|
|
|
|
cursor.goto_first_child_for_byte(offset.0)?;
|
|
}
|
|
|
|
let mut argument_node = cursor.node();
|
|
|
|
if argument_node.byte_range() == open_bracket {
|
|
if !cursor.goto_next_sibling() {
|
|
return Some(inner_bracket_range);
|
|
}
|
|
argument_node = cursor.node();
|
|
}
|
|
while argument_node.byte_range() == close_bracket || argument_node.kind() == "," {
|
|
if !cursor.goto_previous_sibling() {
|
|
return Some(inner_bracket_range);
|
|
}
|
|
argument_node = cursor.node();
|
|
if argument_node.byte_range() == open_bracket {
|
|
return Some(inner_bracket_range);
|
|
}
|
|
}
|
|
|
|
let mut start = argument_node.start_byte();
|
|
let mut end = argument_node.end_byte();
|
|
|
|
let mut needs_surrounding_comma = include_comma;
|
|
|
|
while cursor.goto_previous_sibling() {
|
|
let prev = cursor.node();
|
|
|
|
if prev.start_byte() < open_bracket.end {
|
|
start = open_bracket.end;
|
|
break;
|
|
} else if prev.kind() == "," {
|
|
if needs_surrounding_comma {
|
|
start = prev.start_byte();
|
|
needs_surrounding_comma = false;
|
|
}
|
|
break;
|
|
} else if prev.start_byte() < start {
|
|
start = prev.start_byte();
|
|
}
|
|
}
|
|
|
|
while cursor.goto_next_sibling() {
|
|
let next = cursor.node();
|
|
|
|
if next.end_byte() > close_bracket.start {
|
|
end = close_bracket.start;
|
|
break;
|
|
} else if next.kind() == "," {
|
|
if needs_surrounding_comma {
|
|
if let Some(next_arg) = next.next_sibling() {
|
|
end = next_arg.start_byte();
|
|
} else {
|
|
end = next.end_byte();
|
|
}
|
|
}
|
|
break;
|
|
} else if next.end_byte() > end {
|
|
end = next.end_byte();
|
|
}
|
|
}
|
|
|
|
Some(BufferOffset(start)..BufferOffset(end))
|
|
}
|
|
|
|
let results =
|
|
snapshot.map_excerpt_ranges(offset..offset, |buffer, _excerpt_range, buffer_range| {
|
|
let buffer_offset = buffer_range.start;
|
|
match comma_delimited_range_at(buffer, buffer_offset, around) {
|
|
Some(result) => vec![(result, ())],
|
|
None => vec![],
|
|
}
|
|
})?;
|
|
|
|
let (range, ()) = results.into_iter().next()?;
|
|
Some(range.start.to_display_point(map)..range.end.to_display_point(map))
|
|
}
|
|
|
|
fn indent(
|
|
map: &DisplaySnapshot,
|
|
relative_to: DisplayPoint,
|
|
around: bool,
|
|
include_below: bool,
|
|
) -> Option<Range<DisplayPoint>> {
|
|
let point = relative_to.to_point(map);
|
|
let row = point.row;
|
|
|
|
let desired_indent = map.line_indent_for_buffer_row(MultiBufferRow(row));
|
|
|
|
// Loop backwards until we find a non-blank line with less indent
|
|
let mut start_row = row;
|
|
for prev_row in (0..row).rev() {
|
|
let indent = map.line_indent_for_buffer_row(MultiBufferRow(prev_row));
|
|
if indent.is_line_empty() {
|
|
continue;
|
|
}
|
|
if indent.spaces < desired_indent.spaces || indent.tabs < desired_indent.tabs {
|
|
if around {
|
|
// When around is true, include the first line with less indent
|
|
start_row = prev_row;
|
|
}
|
|
break;
|
|
}
|
|
start_row = prev_row;
|
|
}
|
|
|
|
// Loop forwards until we find a non-blank line with less indent
|
|
let mut end_row = row;
|
|
let max_rows = map.buffer_snapshot().max_row().0;
|
|
for next_row in (row + 1)..=max_rows {
|
|
let indent = map.line_indent_for_buffer_row(MultiBufferRow(next_row));
|
|
if indent.is_line_empty() {
|
|
continue;
|
|
}
|
|
if indent.spaces < desired_indent.spaces || indent.tabs < desired_indent.tabs {
|
|
if around && include_below {
|
|
// When around is true and including below, include this line
|
|
end_row = next_row;
|
|
}
|
|
break;
|
|
}
|
|
end_row = next_row;
|
|
}
|
|
|
|
let end_len = map.buffer_snapshot().line_len(MultiBufferRow(end_row));
|
|
let start = map.point_to_display_point(Point::new(start_row, 0), Bias::Right);
|
|
let end = map.point_to_display_point(Point::new(end_row, end_len), Bias::Left);
|
|
Some(start..end)
|
|
}
|
|
|
|
fn sentence(
|
|
map: &DisplaySnapshot,
|
|
relative_to: DisplayPoint,
|
|
around: bool,
|
|
) -> Option<Range<DisplayPoint>> {
|
|
let mut start = None;
|
|
let relative_offset = relative_to.to_offset(map, Bias::Left);
|
|
let mut previous_end = relative_offset;
|
|
|
|
let mut chars = map.buffer_chars_at(previous_end).peekable();
|
|
|
|
// Search backwards for the previous sentence end or current sentence start. Include the character under relative_to
|
|
for (char, offset) in chars
|
|
.peek()
|
|
.cloned()
|
|
.into_iter()
|
|
.chain(map.reverse_buffer_chars_at(previous_end))
|
|
{
|
|
if is_sentence_end(map, offset) {
|
|
break;
|
|
}
|
|
|
|
if is_possible_sentence_start(char) {
|
|
start = Some(offset);
|
|
}
|
|
|
|
previous_end = offset;
|
|
}
|
|
|
|
// Search forward for the end of the current sentence or if we are between sentences, the start of the next one
|
|
let mut end = relative_offset;
|
|
for (char, offset) in chars {
|
|
if start.is_none() && is_possible_sentence_start(char) {
|
|
if around {
|
|
start = Some(offset);
|
|
continue;
|
|
} else {
|
|
end = offset;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if char != '\n' {
|
|
end = offset + char.len_utf8();
|
|
}
|
|
|
|
if is_sentence_end(map, end) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
let mut range = start.unwrap_or(previous_end).to_display_point(map)..end.to_display_point(map);
|
|
if around {
|
|
range = expand_to_include_whitespace(map, range, false);
|
|
}
|
|
|
|
Some(range)
|
|
}
|
|
|
|
fn is_possible_sentence_start(character: char) -> bool {
|
|
!character.is_whitespace() && character != '.'
|
|
}
|
|
|
|
const SENTENCE_END_PUNCTUATION: &[char] = &['.', '!', '?'];
|
|
const SENTENCE_END_FILLERS: &[char] = &[')', ']', '"', '\''];
|
|
const SENTENCE_END_WHITESPACE: &[char] = &[' ', '\t', '\n'];
|
|
fn is_sentence_end(map: &DisplaySnapshot, offset: MultiBufferOffset) -> bool {
|
|
let mut next_chars = map.buffer_chars_at(offset).peekable();
|
|
if let Some((char, _)) = next_chars.next() {
|
|
// We are at a double newline. This position is a sentence end.
|
|
if char == '\n' && next_chars.peek().map(|(c, _)| c == &'\n').unwrap_or(false) {
|
|
return true;
|
|
}
|
|
|
|
// The next text is not a valid whitespace. This is not a sentence end
|
|
if !SENTENCE_END_WHITESPACE.contains(&char) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
for (char, _) in map.reverse_buffer_chars_at(offset) {
|
|
if SENTENCE_END_PUNCTUATION.contains(&char) {
|
|
return true;
|
|
}
|
|
|
|
if !SENTENCE_END_FILLERS.contains(&char) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
false
|
|
}
|
|
|
|
/// Expands the passed range to include whitespace on one side or the other in a line. Attempts to add the
|
|
/// whitespace to the end first and falls back to the start if there was none.
|
|
pub fn expand_to_include_whitespace(
|
|
map: &DisplaySnapshot,
|
|
range: Range<DisplayPoint>,
|
|
stop_at_newline: bool,
|
|
) -> Range<DisplayPoint> {
|
|
let mut range = range.start.to_offset(map, Bias::Left)..range.end.to_offset(map, Bias::Right);
|
|
let mut whitespace_included = false;
|
|
|
|
let chars = map.buffer_chars_at(range.end).peekable();
|
|
for (char, offset) in chars {
|
|
if char == '\n' && stop_at_newline {
|
|
break;
|
|
}
|
|
|
|
if char.is_whitespace() {
|
|
if char != '\n' || !stop_at_newline {
|
|
range.end = offset + char.len_utf8();
|
|
whitespace_included = true;
|
|
}
|
|
} else {
|
|
// Found non whitespace. Quit out.
|
|
break;
|
|
}
|
|
}
|
|
|
|
if !whitespace_included {
|
|
for (char, point) in map.reverse_buffer_chars_at(range.start) {
|
|
if char == '\n' && stop_at_newline {
|
|
break;
|
|
}
|
|
|
|
if !char.is_whitespace() {
|
|
break;
|
|
}
|
|
|
|
range.start = point;
|
|
}
|
|
}
|
|
|
|
range.start.to_display_point(map)..range.end.to_display_point(map)
|
|
}
|
|
|
|
/// If not `around` (i.e. inner), returns a range that surrounds the paragraph
|
|
/// where `relative_to` is in. If `around`, principally returns the range ending
|
|
/// at the end of the next paragraph.
|
|
///
|
|
/// Here, the "paragraph" is defined as a block of non-blank lines or a block of
|
|
/// blank lines. If the paragraph ends with a trailing newline (i.e. not with
|
|
/// EOF), the returned range ends at the trailing newline of the paragraph (i.e.
|
|
/// the trailing newline is not subject to subsequent operations).
|
|
///
|
|
/// Edge cases:
|
|
/// - If `around` and if the current paragraph is the last paragraph of the
|
|
/// file and is blank, then the selection results in an error.
|
|
/// - If `around` and if the current paragraph is the last paragraph of the
|
|
/// file and is not blank, then the returned range starts at the start of the
|
|
/// previous paragraph, if it exists.
|
|
fn paragraph(
|
|
map: &DisplaySnapshot,
|
|
relative_to: DisplayPoint,
|
|
around: bool,
|
|
times: usize,
|
|
) -> Option<Range<DisplayPoint>> {
|
|
let mut paragraph_start = start_of_paragraph(map, relative_to);
|
|
let mut paragraph_end = end_of_paragraph(map, relative_to);
|
|
|
|
for i in 0..times {
|
|
let paragraph_end_row = paragraph_end.row();
|
|
let paragraph_ends_with_eof = paragraph_end_row == map.max_point().row();
|
|
let point = relative_to.to_point(map);
|
|
let current_line_is_empty = map
|
|
.buffer_snapshot()
|
|
.is_line_blank(MultiBufferRow(point.row));
|
|
|
|
if around {
|
|
if paragraph_ends_with_eof {
|
|
if current_line_is_empty {
|
|
return None;
|
|
}
|
|
|
|
let paragraph_start_buffer_point = paragraph_start.to_point(map);
|
|
if paragraph_start_buffer_point.row != 0 {
|
|
let previous_paragraph_last_line_start =
|
|
Point::new(paragraph_start_buffer_point.row - 1, 0).to_display_point(map);
|
|
paragraph_start = start_of_paragraph(map, previous_paragraph_last_line_start);
|
|
}
|
|
} else {
|
|
let paragraph_end_buffer_point = paragraph_end.to_point(map);
|
|
let mut start_row = paragraph_end_buffer_point.row + 1;
|
|
if i > 0 {
|
|
start_row += 1;
|
|
}
|
|
let next_paragraph_start = Point::new(start_row, 0).to_display_point(map);
|
|
paragraph_end = end_of_paragraph(map, next_paragraph_start);
|
|
}
|
|
}
|
|
}
|
|
|
|
let range = paragraph_start..paragraph_end;
|
|
Some(range)
|
|
}
|
|
|
|
/// Returns a position of the start of the current paragraph, where a paragraph
|
|
/// is defined as a run of non-blank lines or a run of blank lines.
|
|
pub fn start_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
|
|
let point = display_point.to_point(map);
|
|
if point.row == 0 {
|
|
return DisplayPoint::zero();
|
|
}
|
|
|
|
let is_current_line_blank = map
|
|
.buffer_snapshot()
|
|
.is_line_blank(MultiBufferRow(point.row));
|
|
|
|
for row in (0..point.row).rev() {
|
|
let blank = map.buffer_snapshot().is_line_blank(MultiBufferRow(row));
|
|
if blank != is_current_line_blank {
|
|
return Point::new(row + 1, 0).to_display_point(map);
|
|
}
|
|
}
|
|
|
|
DisplayPoint::zero()
|
|
}
|
|
|
|
/// Returns a position of the end of the current paragraph, where a paragraph
|
|
/// is defined as a run of non-blank lines or a run of blank lines.
|
|
/// The trailing newline is excluded from the paragraph.
|
|
pub fn end_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
|
|
let point = display_point.to_point(map);
|
|
if point.row == map.buffer_snapshot().max_row().0 {
|
|
return map.max_point();
|
|
}
|
|
|
|
let is_current_line_blank = map
|
|
.buffer_snapshot()
|
|
.is_line_blank(MultiBufferRow(point.row));
|
|
|
|
for row in point.row + 1..map.buffer_snapshot().max_row().0 + 1 {
|
|
let blank = map.buffer_snapshot().is_line_blank(MultiBufferRow(row));
|
|
if blank != is_current_line_blank {
|
|
let previous_row = row - 1;
|
|
return Point::new(
|
|
previous_row,
|
|
map.buffer_snapshot().line_len(MultiBufferRow(previous_row)),
|
|
)
|
|
.to_display_point(map);
|
|
}
|
|
}
|
|
|
|
map.max_point()
|
|
}
|
|
|
|
pub fn surrounding_markers(
|
|
map: &DisplaySnapshot,
|
|
relative_to: DisplayPoint,
|
|
around: bool,
|
|
search_across_lines: bool,
|
|
open_marker: char,
|
|
close_marker: char,
|
|
) -> Option<Range<DisplayPoint>> {
|
|
let point = relative_to.to_offset(map, Bias::Left);
|
|
|
|
let mut matched_closes = 0;
|
|
let mut opening = None;
|
|
|
|
let mut before_ch = match movement::chars_before(map, point).next() {
|
|
Some((ch, _)) => ch,
|
|
_ => '\0',
|
|
};
|
|
if let Some((ch, range)) = movement::chars_after(map, point).next()
|
|
&& ch == open_marker
|
|
&& before_ch != '\\'
|
|
{
|
|
if open_marker == close_marker {
|
|
let mut total = 0;
|
|
for ((ch, _), (before_ch, _)) in movement::chars_before(map, point).tuple_windows() {
|
|
if ch == '\n' {
|
|
break;
|
|
}
|
|
if ch == open_marker && before_ch != '\\' {
|
|
total += 1;
|
|
}
|
|
}
|
|
if total % 2 == 0 {
|
|
opening = Some(range)
|
|
}
|
|
} else {
|
|
opening = Some(range)
|
|
}
|
|
}
|
|
|
|
if opening.is_none() {
|
|
let mut chars_before = movement::chars_before(map, point).peekable();
|
|
while let Some((ch, range)) = chars_before.next() {
|
|
if ch == '\n' && !search_across_lines {
|
|
break;
|
|
}
|
|
|
|
if let Some((before_ch, _)) = chars_before.peek()
|
|
&& *before_ch == '\\'
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if ch == open_marker {
|
|
if matched_closes == 0 {
|
|
opening = Some(range);
|
|
break;
|
|
}
|
|
matched_closes -= 1;
|
|
} else if ch == close_marker {
|
|
matched_closes += 1
|
|
}
|
|
}
|
|
}
|
|
if opening.is_none() {
|
|
for (ch, range) in movement::chars_after(map, point) {
|
|
if before_ch != '\\' {
|
|
if ch == open_marker {
|
|
opening = Some(range);
|
|
break;
|
|
} else if ch == close_marker {
|
|
break;
|
|
}
|
|
}
|
|
|
|
before_ch = ch;
|
|
}
|
|
}
|
|
|
|
let mut opening = opening?;
|
|
|
|
let mut matched_opens = 0;
|
|
let mut closing = None;
|
|
before_ch = match movement::chars_before(map, opening.end).next() {
|
|
Some((ch, _)) => ch,
|
|
_ => '\0',
|
|
};
|
|
for (ch, range) in movement::chars_after(map, opening.end) {
|
|
if ch == '\n' && !search_across_lines {
|
|
break;
|
|
}
|
|
|
|
if before_ch != '\\' {
|
|
if ch == close_marker {
|
|
if matched_opens == 0 {
|
|
closing = Some(range);
|
|
break;
|
|
}
|
|
matched_opens -= 1;
|
|
} else if ch == open_marker {
|
|
matched_opens += 1;
|
|
}
|
|
}
|
|
|
|
before_ch = ch;
|
|
}
|
|
|
|
let mut closing = closing?;
|
|
|
|
if around && !search_across_lines {
|
|
let mut found = false;
|
|
|
|
for (ch, range) in movement::chars_after(map, closing.end) {
|
|
if ch.is_whitespace() && ch != '\n' {
|
|
found = true;
|
|
closing.end = range.end;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if !found {
|
|
for (ch, range) in movement::chars_before(map, opening.start) {
|
|
if ch.is_whitespace() && ch != '\n' {
|
|
opening.start = range.start
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Adjust selection to remove leading and trailing whitespace for multiline inner brackets
|
|
if !around && open_marker != close_marker {
|
|
let start_point = opening.end.to_display_point(map);
|
|
let end_point = closing.start.to_display_point(map);
|
|
let start_offset = start_point.to_offset(map, Bias::Left);
|
|
let end_offset = end_point.to_offset(map, Bias::Left);
|
|
|
|
if start_point.row() != end_point.row()
|
|
&& map
|
|
.buffer_chars_at(start_offset)
|
|
.take_while(|(_, offset)| offset < &end_offset)
|
|
.any(|(ch, _)| !ch.is_whitespace())
|
|
{
|
|
let mut first_non_ws = None;
|
|
let mut last_non_ws = None;
|
|
for (ch, offset) in map.buffer_chars_at(start_offset) {
|
|
if !ch.is_whitespace() {
|
|
first_non_ws = Some(offset);
|
|
break;
|
|
}
|
|
}
|
|
for (ch, offset) in map.reverse_buffer_chars_at(end_offset) {
|
|
if !ch.is_whitespace() {
|
|
last_non_ws = Some(offset + ch.len_utf8());
|
|
break;
|
|
}
|
|
}
|
|
if let Some(start) = first_non_ws {
|
|
opening.end = start;
|
|
}
|
|
if let Some(end) = last_non_ws {
|
|
closing.start = end;
|
|
}
|
|
}
|
|
}
|
|
|
|
let result = if around {
|
|
opening.start..closing.end
|
|
} else {
|
|
opening.end..closing.start
|
|
};
|
|
|
|
Some(
|
|
map.clip_point(result.start.to_display_point(map), Bias::Left)
|
|
..map.clip_point(result.end.to_display_point(map), Bias::Right),
|
|
)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
use editor::{Editor, EditorMode, MultiBuffer, test::editor_test_context::EditorTestContext};
|
|
use gpui::KeyBinding;
|
|
use indoc::indoc;
|
|
use text::Point;
|
|
|
|
use crate::{
|
|
object::{AnyBrackets, AnyQuotes, MiniBrackets},
|
|
state::Mode,
|
|
test::{NeovimBackedTestContext, VimTestContext},
|
|
};
|
|
|
|
const WORD_LOCATIONS: &str = indoc! {"
|
|
The quick ˇbrowˇnˇ•••
|
|
fox ˇjuˇmpsˇ over
|
|
the lazy dogˇ••
|
|
ˇ
|
|
ˇ
|
|
ˇ
|
|
Thˇeˇ-ˇquˇickˇ ˇbrownˇ•
|
|
ˇ••
|
|
ˇ••
|
|
ˇ fox-jumpˇs over
|
|
the lazy dogˇ•
|
|
ˇ
|
|
"
|
|
};
|
|
|
|
#[gpui::test]
|
|
async fn test_change_word_object(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
|
|
|
cx.simulate_at_each_offset("c i w", WORD_LOCATIONS)
|
|
.await
|
|
.assert_matches();
|
|
cx.simulate_at_each_offset("c i shift-w", WORD_LOCATIONS)
|
|
.await
|
|
.assert_matches();
|
|
cx.simulate_at_each_offset("c a w", WORD_LOCATIONS)
|
|
.await
|
|
.assert_matches();
|
|
cx.simulate_at_each_offset("c a shift-w", WORD_LOCATIONS)
|
|
.await
|
|
.assert_matches();
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_delete_word_object(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
|
|
|
cx.simulate_at_each_offset("d i w", WORD_LOCATIONS)
|
|
.await
|
|
.assert_matches();
|
|
cx.simulate_at_each_offset("d i shift-w", WORD_LOCATIONS)
|
|
.await
|
|
.assert_matches();
|
|
cx.simulate_at_each_offset("d a w", WORD_LOCATIONS)
|
|
.await
|
|
.assert_matches();
|
|
cx.simulate_at_each_offset("d a shift-w", WORD_LOCATIONS)
|
|
.await
|
|
.assert_matches();
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_visual_word_object(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
|
|
|
/*
|
|
cx.set_shared_state("The quick ˇbrown\nfox").await;
|
|
cx.simulate_shared_keystrokes(["v"]).await;
|
|
cx.assert_shared_state("The quick «bˇ»rown\nfox").await;
|
|
cx.simulate_shared_keystrokes(["i", "w"]).await;
|
|
cx.assert_shared_state("The quick «brownˇ»\nfox").await;
|
|
*/
|
|
cx.set_shared_state("The quick brown\nˇ\nfox").await;
|
|
cx.simulate_shared_keystrokes("v").await;
|
|
cx.shared_state()
|
|
.await
|
|
.assert_eq("The quick brown\n«\nˇ»fox");
|
|
cx.simulate_shared_keystrokes("i w").await;
|
|
cx.shared_state()
|
|
.await
|
|
.assert_eq("The quick brown\n«\nˇ»fox");
|
|
|
|
cx.simulate_at_each_offset("v i w", WORD_LOCATIONS)
|
|
.await
|
|
.assert_matches();
|
|
cx.simulate_at_each_offset("v i shift-w", WORD_LOCATIONS)
|
|
.await
|
|
.assert_matches();
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_word_object_with_count(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
|
|
|
cx.set_shared_state("ˇone two three four").await;
|
|
cx.simulate_shared_keystrokes("2 d a w").await;
|
|
cx.shared_state().await.assert_matches();
|
|
|
|
cx.set_shared_state("ˇone two three four").await;
|
|
cx.simulate_shared_keystrokes("d 2 a w").await;
|
|
cx.shared_state().await.assert_matches();
|
|
|
|
// WORD (shift-w) ignores punctuation
|
|
cx.set_shared_state("ˇone-two three-four five").await;
|
|
cx.simulate_shared_keystrokes("2 d a shift-w").await;
|
|
cx.shared_state().await.assert_matches();
|
|
|
|
cx.set_shared_state("ˇone two three four five").await;
|
|
cx.simulate_shared_keystrokes("3 d a w").await;
|
|
cx.shared_state().await.assert_matches();
|
|
|
|
// Multiplied counts: 2d2aw deletes 4 words (2*2)
|
|
cx.set_shared_state("ˇone two three four five six").await;
|
|
cx.simulate_shared_keystrokes("2 d 2 a w").await;
|
|
cx.shared_state().await.assert_matches();
|
|
|
|
cx.set_shared_state("ˇone two three four").await;
|
|
cx.simulate_shared_keystrokes("2 c a w").await;
|
|
cx.shared_state().await.assert_matches();
|
|
|
|
cx.set_shared_state("ˇone two three four").await;
|
|
cx.simulate_shared_keystrokes("2 y a w p").await;
|
|
cx.shared_state().await.assert_matches();
|
|
|
|
// Punctuation: foo-bar is 3 word units (foo, -, bar), so 2aw selects "foo-"
|
|
cx.set_shared_state(" ˇfoo-bar baz").await;
|
|
cx.simulate_shared_keystrokes("2 d a w").await;
|
|
cx.shared_state().await.assert_matches();
|
|
|
|
// Trailing whitespace counts as a word unit for iw
|
|
cx.set_shared_state("ˇfoo ").await;
|
|
cx.simulate_shared_keystrokes("2 d i w").await;
|
|
cx.shared_state().await.assert_matches();
|
|
|
|
// Multi-line: count > 1 crosses line boundaries
|
|
cx.set_shared_state("ˇone\ntwo\nthree").await;
|
|
cx.simulate_shared_keystrokes("2 d a w").await;
|
|
cx.shared_state().await.assert_matches();
|
|
|
|
cx.set_shared_state("ˇone\ntwo\nthree\nfour").await;
|
|
cx.simulate_shared_keystrokes("3 d a w").await;
|
|
cx.shared_state().await.assert_matches();
|
|
|
|
cx.set_shared_state("ˇone\ntwo\nthree").await;
|
|
cx.simulate_shared_keystrokes("2 d i w").await;
|
|
cx.shared_state().await.assert_matches();
|
|
|
|
cx.set_shared_state("one ˇtwo\nthree four").await;
|
|
cx.simulate_shared_keystrokes("2 d a w").await;
|
|
cx.shared_state().await.assert_matches();
|
|
}
|
|
|
|
const PARAGRAPH_EXAMPLES: &[&str] = &[
|
|
// Single line
|
|
"ˇThe quick brown fox jumpˇs over the lazy dogˇ.ˇ",
|
|
// Multiple lines without empty lines
|
|
indoc! {"
|
|
ˇThe quick brownˇ
|
|
ˇfox jumps overˇ
|
|
the lazy dog.ˇ
|
|
"},
|
|
// Heading blank paragraph and trailing normal paragraph
|
|
indoc! {"
|
|
ˇ
|
|
ˇ
|
|
ˇThe quick brown fox jumps
|
|
ˇover the lazy dog.
|
|
ˇ
|
|
ˇ
|
|
ˇThe quick brown fox jumpsˇ
|
|
ˇover the lazy dog.ˇ
|
|
"},
|
|
// Inserted blank paragraph and trailing blank paragraph
|
|
indoc! {"
|
|
ˇThe quick brown fox jumps
|
|
ˇover the lazy dog.
|
|
ˇ
|
|
ˇ
|
|
ˇ
|
|
ˇThe quick brown fox jumpsˇ
|
|
ˇover the lazy dog.ˇ
|
|
ˇ
|
|
ˇ
|
|
ˇ
|
|
"},
|
|
// "Blank" paragraph with whitespace characters
|
|
indoc! {"
|
|
ˇThe quick brown fox jumps
|
|
over the lazy dog.
|
|
|
|
ˇ \t
|
|
|
|
ˇThe quick brown fox jumps
|
|
over the lazy dog.ˇ
|
|
ˇ
|
|
ˇ \t
|
|
\t \t
|
|
"},
|
|
// Single line "paragraphs", where selection size might be zero.
|
|
indoc! {"
|
|
ˇThe quick brown fox jumps over the lazy dog.
|
|
ˇ
|
|
ˇThe quick brown fox jumpˇs over the lazy dog.ˇ
|
|
ˇ
|
|
"},
|
|
];
|
|
|
|
#[gpui::test]
|
|
async fn test_change_paragraph_object(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
|
|
|
for paragraph_example in PARAGRAPH_EXAMPLES {
|
|
cx.simulate_at_each_offset("c i p", paragraph_example)
|
|
.await
|
|
.assert_matches();
|
|
cx.simulate_at_each_offset("c a p", paragraph_example)
|
|
.await
|
|
.assert_matches();
|
|
}
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_delete_paragraph_object(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
|
|
|
for paragraph_example in PARAGRAPH_EXAMPLES {
|
|
cx.simulate_at_each_offset("d i p", paragraph_example)
|
|
.await
|
|
.assert_matches();
|
|
cx.simulate_at_each_offset("d a p", paragraph_example)
|
|
.await
|
|
.assert_matches();
|
|
}
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_visual_paragraph_object(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
|
|
|
const EXAMPLES: &[&str] = &[
|
|
indoc! {"
|
|
ˇThe quick brown
|
|
fox jumps over
|
|
the lazy dog.
|
|
"},
|
|
indoc! {"
|
|
ˇ
|
|
|
|
ˇThe quick brown fox jumps
|
|
over the lazy dog.
|
|
ˇ
|
|
|
|
ˇThe quick brown fox jumps
|
|
over the lazy dog.
|
|
"},
|
|
indoc! {"
|
|
ˇThe quick brown fox jumps over the lazy dog.
|
|
ˇ
|
|
ˇThe quick brown fox jumps over the lazy dog.
|
|
|
|
"},
|
|
];
|
|
|
|
for paragraph_example in EXAMPLES {
|
|
cx.simulate_at_each_offset("v i p", paragraph_example)
|
|
.await
|
|
.assert_matches();
|
|
cx.simulate_at_each_offset("v a p", paragraph_example)
|
|
.await
|
|
.assert_matches();
|
|
}
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_change_paragraph_object_with_soft_wrap(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
|
|
|
const WRAPPING_EXAMPLE: &str = indoc! {"
|
|
ˇFirst paragraph with very long text that will wrap when soft wrap is enabled and line length is ˇlimited making it span multiple display lines.
|
|
|
|
ˇSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and ˇshould be handled correctly.
|
|
|
|
ˇThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping ˇsettings.ˇ
|
|
"};
|
|
|
|
cx.set_shared_wrap(20).await;
|
|
|
|
cx.simulate_at_each_offset("c i p", WRAPPING_EXAMPLE)
|
|
.await
|
|
.assert_matches();
|
|
cx.simulate_at_each_offset("c a p", WRAPPING_EXAMPLE)
|
|
.await
|
|
.assert_matches();
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_delete_paragraph_object_with_soft_wrap(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
|
|
|
const WRAPPING_EXAMPLE: &str = indoc! {"
|
|
ˇFirst paragraph with very long text that will wrap when soft wrap is enabled and line length is ˇlimited making it span multiple display lines.
|
|
|
|
ˇSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and ˇshould be handled correctly.
|
|
|
|
ˇThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping ˇsettings.ˇ
|
|
"};
|
|
|
|
cx.set_shared_wrap(20).await;
|
|
|
|
cx.simulate_at_each_offset("d i p", WRAPPING_EXAMPLE)
|
|
.await
|
|
.assert_matches();
|
|
cx.simulate_at_each_offset("d a p", WRAPPING_EXAMPLE)
|
|
.await
|
|
.assert_matches();
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_delete_paragraph_whitespace(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
|
|
|
cx.set_shared_state(indoc! {"
|
|
a
|
|
ˇ•
|
|
aaaaaaaaaaaaa
|
|
"})
|
|
.await;
|
|
|
|
cx.simulate_shared_keystrokes("d i p").await;
|
|
cx.shared_state().await.assert_eq(indoc! {"
|
|
a
|
|
aaaaaaaˇaaaaaa
|
|
"});
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_visual_paragraph_object_with_soft_wrap(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
|
|
|
const WRAPPING_EXAMPLE: &str = indoc! {"
|
|
ˇFirst paragraph with very long text that will wrap when soft wrap is enabled and line length is ˇlimited making it span multiple display lines.
|
|
|
|
ˇSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and ˇshould be handled correctly.
|
|
|
|
ˇThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping ˇsettings.ˇ
|
|
"};
|
|
|
|
cx.set_shared_wrap(20).await;
|
|
|
|
cx.simulate_at_each_offset("v i p", WRAPPING_EXAMPLE)
|
|
.await
|
|
.assert_matches();
|
|
cx.simulate_at_each_offset("v a p", WRAPPING_EXAMPLE)
|
|
.await
|
|
.assert_matches();
|
|
}
|
|
|
|
// Test string with "`" for opening surrounders and "'" for closing surrounders
|
|
const SURROUNDING_MARKER_STRING: &str = indoc! {"
|
|
ˇTh'ˇe ˇ`ˇ'ˇquˇi`ˇck broˇ'wn`
|
|
'ˇfox juˇmps ov`ˇer
|
|
the ˇlazy d'o`ˇg"};
|
|
|
|
const SURROUNDING_OBJECTS: &[(char, char)] = &[
|
|
('"', '"'), // Double Quote
|
|
('(', ')'), // Parentheses
|
|
];
|
|
|
|
#[gpui::test]
|
|
async fn test_change_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
|
|
|
for (start, end) in SURROUNDING_OBJECTS {
|
|
let marked_string = SURROUNDING_MARKER_STRING
|
|
.replace('`', &start.to_string())
|
|
.replace('\'', &end.to_string());
|
|
|
|
cx.simulate_at_each_offset(&format!("c i {start}"), &marked_string)
|
|
.await
|
|
.assert_matches();
|
|
cx.simulate_at_each_offset(&format!("c i {end}"), &marked_string)
|
|
.await
|
|
.assert_matches();
|
|
cx.simulate_at_each_offset(&format!("c a {start}"), &marked_string)
|
|
.await
|
|
.assert_matches();
|
|
cx.simulate_at_each_offset(&format!("c a {end}"), &marked_string)
|
|
.await
|
|
.assert_matches();
|
|
}
|
|
}
|
|
#[gpui::test]
|
|
async fn test_singleline_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
|
cx.set_shared_wrap(12).await;
|
|
|
|
cx.set_shared_state(indoc! {
|
|
"\"ˇhello world\"!"
|
|
})
|
|
.await;
|
|
cx.simulate_shared_keystrokes("v i \"").await;
|
|
cx.shared_state().await.assert_eq(indoc! {
|
|
"\"«hello worldˇ»\"!"
|
|
});
|
|
|
|
cx.set_shared_state(indoc! {
|
|
"\"hˇello world\"!"
|
|
})
|
|
.await;
|
|
cx.simulate_shared_keystrokes("v i \"").await;
|
|
cx.shared_state().await.assert_eq(indoc! {
|
|
"\"«hello worldˇ»\"!"
|
|
});
|
|
|
|
cx.set_shared_state(indoc! {
|
|
"helˇlo \"world\"!"
|
|
})
|
|
.await;
|
|
cx.simulate_shared_keystrokes("v i \"").await;
|
|
cx.shared_state().await.assert_eq(indoc! {
|
|
"hello \"«worldˇ»\"!"
|
|
});
|
|
|
|
cx.set_shared_state(indoc! {
|
|
"hello \"wˇorld\"!"
|
|
})
|
|
.await;
|
|
cx.simulate_shared_keystrokes("v i \"").await;
|
|
cx.shared_state().await.assert_eq(indoc! {
|
|
"hello \"«worldˇ»\"!"
|
|
});
|
|
|
|
cx.set_shared_state(indoc! {
|
|
"hello \"wˇorld\"!"
|
|
})
|
|
.await;
|
|
cx.simulate_shared_keystrokes("v a \"").await;
|
|
cx.shared_state().await.assert_eq(indoc! {
|
|
"hello« \"world\"ˇ»!"
|
|
});
|
|
|
|
cx.set_shared_state(indoc! {
|
|
"hello \"wˇorld\" !"
|
|
})
|
|
.await;
|
|
cx.simulate_shared_keystrokes("v a \"").await;
|
|
cx.shared_state().await.assert_eq(indoc! {
|
|
"hello «\"world\" ˇ»!"
|
|
});
|
|
|
|
cx.set_shared_state(indoc! {
|
|
"hello \"wˇorld\"•
|
|
goodbye"
|
|
})
|
|
.await;
|
|
cx.simulate_shared_keystrokes("v a \"").await;
|
|
cx.shared_state().await.assert_eq(indoc! {
|
|
"hello «\"world\" ˇ»
|
|
goodbye"
|
|
});
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_multiline_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = VimTestContext::new(cx, true).await;
|
|
|
|
cx.set_state(
|
|
indoc! {
|
|
"func empty(a string) bool {
|
|
if a == \"\" {
|
|
return true
|
|
}
|
|
ˇreturn false
|
|
}"
|
|
},
|
|
Mode::Normal,
|
|
);
|
|
cx.simulate_keystrokes("v i {");
|
|
cx.assert_state(
|
|
indoc! {
|
|
"func empty(a string) bool {
|
|
«if a == \"\" {
|
|
return true
|
|
}
|
|
return falseˇ»
|
|
}"
|
|
},
|
|
Mode::Visual,
|
|
);
|
|
|
|
cx.set_state(
|
|
indoc! {
|
|
"func empty(a string) bool {
|
|
if a == \"\" {
|
|
ˇreturn true
|
|
}
|
|
return false
|
|
}"
|
|
},
|
|
Mode::Normal,
|
|
);
|
|
cx.simulate_keystrokes("v i {");
|
|
cx.assert_state(
|
|
indoc! {
|
|
"func empty(a string) bool {
|
|
if a == \"\" {
|
|
«return trueˇ»
|
|
}
|
|
return false
|
|
}"
|
|
},
|
|
Mode::Visual,
|
|
);
|
|
|
|
cx.set_state(
|
|
indoc! {
|
|
"func empty(a string) bool {
|
|
if a == \"\" ˇ{
|
|
return true
|
|
}
|
|
return false
|
|
}"
|
|
},
|
|
Mode::Normal,
|
|
);
|
|
cx.simulate_keystrokes("v i {");
|
|
cx.assert_state(
|
|
indoc! {
|
|
"func empty(a string) bool {
|
|
if a == \"\" {
|
|
«return trueˇ»
|
|
}
|
|
return false
|
|
}"
|
|
},
|
|
Mode::Visual,
|
|
);
|
|
|
|
cx.set_state(
|
|
indoc! {
|
|
"func empty(a string) bool {
|
|
if a == \"\" {
|
|
return true
|
|
}
|
|
return false
|
|
ˇ}"
|
|
},
|
|
Mode::Normal,
|
|
);
|
|
cx.simulate_keystrokes("v i {");
|
|
cx.assert_state(
|
|
indoc! {
|
|
"func empty(a string) bool {
|
|
«if a == \"\" {
|
|
return true
|
|
}
|
|
return falseˇ»
|
|
}"
|
|
},
|
|
Mode::Visual,
|
|
);
|
|
|
|
cx.set_state(
|
|
indoc! {
|
|
"func empty(a string) bool {
|
|
if a == \"\" {
|
|
ˇ
|
|
|
|
}"
|
|
},
|
|
Mode::Normal,
|
|
);
|
|
cx.simulate_keystrokes("c i {");
|
|
cx.assert_state(
|
|
indoc! {
|
|
"func empty(a string) bool {
|
|
if a == \"\" {ˇ}"
|
|
},
|
|
Mode::Insert,
|
|
);
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_singleline_surrounding_character_objects_with_escape(
|
|
cx: &mut gpui::TestAppContext,
|
|
) {
|
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
|
cx.set_shared_state(indoc! {
|
|
"h\"e\\\"lˇlo \\\"world\"!"
|
|
})
|
|
.await;
|
|
cx.simulate_shared_keystrokes("v i \"").await;
|
|
cx.shared_state().await.assert_eq(indoc! {
|
|
"h\"«e\\\"llo \\\"worldˇ»\"!"
|
|
});
|
|
|
|
cx.set_shared_state(indoc! {
|
|
"hello \"teˇst \\\"inside\\\" world\""
|
|
})
|
|
.await;
|
|
cx.simulate_shared_keystrokes("v i \"").await;
|
|
cx.shared_state().await.assert_eq(indoc! {
|
|
"hello \"«test \\\"inside\\\" worldˇ»\""
|
|
});
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_vertical_bars(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = VimTestContext::new(cx, true).await;
|
|
cx.set_state(
|
|
indoc! {"
|
|
fn boop() {
|
|
baz(ˇ|a, b| { bar(|j, k| { })})
|
|
}"
|
|
},
|
|
Mode::Normal,
|
|
);
|
|
cx.simulate_keystrokes("c i |");
|
|
cx.assert_state(
|
|
indoc! {"
|
|
fn boop() {
|
|
baz(|ˇ| { bar(|j, k| { })})
|
|
}"
|
|
},
|
|
Mode::Insert,
|
|
);
|
|
cx.simulate_keystrokes("escape 1 8 |");
|
|
cx.assert_state(
|
|
indoc! {"
|
|
fn boop() {
|
|
baz(|| { bar(ˇ|j, k| { })})
|
|
}"
|
|
},
|
|
Mode::Normal,
|
|
);
|
|
|
|
cx.simulate_keystrokes("v a |");
|
|
cx.assert_state(
|
|
indoc! {"
|
|
fn boop() {
|
|
baz(|| { bar(«|j, k| ˇ»{ })})
|
|
}"
|
|
},
|
|
Mode::Visual,
|
|
);
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_argument_object(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = VimTestContext::new(cx, true).await;
|
|
|
|
// Generic arguments
|
|
cx.set_state("fn boop<A: ˇDebug, B>() {}", Mode::Normal);
|
|
cx.simulate_keystrokes("v i a");
|
|
cx.assert_state("fn boop<«A: Debugˇ», B>() {}", Mode::Visual);
|
|
|
|
// Function arguments
|
|
cx.set_state(
|
|
"fn boop(ˇarg_a: (Tuple, Of, Types), arg_b: String) {}",
|
|
Mode::Normal,
|
|
);
|
|
cx.simulate_keystrokes("d a a");
|
|
cx.assert_state("fn boop(ˇarg_b: String) {}", Mode::Normal);
|
|
|
|
cx.set_state("std::namespace::test(\"strinˇg\", a.b.c())", Mode::Normal);
|
|
cx.simulate_keystrokes("v a a");
|
|
cx.assert_state("std::namespace::test(«\"string\", ˇ»a.b.c())", Mode::Visual);
|
|
|
|
// Tuple, vec, and array arguments
|
|
cx.set_state(
|
|
"fn boop(arg_a: (Tuple, Ofˇ, Types), arg_b: String) {}",
|
|
Mode::Normal,
|
|
);
|
|
cx.simulate_keystrokes("c i a");
|
|
cx.assert_state(
|
|
"fn boop(arg_a: (Tuple, ˇ, Types), arg_b: String) {}",
|
|
Mode::Insert,
|
|
);
|
|
|
|
// TODO regressed with the up-to-date Rust grammar.
|
|
// cx.set_state("let a = (test::call(), 'p', my_macro!{ˇ});", Mode::Normal);
|
|
// cx.simulate_keystrokes("c a a");
|
|
// cx.assert_state("let a = (test::call(), 'p'ˇ);", Mode::Insert);
|
|
|
|
cx.set_state("let a = [test::call(ˇ), 300];", Mode::Normal);
|
|
cx.simulate_keystrokes("c i a");
|
|
cx.assert_state("let a = [ˇ, 300];", Mode::Insert);
|
|
|
|
cx.set_state(
|
|
"let a = vec![Vec::new(), vecˇ![test::call(), 300]];",
|
|
Mode::Normal,
|
|
);
|
|
cx.simulate_keystrokes("c a a");
|
|
cx.assert_state("let a = vec![Vec::new()ˇ];", Mode::Insert);
|
|
|
|
// Cursor immediately before / after brackets
|
|
cx.set_state("let a = [test::call(first_arg)ˇ]", Mode::Normal);
|
|
cx.simulate_keystrokes("v i a");
|
|
cx.assert_state("let a = [«test::call(first_arg)ˇ»]", Mode::Visual);
|
|
|
|
cx.set_state("let a = [test::callˇ(first_arg)]", Mode::Normal);
|
|
cx.simulate_keystrokes("v i a");
|
|
cx.assert_state("let a = [«test::call(first_arg)ˇ»]", Mode::Visual);
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_indent_object(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = VimTestContext::new(cx, true).await;
|
|
|
|
// Base use case
|
|
cx.set_state(
|
|
indoc! {"
|
|
fn boop() {
|
|
// Comment
|
|
baz();ˇ
|
|
|
|
loop {
|
|
bar(1);
|
|
bar(2);
|
|
}
|
|
|
|
result
|
|
}
|
|
"},
|
|
Mode::Normal,
|
|
);
|
|
cx.simulate_keystrokes("v i i");
|
|
cx.assert_state(
|
|
indoc! {"
|
|
fn boop() {
|
|
« // Comment
|
|
baz();
|
|
|
|
loop {
|
|
bar(1);
|
|
bar(2);
|
|
}
|
|
|
|
resultˇ»
|
|
}
|
|
"},
|
|
Mode::Visual,
|
|
);
|
|
|
|
// Around indent (include line above)
|
|
cx.set_state(
|
|
indoc! {"
|
|
const ABOVE: str = true;
|
|
fn boop() {
|
|
|
|
hello();
|
|
worˇld()
|
|
}
|
|
"},
|
|
Mode::Normal,
|
|
);
|
|
cx.simulate_keystrokes("v a i");
|
|
cx.assert_state(
|
|
indoc! {"
|
|
const ABOVE: str = true;
|
|
«fn boop() {
|
|
|
|
hello();
|
|
world()ˇ»
|
|
}
|
|
"},
|
|
Mode::Visual,
|
|
);
|
|
|
|
// Around indent (include line above & below)
|
|
cx.set_state(
|
|
indoc! {"
|
|
const ABOVE: str = true;
|
|
fn boop() {
|
|
hellˇo();
|
|
world()
|
|
|
|
}
|
|
const BELOW: str = true;
|
|
"},
|
|
Mode::Normal,
|
|
);
|
|
cx.simulate_keystrokes("c a shift-i");
|
|
cx.assert_state(
|
|
indoc! {"
|
|
const ABOVE: str = true;
|
|
ˇ
|
|
const BELOW: str = true;
|
|
"},
|
|
Mode::Insert,
|
|
);
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_delete_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
|
|
|
for (start, end) in SURROUNDING_OBJECTS {
|
|
let marked_string = SURROUNDING_MARKER_STRING
|
|
.replace('`', &start.to_string())
|
|
.replace('\'', &end.to_string());
|
|
|
|
cx.simulate_at_each_offset(&format!("d i {start}"), &marked_string)
|
|
.await
|
|
.assert_matches();
|
|
cx.simulate_at_each_offset(&format!("d i {end}"), &marked_string)
|
|
.await
|
|
.assert_matches();
|
|
cx.simulate_at_each_offset(&format!("d a {start}"), &marked_string)
|
|
.await
|
|
.assert_matches();
|
|
cx.simulate_at_each_offset(&format!("d a {end}"), &marked_string)
|
|
.await
|
|
.assert_matches();
|
|
}
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_anyquotes_object(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = VimTestContext::new(cx, true).await;
|
|
cx.update(|_, cx| {
|
|
cx.bind_keys([KeyBinding::new(
|
|
"q",
|
|
AnyQuotes,
|
|
Some("vim_operator == a || vim_operator == i || vim_operator == cs"),
|
|
)]);
|
|
});
|
|
|
|
const TEST_CASES: &[(&str, &str, &str, Mode)] = &[
|
|
// the false string in the middle should be considered
|
|
(
|
|
"c i q",
|
|
"'first' false ˇstring 'second'",
|
|
"'first'ˇ'second'",
|
|
Mode::Insert,
|
|
),
|
|
// Single quotes
|
|
(
|
|
"c i q",
|
|
"Thisˇ is a 'quote' example.",
|
|
"This is a 'ˇ' example.",
|
|
Mode::Insert,
|
|
),
|
|
(
|
|
"c a q",
|
|
"Thisˇ is a 'quote' example.",
|
|
"This is a ˇexample.",
|
|
Mode::Insert,
|
|
),
|
|
(
|
|
"c i q",
|
|
"This is a \"simple 'qˇuote'\" example.",
|
|
"This is a \"simple 'ˇ'\" example.",
|
|
Mode::Insert,
|
|
),
|
|
(
|
|
"c a q",
|
|
"This is a \"simple 'qˇuote'\" example.",
|
|
"This is a \"simpleˇ\" example.",
|
|
Mode::Insert,
|
|
),
|
|
(
|
|
"c i q",
|
|
"This is a 'qˇuote' example.",
|
|
"This is a 'ˇ' example.",
|
|
Mode::Insert,
|
|
),
|
|
(
|
|
"c a q",
|
|
"This is a 'qˇuote' example.",
|
|
"This is a ˇexample.",
|
|
Mode::Insert,
|
|
),
|
|
(
|
|
"d i q",
|
|
"This is a 'qˇuote' example.",
|
|
"This is a 'ˇ' example.",
|
|
Mode::Normal,
|
|
),
|
|
(
|
|
"d a q",
|
|
"This is a 'qˇuote' example.",
|
|
"This is a ˇexample.",
|
|
Mode::Normal,
|
|
),
|
|
// Double quotes
|
|
(
|
|
"c i q",
|
|
"This is a \"qˇuote\" example.",
|
|
"This is a \"ˇ\" example.",
|
|
Mode::Insert,
|
|
),
|
|
(
|
|
"c a q",
|
|
"This is a \"qˇuote\" example.",
|
|
"This is a ˇexample.",
|
|
Mode::Insert,
|
|
),
|
|
(
|
|
"d i q",
|
|
"This is a \"qˇuote\" example.",
|
|
"This is a \"ˇ\" example.",
|
|
Mode::Normal,
|
|
),
|
|
(
|
|
"d a q",
|
|
"This is a \"qˇuote\" example.",
|
|
"This is a ˇexample.",
|
|
Mode::Normal,
|
|
),
|
|
// Back quotes
|
|
(
|
|
"c i q",
|
|
"This is a `qˇuote` example.",
|
|
"This is a `ˇ` example.",
|
|
Mode::Insert,
|
|
),
|
|
(
|
|
"c a q",
|
|
"This is a `qˇuote` example.",
|
|
"This is a ˇexample.",
|
|
Mode::Insert,
|
|
),
|
|
(
|
|
"d i q",
|
|
"This is a `qˇuote` example.",
|
|
"This is a `ˇ` example.",
|
|
Mode::Normal,
|
|
),
|
|
(
|
|
"d a q",
|
|
"This is a `qˇuote` example.",
|
|
"This is a ˇexample.",
|
|
Mode::Normal,
|
|
),
|
|
];
|
|
|
|
for (keystrokes, initial_state, expected_state, expected_mode) in TEST_CASES {
|
|
cx.set_state(initial_state, Mode::Normal);
|
|
|
|
cx.simulate_keystrokes(keystrokes);
|
|
|
|
cx.assert_state(expected_state, *expected_mode);
|
|
}
|
|
|
|
const INVALID_CASES: &[(&str, &str, Mode)] = &[
|
|
("c i q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote
|
|
("c a q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote
|
|
("d i q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote
|
|
("d a q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote
|
|
("c i q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing double quote
|
|
("c a q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing double quote
|
|
("d i q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing double quote
|
|
("d a q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing back quote
|
|
("c i q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote
|
|
("c a q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote
|
|
("d i q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote
|
|
("d a q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote
|
|
];
|
|
|
|
for (keystrokes, initial_state, mode) in INVALID_CASES {
|
|
cx.set_state(initial_state, Mode::Normal);
|
|
|
|
cx.simulate_keystrokes(keystrokes);
|
|
|
|
cx.assert_state(initial_state, *mode);
|
|
}
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_miniquotes_object(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = VimTestContext::new_typescript(cx).await;
|
|
|
|
const TEST_CASES: &[(&str, &str, &str, Mode)] = &[
|
|
// Special cases from mini.ai plugin
|
|
// the false string in the middle should not be considered
|
|
(
|
|
"c i q",
|
|
"'first' false ˇstring 'second'",
|
|
"'first' false string 'ˇ'",
|
|
Mode::Insert,
|
|
),
|
|
// Multiline support :)! Same behavior as mini.ai plugin
|
|
(
|
|
"c i q",
|
|
indoc! {"
|
|
`
|
|
first
|
|
middle ˇstring
|
|
second
|
|
`
|
|
"},
|
|
indoc! {"
|
|
`ˇ`
|
|
"},
|
|
Mode::Insert,
|
|
),
|
|
// If you are in the close quote and it is the only quote in the buffer, it should replace inside the quote
|
|
// This is not working with the core motion ci' for this special edge case, so I am happy to fix it in MiniQuotes :)
|
|
// Bug reference: https://github.com/zed-industries/zed/issues/23889
|
|
("c i q", "'quote«'ˇ»", "'ˇ'", Mode::Insert),
|
|
// Single quotes
|
|
(
|
|
"c i q",
|
|
"Thisˇ is a 'quote' example.",
|
|
"This is a 'ˇ' example.",
|
|
Mode::Insert,
|
|
),
|
|
(
|
|
"c a q",
|
|
"Thisˇ is a 'quote' example.",
|
|
"This is a ˇ example.", // same mini.ai plugin behavior
|
|
Mode::Insert,
|
|
),
|
|
(
|
|
"c i q",
|
|
"This is a \"simple 'qˇuote'\" example.",
|
|
"This is a \"ˇ\" example.", // Not supported by Tree-sitter queries for now
|
|
Mode::Insert,
|
|
),
|
|
(
|
|
"c a q",
|
|
"This is a \"simple 'qˇuote'\" example.",
|
|
"This is a ˇ example.", // Not supported by Tree-sitter queries for now
|
|
Mode::Insert,
|
|
),
|
|
(
|
|
"c i q",
|
|
"This is a 'qˇuote' example.",
|
|
"This is a 'ˇ' example.",
|
|
Mode::Insert,
|
|
),
|
|
(
|
|
"c a q",
|
|
"This is a 'qˇuote' example.",
|
|
"This is a ˇ example.", // same mini.ai plugin behavior
|
|
Mode::Insert,
|
|
),
|
|
(
|
|
"d i q",
|
|
"This is a 'qˇuote' example.",
|
|
"This is a 'ˇ' example.",
|
|
Mode::Normal,
|
|
),
|
|
(
|
|
"d a q",
|
|
"This is a 'qˇuote' example.",
|
|
"This is a ˇ example.", // same mini.ai plugin behavior
|
|
Mode::Normal,
|
|
),
|
|
// Double quotes
|
|
(
|
|
"c i q",
|
|
"This is a \"qˇuote\" example.",
|
|
"This is a \"ˇ\" example.",
|
|
Mode::Insert,
|
|
),
|
|
(
|
|
"c a q",
|
|
"This is a \"qˇuote\" example.",
|
|
"This is a ˇ example.", // same mini.ai plugin behavior
|
|
Mode::Insert,
|
|
),
|
|
(
|
|
"d i q",
|
|
"This is a \"qˇuote\" example.",
|
|
"This is a \"ˇ\" example.",
|
|
Mode::Normal,
|
|
),
|
|
(
|
|
"d a q",
|
|
"This is a \"qˇuote\" example.",
|
|
"This is a ˇ example.", // same mini.ai plugin behavior
|
|
Mode::Normal,
|
|
),
|
|
// Back quotes
|
|
(
|
|
"c i q",
|
|
"This is a `qˇuote` example.",
|
|
"This is a `ˇ` example.",
|
|
Mode::Insert,
|
|
),
|
|
(
|
|
"c a q",
|
|
"This is a `qˇuote` example.",
|
|
"This is a ˇ example.", // same mini.ai plugin behavior
|
|
Mode::Insert,
|
|
),
|
|
(
|
|
"d i q",
|
|
"This is a `qˇuote` example.",
|
|
"This is a `ˇ` example.",
|
|
Mode::Normal,
|
|
),
|
|
(
|
|
"d a q",
|
|
"This is a `qˇuote` example.",
|
|
"This is a ˇ example.", // same mini.ai plugin behavior
|
|
Mode::Normal,
|
|
),
|
|
];
|
|
|
|
for (keystrokes, initial_state, expected_state, expected_mode) in TEST_CASES {
|
|
cx.set_state(initial_state, Mode::Normal);
|
|
cx.buffer(|buffer, _| buffer.parsing_idle()).await;
|
|
cx.simulate_keystrokes(keystrokes);
|
|
cx.assert_state(expected_state, *expected_mode);
|
|
}
|
|
|
|
const INVALID_CASES: &[(&str, &str, Mode)] = &[
|
|
("c i q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote
|
|
("c a q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote
|
|
("d i q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote
|
|
("d a q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote
|
|
("c i q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing double quote
|
|
("c a q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing double quote
|
|
("d i q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing double quote
|
|
("d a q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing back quote
|
|
("c i q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote
|
|
("c a q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote
|
|
("d i q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote
|
|
("d a q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote
|
|
];
|
|
|
|
for (keystrokes, initial_state, mode) in INVALID_CASES {
|
|
cx.set_state(initial_state, Mode::Normal);
|
|
cx.buffer(|buffer, _| buffer.parsing_idle()).await;
|
|
cx.simulate_keystrokes(keystrokes);
|
|
cx.assert_state(initial_state, *mode);
|
|
}
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_anybrackets_object(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = VimTestContext::new(cx, true).await;
|
|
cx.update(|_, cx| {
|
|
cx.bind_keys([KeyBinding::new(
|
|
"b",
|
|
AnyBrackets,
|
|
Some("vim_operator == a || vim_operator == i || vim_operator == cs"),
|
|
)]);
|
|
});
|
|
|
|
const TEST_CASES: &[(&str, &str, &str, Mode)] = &[
|
|
(
|
|
"c i b",
|
|
indoc! {"
|
|
{
|
|
{
|
|
ˇprint('hello')
|
|
}
|
|
}
|
|
"},
|
|
indoc! {"
|
|
{
|
|
{
|
|
ˇ
|
|
}
|
|
}
|
|
"},
|
|
Mode::Insert,
|
|
),
|
|
// Bracket (Parentheses)
|
|
(
|
|
"c i b",
|
|
"Thisˇ is a (simple [quote]) example.",
|
|
"This is a (ˇ) example.",
|
|
Mode::Insert,
|
|
),
|
|
(
|
|
"c i b",
|
|
"This is a [simple (qˇuote)] example.",
|
|
"This is a [simple (ˇ)] example.",
|
|
Mode::Insert,
|
|
),
|
|
(
|
|
"c a b",
|
|
"This is a [simple (qˇuote)] example.",
|
|
"This is a [simple ˇ] example.",
|
|
Mode::Insert,
|
|
),
|
|
(
|
|
"c a b",
|
|
"Thisˇ is a (simple [quote]) example.",
|
|
"This is a ˇ example.",
|
|
Mode::Insert,
|
|
),
|
|
(
|
|
"c i b",
|
|
"This is a (qˇuote) example.",
|
|
"This is a (ˇ) example.",
|
|
Mode::Insert,
|
|
),
|
|
(
|
|
"c a b",
|
|
"This is a (qˇuote) example.",
|
|
"This is a ˇ example.",
|
|
Mode::Insert,
|
|
),
|
|
(
|
|
"d i b",
|
|
"This is a (qˇuote) example.",
|
|
"This is a (ˇ) example.",
|
|
Mode::Normal,
|
|
),
|
|
(
|
|
"d a b",
|
|
"This is a (qˇuote) example.",
|
|
"This is a ˇ example.",
|
|
Mode::Normal,
|
|
),
|
|
// Square brackets
|
|
(
|
|
"c i b",
|
|
"This is a [qˇuote] example.",
|
|
"This is a [ˇ] example.",
|
|
Mode::Insert,
|
|
),
|
|
(
|
|
"c a b",
|
|
"This is a [qˇuote] example.",
|
|
"This is a ˇ example.",
|
|
Mode::Insert,
|
|
),
|
|
(
|
|
"d i b",
|
|
"This is a [qˇuote] example.",
|
|
"This is a [ˇ] example.",
|
|
Mode::Normal,
|
|
),
|
|
(
|
|
"d a b",
|
|
"This is a [qˇuote] example.",
|
|
"This is a ˇ example.",
|
|
Mode::Normal,
|
|
),
|
|
// Curly brackets
|
|
(
|
|
"c i b",
|
|
"This is a {qˇuote} example.",
|
|
"This is a {ˇ} example.",
|
|
Mode::Insert,
|
|
),
|
|
(
|
|
"c a b",
|
|
"This is a {qˇuote} example.",
|
|
"This is a ˇ example.",
|
|
Mode::Insert,
|
|
),
|
|
(
|
|
"d i b",
|
|
"This is a {qˇuote} example.",
|
|
"This is a {ˇ} example.",
|
|
Mode::Normal,
|
|
),
|
|
(
|
|
"d a b",
|
|
"This is a {qˇuote} example.",
|
|
"This is a ˇ example.",
|
|
Mode::Normal,
|
|
),
|
|
];
|
|
|
|
for (keystrokes, initial_state, expected_state, expected_mode) in TEST_CASES {
|
|
cx.set_state(initial_state, Mode::Normal);
|
|
|
|
cx.simulate_keystrokes(keystrokes);
|
|
|
|
cx.assert_state(expected_state, *expected_mode);
|
|
}
|
|
|
|
const INVALID_CASES: &[(&str, &str, Mode)] = &[
|
|
("c i b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket
|
|
("c a b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket
|
|
("d i b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket
|
|
("d a b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket
|
|
("c i b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket
|
|
("c a b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket
|
|
("d i b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket
|
|
("d a b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket
|
|
("c i b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket
|
|
("c a b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket
|
|
("d i b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket
|
|
("d a b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket
|
|
];
|
|
|
|
for (keystrokes, initial_state, mode) in INVALID_CASES {
|
|
cx.set_state(initial_state, Mode::Normal);
|
|
|
|
cx.simulate_keystrokes(keystrokes);
|
|
|
|
cx.assert_state(initial_state, *mode);
|
|
}
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_minibrackets_object(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = VimTestContext::new(cx, true).await;
|
|
cx.update(|_, cx| {
|
|
cx.bind_keys([KeyBinding::new(
|
|
"b",
|
|
MiniBrackets,
|
|
Some("vim_operator == a || vim_operator == i || vim_operator == cs"),
|
|
)]);
|
|
});
|
|
|
|
const TEST_CASES: &[(&str, &str, &str, Mode)] = &[
|
|
// Special cases from mini.ai plugin
|
|
// Current line has more priority for the cover or next algorithm, to avoid changing curly brackets which is supper anoying
|
|
// Same behavior as mini.ai plugin
|
|
(
|
|
"c i b",
|
|
indoc! {"
|
|
{
|
|
{
|
|
ˇprint('hello')
|
|
}
|
|
}
|
|
"},
|
|
indoc! {"
|
|
{
|
|
{
|
|
print(ˇ)
|
|
}
|
|
}
|
|
"},
|
|
Mode::Insert,
|
|
),
|
|
// If the current line doesn't have brackets then it should consider if the caret is inside an external bracket
|
|
// Same behavior as mini.ai plugin
|
|
(
|
|
"c i b",
|
|
indoc! {"
|
|
{
|
|
{
|
|
ˇ
|
|
print('hello')
|
|
}
|
|
}
|
|
"},
|
|
indoc! {"
|
|
{
|
|
{ˇ}
|
|
}
|
|
"},
|
|
Mode::Insert,
|
|
),
|
|
// If you are in the open bracket then it has higher priority
|
|
(
|
|
"c i b",
|
|
indoc! {"
|
|
«{ˇ»
|
|
{
|
|
print('hello')
|
|
}
|
|
}
|
|
"},
|
|
indoc! {"
|
|
{ˇ}
|
|
"},
|
|
Mode::Insert,
|
|
),
|
|
// If you are in the close bracket then it has higher priority
|
|
(
|
|
"c i b",
|
|
indoc! {"
|
|
{
|
|
{
|
|
print('hello')
|
|
}
|
|
«}ˇ»
|
|
"},
|
|
indoc! {"
|
|
{ˇ}
|
|
"},
|
|
Mode::Insert,
|
|
),
|
|
// Bracket (Parentheses)
|
|
(
|
|
"c i b",
|
|
"Thisˇ is a (simple [quote]) example.",
|
|
"This is a (ˇ) example.",
|
|
Mode::Insert,
|
|
),
|
|
(
|
|
"c i b",
|
|
"This is a [simple (qˇuote)] example.",
|
|
"This is a [simple (ˇ)] example.",
|
|
Mode::Insert,
|
|
),
|
|
(
|
|
"c a b",
|
|
"This is a [simple (qˇuote)] example.",
|
|
"This is a [simple ˇ] example.",
|
|
Mode::Insert,
|
|
),
|
|
(
|
|
"c a b",
|
|
"Thisˇ is a (simple [quote]) example.",
|
|
"This is a ˇ example.",
|
|
Mode::Insert,
|
|
),
|
|
(
|
|
"c i b",
|
|
"This is a (qˇuote) example.",
|
|
"This is a (ˇ) example.",
|
|
Mode::Insert,
|
|
),
|
|
(
|
|
"c a b",
|
|
"This is a (qˇuote) example.",
|
|
"This is a ˇ example.",
|
|
Mode::Insert,
|
|
),
|
|
(
|
|
"d i b",
|
|
"This is a (qˇuote) example.",
|
|
"This is a (ˇ) example.",
|
|
Mode::Normal,
|
|
),
|
|
(
|
|
"d a b",
|
|
"This is a (qˇuote) example.",
|
|
"This is a ˇ example.",
|
|
Mode::Normal,
|
|
),
|
|
// Square brackets
|
|
(
|
|
"c i b",
|
|
"This is a [qˇuote] example.",
|
|
"This is a [ˇ] example.",
|
|
Mode::Insert,
|
|
),
|
|
(
|
|
"c a b",
|
|
"This is a [qˇuote] example.",
|
|
"This is a ˇ example.",
|
|
Mode::Insert,
|
|
),
|
|
(
|
|
"d i b",
|
|
"This is a [qˇuote] example.",
|
|
"This is a [ˇ] example.",
|
|
Mode::Normal,
|
|
),
|
|
(
|
|
"d a b",
|
|
"This is a [qˇuote] example.",
|
|
"This is a ˇ example.",
|
|
Mode::Normal,
|
|
),
|
|
// Curly brackets
|
|
(
|
|
"c i b",
|
|
"This is a {qˇuote} example.",
|
|
"This is a {ˇ} example.",
|
|
Mode::Insert,
|
|
),
|
|
(
|
|
"c a b",
|
|
"This is a {qˇuote} example.",
|
|
"This is a ˇ example.",
|
|
Mode::Insert,
|
|
),
|
|
(
|
|
"d i b",
|
|
"This is a {qˇuote} example.",
|
|
"This is a {ˇ} example.",
|
|
Mode::Normal,
|
|
),
|
|
(
|
|
"d a b",
|
|
"This is a {qˇuote} example.",
|
|
"This is a ˇ example.",
|
|
Mode::Normal,
|
|
),
|
|
];
|
|
|
|
for (keystrokes, initial_state, expected_state, expected_mode) in TEST_CASES {
|
|
cx.set_state(initial_state, Mode::Normal);
|
|
cx.buffer(|buffer, _| buffer.parsing_idle()).await;
|
|
cx.simulate_keystrokes(keystrokes);
|
|
cx.assert_state(expected_state, *expected_mode);
|
|
}
|
|
|
|
const INVALID_CASES: &[(&str, &str, Mode)] = &[
|
|
("c i b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket
|
|
("c a b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket
|
|
("d i b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket
|
|
("d a b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket
|
|
("c i b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket
|
|
("c a b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket
|
|
("d i b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket
|
|
("d a b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket
|
|
("c i b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket
|
|
("c a b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket
|
|
("d i b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket
|
|
("d a b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket
|
|
];
|
|
|
|
for (keystrokes, initial_state, mode) in INVALID_CASES {
|
|
cx.set_state(initial_state, Mode::Normal);
|
|
cx.buffer(|buffer, _| buffer.parsing_idle()).await;
|
|
cx.simulate_keystrokes(keystrokes);
|
|
cx.assert_state(initial_state, *mode);
|
|
}
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_minibrackets_multibuffer(cx: &mut gpui::TestAppContext) {
|
|
// Initialize test context with the TypeScript language loaded, so we
|
|
// can actually get brackets definition.
|
|
let mut cx = VimTestContext::new(cx, true).await;
|
|
|
|
// Update `b` to `MiniBrackets` so we can later use it when simulating
|
|
// keystrokes.
|
|
cx.update(|_, cx| {
|
|
cx.bind_keys([KeyBinding::new("b", MiniBrackets, None)]);
|
|
});
|
|
|
|
let (editor, cx) = cx.add_window_view(|window, cx| {
|
|
let multi_buffer = MultiBuffer::build_multi(
|
|
[
|
|
("111\n222\n333\n444\n", vec![Point::row_range(0..2)]),
|
|
("111\na {bracket} example\n", vec![Point::row_range(0..2)]),
|
|
],
|
|
cx,
|
|
);
|
|
|
|
// In order for the brackets to actually be found, we need to update
|
|
// the language used for the second buffer. This is something that
|
|
// is handled automatically when simply using `VimTestContext::new`
|
|
// but, since this is being set manually, the language isn't
|
|
// automatically set.
|
|
let editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
|
|
let buffer_ids = multi_buffer
|
|
.read(cx)
|
|
.snapshot(cx)
|
|
.excerpts()
|
|
.map(|excerpt| excerpt.context.start.buffer_id)
|
|
.collect::<Vec<_>>();
|
|
if let Some(buffer) = multi_buffer.read(cx).buffer(buffer_ids[1]) {
|
|
buffer.update(cx, |buffer, cx| {
|
|
buffer.set_language(Some(language::rust_lang()), cx);
|
|
})
|
|
};
|
|
|
|
editor
|
|
});
|
|
|
|
let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
|
|
|
|
cx.assert_excerpts_with_selections(indoc! {"
|
|
[EXCERPT]
|
|
ˇ111
|
|
222
|
|
[EXCERPT]
|
|
111
|
|
a {bracket} example
|
|
"
|
|
});
|
|
|
|
cx.simulate_keystrokes("j j j j f r");
|
|
cx.assert_excerpts_with_selections(indoc! {"
|
|
[EXCERPT]
|
|
111
|
|
222
|
|
[EXCERPT]
|
|
111
|
|
a {bˇracket} example
|
|
"
|
|
});
|
|
|
|
cx.simulate_keystrokes("d i b");
|
|
cx.assert_excerpts_with_selections(indoc! {"
|
|
[EXCERPT]
|
|
111
|
|
222
|
|
[EXCERPT]
|
|
111
|
|
a {ˇ} example
|
|
"
|
|
});
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_minibrackets_trailing_space(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
|
cx.set_shared_state("(trailingˇ whitespace )")
|
|
.await;
|
|
cx.simulate_shared_keystrokes("v i b").await;
|
|
cx.shared_state().await.assert_matches();
|
|
cx.simulate_shared_keystrokes("escape y i b").await;
|
|
cx.shared_clipboard()
|
|
.await
|
|
.assert_eq("trailing whitespace ");
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_tags(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = VimTestContext::new_html(cx).await;
|
|
|
|
cx.set_state("<html><head></head><body><b>hˇi!</b></body>", Mode::Normal);
|
|
cx.simulate_keystrokes("v i t");
|
|
cx.assert_state(
|
|
"<html><head></head><body><b>«hi!ˇ»</b></body>",
|
|
Mode::Visual,
|
|
);
|
|
cx.simulate_keystrokes("a t");
|
|
cx.assert_state(
|
|
"<html><head></head><body>«<b>hi!</b>ˇ»</body>",
|
|
Mode::Visual,
|
|
);
|
|
cx.simulate_keystrokes("a t");
|
|
cx.assert_state(
|
|
"<html><head></head>«<body><b>hi!</b></body>ˇ»",
|
|
Mode::Visual,
|
|
);
|
|
|
|
// The cursor is before the tag
|
|
cx.set_state(
|
|
"<html><head></head><body> ˇ <b>hi!</b></body>",
|
|
Mode::Normal,
|
|
);
|
|
cx.simulate_keystrokes("v i t");
|
|
cx.assert_state(
|
|
"<html><head></head><body> <b>«hi!ˇ»</b></body>",
|
|
Mode::Visual,
|
|
);
|
|
cx.simulate_keystrokes("a t");
|
|
cx.assert_state(
|
|
"<html><head></head><body> «<b>hi!</b>ˇ»</body>",
|
|
Mode::Visual,
|
|
);
|
|
|
|
// The cursor is in the open tag
|
|
cx.set_state(
|
|
"<html><head></head><body><bˇ>hi!</b><b>hello!</b></body>",
|
|
Mode::Normal,
|
|
);
|
|
cx.simulate_keystrokes("v a t");
|
|
cx.assert_state(
|
|
"<html><head></head><body>«<b>hi!</b>ˇ»<b>hello!</b></body>",
|
|
Mode::Visual,
|
|
);
|
|
cx.simulate_keystrokes("i t");
|
|
cx.assert_state(
|
|
"<html><head></head><body>«<b>hi!</b><b>hello!</b>ˇ»</body>",
|
|
Mode::Visual,
|
|
);
|
|
|
|
// current selection length greater than 1
|
|
cx.set_state(
|
|
"<html><head></head><body><«b>hi!ˇ»</b></body>",
|
|
Mode::Visual,
|
|
);
|
|
cx.simulate_keystrokes("i t");
|
|
cx.assert_state(
|
|
"<html><head></head><body><b>«hi!ˇ»</b></body>",
|
|
Mode::Visual,
|
|
);
|
|
cx.simulate_keystrokes("a t");
|
|
cx.assert_state(
|
|
"<html><head></head><body>«<b>hi!</b>ˇ»</body>",
|
|
Mode::Visual,
|
|
);
|
|
|
|
cx.set_state(
|
|
"<html><head></head><body><«b>hi!</ˇ»b></body>",
|
|
Mode::Visual,
|
|
);
|
|
cx.simulate_keystrokes("a t");
|
|
cx.assert_state(
|
|
"<html><head></head>«<body><b>hi!</b></body>ˇ»",
|
|
Mode::Visual,
|
|
);
|
|
}
|
|
#[gpui::test]
|
|
async fn test_around_containing_word_indent(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
|
|
|
cx.set_shared_state(" ˇconst f = (x: unknown) => {")
|
|
.await;
|
|
cx.simulate_shared_keystrokes("v a w").await;
|
|
cx.shared_state()
|
|
.await
|
|
.assert_eq(" «const ˇ»f = (x: unknown) => {");
|
|
|
|
cx.set_shared_state(" ˇconst f = (x: unknown) => {")
|
|
.await;
|
|
cx.simulate_shared_keystrokes("y a w").await;
|
|
cx.shared_clipboard().await.assert_eq("const ");
|
|
|
|
cx.set_shared_state(" ˇconst f = (x: unknown) => {")
|
|
.await;
|
|
cx.simulate_shared_keystrokes("d a w").await;
|
|
cx.shared_state()
|
|
.await
|
|
.assert_eq(" ˇf = (x: unknown) => {");
|
|
cx.shared_clipboard().await.assert_eq("const ");
|
|
|
|
cx.set_shared_state(" ˇconst f = (x: unknown) => {")
|
|
.await;
|
|
cx.simulate_shared_keystrokes("c a w").await;
|
|
cx.shared_state()
|
|
.await
|
|
.assert_eq(" ˇf = (x: unknown) => {");
|
|
cx.shared_clipboard().await.assert_eq("const ");
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_arrow_function_text_object(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = VimTestContext::new_typescript(cx).await;
|
|
|
|
cx.set_state(
|
|
indoc! {"
|
|
const foo = () => {
|
|
return ˇ1;
|
|
};
|
|
"},
|
|
Mode::Normal,
|
|
);
|
|
cx.simulate_keystrokes("v a f");
|
|
cx.assert_state(
|
|
indoc! {"
|
|
«const foo = () => {
|
|
return 1;
|
|
};ˇ»
|
|
"},
|
|
Mode::VisualLine,
|
|
);
|
|
|
|
cx.set_state(
|
|
indoc! {"
|
|
arr.map(() => {
|
|
return ˇ1;
|
|
});
|
|
"},
|
|
Mode::Normal,
|
|
);
|
|
cx.simulate_keystrokes("v a f");
|
|
cx.assert_state(
|
|
indoc! {"
|
|
arr.map(«() => {
|
|
return 1;
|
|
}ˇ»);
|
|
"},
|
|
Mode::VisualLine,
|
|
);
|
|
|
|
cx.set_state(
|
|
indoc! {"
|
|
const foo = () => {
|
|
return ˇ1;
|
|
};
|
|
"},
|
|
Mode::Normal,
|
|
);
|
|
cx.simulate_keystrokes("v i f");
|
|
cx.assert_state(
|
|
indoc! {"
|
|
const foo = () => {
|
|
«return 1;ˇ»
|
|
};
|
|
"},
|
|
Mode::Visual,
|
|
);
|
|
|
|
cx.set_state(
|
|
indoc! {"
|
|
(() => {
|
|
console.log(ˇ1);
|
|
})();
|
|
"},
|
|
Mode::Normal,
|
|
);
|
|
cx.simulate_keystrokes("v a f");
|
|
cx.assert_state(
|
|
indoc! {"
|
|
(«() => {
|
|
console.log(1);
|
|
}ˇ»)();
|
|
"},
|
|
Mode::VisualLine,
|
|
);
|
|
|
|
cx.set_state(
|
|
indoc! {"
|
|
const foo = () => {
|
|
return ˇ1;
|
|
};
|
|
export { foo };
|
|
"},
|
|
Mode::Normal,
|
|
);
|
|
cx.simulate_keystrokes("v a f");
|
|
cx.assert_state(
|
|
indoc! {"
|
|
«const foo = () => {
|
|
return 1;
|
|
};ˇ»
|
|
export { foo };
|
|
"},
|
|
Mode::VisualLine,
|
|
);
|
|
|
|
cx.set_state(
|
|
indoc! {"
|
|
let bar = () => {
|
|
return ˇ2;
|
|
};
|
|
"},
|
|
Mode::Normal,
|
|
);
|
|
cx.simulate_keystrokes("v a f");
|
|
cx.assert_state(
|
|
indoc! {"
|
|
«let bar = () => {
|
|
return 2;
|
|
};ˇ»
|
|
"},
|
|
Mode::VisualLine,
|
|
);
|
|
|
|
cx.set_state(
|
|
indoc! {"
|
|
var baz = () => {
|
|
return ˇ3;
|
|
};
|
|
"},
|
|
Mode::Normal,
|
|
);
|
|
cx.simulate_keystrokes("v a f");
|
|
cx.assert_state(
|
|
indoc! {"
|
|
«var baz = () => {
|
|
return 3;
|
|
};ˇ»
|
|
"},
|
|
Mode::VisualLine,
|
|
);
|
|
|
|
cx.set_state(
|
|
indoc! {"
|
|
const add = (a, b) => a + ˇb;
|
|
"},
|
|
Mode::Normal,
|
|
);
|
|
cx.simulate_keystrokes("v a f");
|
|
cx.assert_state(
|
|
indoc! {"
|
|
«const add = (a, b) => a + b;ˇ»
|
|
"},
|
|
Mode::VisualLine,
|
|
);
|
|
|
|
cx.set_state(
|
|
indoc! {"
|
|
const add = ˇ(a, b) => a + b;
|
|
"},
|
|
Mode::Normal,
|
|
);
|
|
cx.simulate_keystrokes("v a f");
|
|
cx.assert_state(
|
|
indoc! {"
|
|
«const add = (a, b) => a + b;ˇ»
|
|
"},
|
|
Mode::VisualLine,
|
|
);
|
|
|
|
cx.set_state(
|
|
indoc! {"
|
|
const add = (a, b) => a + bˇ;
|
|
"},
|
|
Mode::Normal,
|
|
);
|
|
cx.simulate_keystrokes("v a f");
|
|
cx.assert_state(
|
|
indoc! {"
|
|
«const add = (a, b) => a + b;ˇ»
|
|
"},
|
|
Mode::VisualLine,
|
|
);
|
|
|
|
cx.set_state(
|
|
indoc! {"
|
|
const add = (a, b) =ˇ> a + b;
|
|
"},
|
|
Mode::Normal,
|
|
);
|
|
cx.simulate_keystrokes("v a f");
|
|
cx.assert_state(
|
|
indoc! {"
|
|
«const add = (a, b) => a + b;ˇ»
|
|
"},
|
|
Mode::VisualLine,
|
|
);
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_arrow_function_in_jsx(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = VimTestContext::new_tsx(cx).await;
|
|
|
|
cx.set_state(
|
|
indoc! {r#"
|
|
export const MyComponent = () => {
|
|
return (
|
|
<div>
|
|
<div onClick={() => {
|
|
alert("Hello world!");
|
|
console.log(ˇ"clicked");
|
|
}}>Hello world!</div>
|
|
</div>
|
|
);
|
|
};
|
|
"#},
|
|
Mode::Normal,
|
|
);
|
|
cx.simulate_keystrokes("v a f");
|
|
cx.assert_state(
|
|
indoc! {r#"
|
|
export const MyComponent = () => {
|
|
return (
|
|
<div>
|
|
<div onClick={«() => {
|
|
alert("Hello world!");
|
|
console.log("clicked");
|
|
}ˇ»}>Hello world!</div>
|
|
</div>
|
|
);
|
|
};
|
|
"#},
|
|
Mode::VisualLine,
|
|
);
|
|
|
|
cx.set_state(
|
|
indoc! {r#"
|
|
export const MyComponent = () => {
|
|
return (
|
|
<div>
|
|
<div onClick={() => console.log("clickˇed")}>Hello world!</div>
|
|
</div>
|
|
);
|
|
};
|
|
"#},
|
|
Mode::Normal,
|
|
);
|
|
cx.simulate_keystrokes("v a f");
|
|
cx.assert_state(
|
|
indoc! {r#"
|
|
export const MyComponent = () => {
|
|
return (
|
|
<div>
|
|
<div onClick={«() => console.log("clicked")ˇ»}>Hello world!</div>
|
|
</div>
|
|
);
|
|
};
|
|
"#},
|
|
Mode::VisualLine,
|
|
);
|
|
|
|
cx.set_state(
|
|
indoc! {r#"
|
|
export const MyComponent = () => {
|
|
return (
|
|
<div>
|
|
<div onClick={ˇ() => console.log("clicked")}>Hello world!</div>
|
|
</div>
|
|
);
|
|
};
|
|
"#},
|
|
Mode::Normal,
|
|
);
|
|
cx.simulate_keystrokes("v a f");
|
|
cx.assert_state(
|
|
indoc! {r#"
|
|
export const MyComponent = () => {
|
|
return (
|
|
<div>
|
|
<div onClick={«() => console.log("clicked")ˇ»}>Hello world!</div>
|
|
</div>
|
|
);
|
|
};
|
|
"#},
|
|
Mode::VisualLine,
|
|
);
|
|
|
|
cx.set_state(
|
|
indoc! {r#"
|
|
export const MyComponent = () => {
|
|
return (
|
|
<div>
|
|
<div onClick={() => console.log("clicked"ˇ)}>Hello world!</div>
|
|
</div>
|
|
);
|
|
};
|
|
"#},
|
|
Mode::Normal,
|
|
);
|
|
cx.simulate_keystrokes("v a f");
|
|
cx.assert_state(
|
|
indoc! {r#"
|
|
export const MyComponent = () => {
|
|
return (
|
|
<div>
|
|
<div onClick={«() => console.log("clicked")ˇ»}>Hello world!</div>
|
|
</div>
|
|
);
|
|
};
|
|
"#},
|
|
Mode::VisualLine,
|
|
);
|
|
|
|
cx.set_state(
|
|
indoc! {r#"
|
|
export const MyComponent = () => {
|
|
return (
|
|
<div>
|
|
<div onClick={() =ˇ> console.log("clicked")}>Hello world!</div>
|
|
</div>
|
|
);
|
|
};
|
|
"#},
|
|
Mode::Normal,
|
|
);
|
|
cx.simulate_keystrokes("v a f");
|
|
cx.assert_state(
|
|
indoc! {r#"
|
|
export const MyComponent = () => {
|
|
return (
|
|
<div>
|
|
<div onClick={«() => console.log("clicked")ˇ»}>Hello world!</div>
|
|
</div>
|
|
);
|
|
};
|
|
"#},
|
|
Mode::VisualLine,
|
|
);
|
|
|
|
cx.set_state(
|
|
indoc! {r#"
|
|
export const MyComponent = () => {
|
|
return (
|
|
<div>
|
|
<div onClick={() => {
|
|
console.log("cliˇcked");
|
|
}}>Hello world!</div>
|
|
</div>
|
|
);
|
|
};
|
|
"#},
|
|
Mode::Normal,
|
|
);
|
|
cx.simulate_keystrokes("v a f");
|
|
cx.assert_state(
|
|
indoc! {r#"
|
|
export const MyComponent = () => {
|
|
return (
|
|
<div>
|
|
<div onClick={«() => {
|
|
console.log("clicked");
|
|
}ˇ»}>Hello world!</div>
|
|
</div>
|
|
);
|
|
};
|
|
"#},
|
|
Mode::VisualLine,
|
|
);
|
|
|
|
cx.set_state(
|
|
indoc! {r#"
|
|
export const MyComponent = () => {
|
|
return (
|
|
<div>
|
|
<div onClick={() => fˇoo()}>Hello world!</div>
|
|
</div>
|
|
);
|
|
};
|
|
"#},
|
|
Mode::Normal,
|
|
);
|
|
cx.simulate_keystrokes("v a f");
|
|
cx.assert_state(
|
|
indoc! {r#"
|
|
export const MyComponent = () => {
|
|
return (
|
|
<div>
|
|
<div onClick={«() => foo()ˇ»}>Hello world!</div>
|
|
</div>
|
|
);
|
|
};
|
|
"#},
|
|
Mode::VisualLine,
|
|
);
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_subword_object(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = VimTestContext::new(cx, true).await;
|
|
|
|
// Setup custom keybindings for subword object so we can use the
|
|
// bindings in `simulate_keystrokes`.
|
|
cx.update(|_window, cx| {
|
|
cx.bind_keys([KeyBinding::new(
|
|
"w",
|
|
super::Subword {
|
|
ignore_punctuation: false,
|
|
},
|
|
Some("vim_operator"),
|
|
)]);
|
|
});
|
|
|
|
cx.set_state("foo_ˇbar_baz", Mode::Normal);
|
|
cx.simulate_keystrokes("c i w");
|
|
cx.assert_state("foo_ˇ_baz", Mode::Insert);
|
|
|
|
cx.set_state("ˇfoo_bar_baz", Mode::Normal);
|
|
cx.simulate_keystrokes("c i w");
|
|
cx.assert_state("ˇ_bar_baz", Mode::Insert);
|
|
|
|
cx.set_state("foo_bar_baˇz", Mode::Normal);
|
|
cx.simulate_keystrokes("c i w");
|
|
cx.assert_state("foo_bar_ˇ", Mode::Insert);
|
|
|
|
cx.set_state("fooˇBarBaz", Mode::Normal);
|
|
cx.simulate_keystrokes("c i w");
|
|
cx.assert_state("fooˇBaz", Mode::Insert);
|
|
|
|
cx.set_state("ˇfooBarBaz", Mode::Normal);
|
|
cx.simulate_keystrokes("c i w");
|
|
cx.assert_state("ˇBarBaz", Mode::Insert);
|
|
|
|
cx.set_state("fooBarBaˇz", Mode::Normal);
|
|
cx.simulate_keystrokes("c i w");
|
|
cx.assert_state("fooBarˇ", Mode::Insert);
|
|
|
|
cx.set_state("foo.ˇbar.baz", Mode::Normal);
|
|
cx.simulate_keystrokes("c i w");
|
|
cx.assert_state("foo.ˇ.baz", Mode::Insert);
|
|
|
|
cx.set_state("foo_ˇbar_baz", Mode::Normal);
|
|
cx.simulate_keystrokes("d i w");
|
|
cx.assert_state("foo_ˇ_baz", Mode::Normal);
|
|
|
|
cx.set_state("fooˇBarBaz", Mode::Normal);
|
|
cx.simulate_keystrokes("d i w");
|
|
cx.assert_state("fooˇBaz", Mode::Normal);
|
|
|
|
cx.set_state("foo_ˇbar_baz", Mode::Normal);
|
|
cx.simulate_keystrokes("c a w");
|
|
cx.assert_state("foo_ˇ_baz", Mode::Insert);
|
|
|
|
cx.set_state("fooˇBarBaz", Mode::Normal);
|
|
cx.simulate_keystrokes("c a w");
|
|
cx.assert_state("fooˇBaz", Mode::Insert);
|
|
|
|
cx.set_state("foo_ˇbar_baz", Mode::Normal);
|
|
cx.simulate_keystrokes("d a w");
|
|
cx.assert_state("foo_ˇ_baz", Mode::Normal);
|
|
|
|
cx.set_state("fooˇBarBaz", Mode::Normal);
|
|
cx.simulate_keystrokes("d a w");
|
|
cx.assert_state("fooˇBaz", Mode::Normal);
|
|
}
|
|
}
|