Compare commits

...

5 Commits

Author SHA1 Message Date
dino
727bce98cd test(vim): add simple test to delete object with new object scope
Introduce a simple test to verify that, if `PushObject { around: true,
whitespace: false }` is used, the surrounding whitespace will not be
deleted.
2025-10-28 16:57:08 +00:00
dino
40a2f1e458 refactor(vim): remove calls to around and whitespace
Remove `ObjectScope.around` and `ObjectScope.inside`, ensuring that
pretty much all object related functions are actually leveraging
`ObjectScope` instead of the `around` and `whitespace` booleans.

In some rare cases the `around` boolean was kept because it made more
sense in the function's context to do so, as it'll likely never need the
`whitespace` argument, but this is something we can refactor as we work
more on the codebase.
2025-10-28 16:14:17 +00:00
dino
613e851df9 refactor(vim): leverage scope in normal object
Update `vim::normal::Vim.normal_object` to always provide the scope to
the functions being called.
2025-10-28 14:42:04 +00:00
dino
56bd762ac7 chore(vim): introduce enum to control object scope
In order to avoid having to pass around the values that determine
whether an operator should be applied to inside the object or include
the object as well as the surrounding whitespace, this commit introduces
a new enum, `vim::state::ObjectScope`, which we'll use to pass this
information around.

As such, `vim::state::Operator::Object` has already been updated so that
it now only has a `scope` field with the `ObejctScope`. Existing
functionality has been migrated so as to calculate the `around` and
`whitespace` values from this scope, but a future commit will update it
so that all dependencies start operating on the `ObjectScope` instead.
2025-10-28 12:52:38 +00:00
dino
562b8589e7 feat(vim): allow ignoring whitespace in around motion
- Update `PushObject` with `whitespace: bool` field to control whether,
when `around: true`, whitespace should be included in the selection
(vim's default) or not (neovim's default).
- Update `Object` with a `whitespace: bool` field, reflecting the
changes in `PushObject`.
- Update `vim::normal::delete::Vim.delete_object` to correctly leverage
this new `whitespace` value. This is meant as a proof of concept and
we'd need to implement this for other operations like, for example, the
change operator.
2025-10-27 14:19:29 +00:00
19 changed files with 314 additions and 191 deletions

View File

@@ -47,7 +47,7 @@ use crate::{
search::{FindCommand, ReplaceCommand, Replacement},
},
object::Object,
state::{Mark, Mode},
state::{Mark, Mode, ObjectScope},
visual::VisualDeleteLine,
};
@@ -2043,7 +2043,7 @@ impl Vim {
pub fn shell_command_object(
&mut self,
object: Object,
around: bool,
scope: ObjectScope,
window: &mut Window,
cx: &mut Context<Vim>,
) {
@@ -2057,7 +2057,7 @@ impl Vim {
.selections
.newest_display(&editor.display_snapshot(cx));
let range = object
.range(&snapshot, start.clone(), around, None)
.range(&snapshot, start.clone(), &scope, None)
.unwrap_or(start.range());
if range.start != start.start {
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {

View File

@@ -10,6 +10,7 @@ use text::Selection;
use crate::{
helix::boundary::{FuzzyBoundary, ImmediateBoundary},
object::Object as VimObject,
state::ObjectScope,
};
/// A text object from helix or an extra one
@@ -43,11 +44,17 @@ impl VimObject {
self,
map: &DisplaySnapshot,
selection: Selection<DisplayPoint>,
around: bool,
scope: &ObjectScope,
) -> Result<Option<Range<DisplayPoint>>, VimToHelixError> {
let cursor = cursor_range(&selection, map);
if let Some(helix_object) = self.to_helix_object() {
Ok(helix_object.range(map, cursor, around))
// TODO!: Does it make sense to update the trait so as to work on
// `ObjectScope` instead of the `around` bool?
Ok(helix_object.range(
map,
cursor,
matches!(scope, ObjectScope::Around | ObjectScope::AroundTrimmed),
))
} else {
Err(VimToHelixError)
}

View File

@@ -1,7 +1,7 @@
use text::SelectionGoal;
use ui::{Context, Window};
use crate::{Vim, helix::object::cursor_range, object::Object};
use crate::{Vim, helix::object::cursor_range, object::Object, state::ObjectScope};
impl Vim {
/// Selects the object each cursor is over.
@@ -9,7 +9,7 @@ impl Vim {
pub fn select_current_object(
&mut self,
object: Object,
around: bool,
scope: ObjectScope,
window: &mut Window,
cx: &mut Context<Self>,
) {
@@ -18,9 +18,9 @@ impl Vim {
editor.change_selections(Default::default(), window, cx, |s| {
s.move_with(|map, selection| {
let Some(range) = object
.helix_range(map, selection.clone(), around)
.helix_range(map, selection.clone(), &scope)
.unwrap_or({
let vim_range = object.range(map, selection.clone(), around, None);
let vim_range = object.range(map, selection.clone(), &scope, None);
vim_range.filter(|r| r.start <= cursor_range(selection, map).start)
})
else {

View File

@@ -1,4 +1,9 @@
use crate::{Vim, motion::Motion, object::Object, state::Mode};
use crate::{
Vim,
motion::Motion,
object::Object,
state::{Mode, ObjectScope},
};
use collections::HashMap;
use editor::SelectionEffects;
use editor::{Bias, Editor, display_map::ToDisplayPoint};
@@ -136,7 +141,7 @@ impl Vim {
pub(crate) fn indent_object(
&mut self,
object: Object,
around: bool,
scope: ObjectScope,
dir: IndentDirection,
times: Option<usize>,
window: &mut Window,
@@ -150,7 +155,7 @@ impl Vim {
s.move_with(|map, selection| {
let anchor = map.display_point_to_anchor(selection.head(), Bias::Right);
original_positions.insert(selection.id, anchor);
object.expand_selection(map, selection, around, times);
object.expand_selection(map, selection, &scope, times);
});
});
match dir {

View File

@@ -16,7 +16,7 @@ use workspace::searchable::Direction;
use crate::{
Vim,
normal::mark,
state::{Mode, Operator},
state::{Mode, ObjectScope, Operator},
surrounds::SurroundsType,
};
@@ -2336,8 +2336,8 @@ fn end_of_document(
}
fn matching_tag(map: &DisplaySnapshot, head: DisplayPoint) -> Option<DisplayPoint> {
let inner = crate::object::surrounding_html_tag(map, head, head..head, false)?;
let outer = crate::object::surrounding_html_tag(map, head, head..head, true)?;
let inner = crate::object::surrounding_html_tag(map, head, head..head, &ObjectScope::Inside)?;
let outer = crate::object::surrounding_html_tag(map, head, head..head, &ObjectScope::Around)?;
if head > outer.start && head < inner.start {
let mut offset = inner.end.to_offset(map, Bias::Left);

View File

@@ -19,7 +19,7 @@ use crate::{
indent::IndentDirection,
motion::{self, Motion, first_non_whitespace, next_line_end, right},
object::Object,
state::{Mark, Mode, Operator},
state::{Mark, Mode, ObjectScope, Operator},
surrounds::SurroundsType,
};
use collections::BTreeSet;
@@ -454,62 +454,80 @@ impl Vim {
) {
let mut waiting_operator: Option<Operator> = None;
match self.maybe_pop_operator() {
Some(Operator::Object { around }) => match self.maybe_pop_operator() {
Some(Operator::Change) => self.change_object(object, around, times, window, cx),
Some(Operator::Delete) => self.delete_object(object, around, times, window, cx),
Some(Operator::Yank) => self.yank_object(object, around, times, window, cx),
Some(Operator::Indent) => {
self.indent_object(object, around, IndentDirection::In, times, window, cx)
Some(Operator::Object { scope }) => {
let (around, _whitespace) = match scope {
ObjectScope::Inside => (false, false),
ObjectScope::Around => (true, true),
ObjectScope::AroundTrimmed => (true, false),
};
match self.maybe_pop_operator() {
Some(Operator::Change) => self.change_object(object, scope, times, window, cx),
Some(Operator::Delete) => self.delete_object(object, scope, times, window, cx),
Some(Operator::Yank) => self.yank_object(object, scope, times, window, cx),
Some(Operator::Indent) => {
self.indent_object(object, scope, IndentDirection::In, times, window, cx)
}
Some(Operator::Outdent) => {
self.indent_object(object, scope, IndentDirection::Out, times, window, cx)
}
Some(Operator::AutoIndent) => {
self.indent_object(object, scope, IndentDirection::Auto, times, window, cx)
}
Some(Operator::ShellCommand) => {
self.shell_command_object(object, scope, window, cx);
}
Some(Operator::Rewrap) => self.rewrap_object(object, scope, times, window, cx),
Some(Operator::Lowercase) => self.convert_object(
object,
scope,
ConvertTarget::LowerCase,
times,
window,
cx,
),
Some(Operator::Uppercase) => self.convert_object(
object,
scope,
ConvertTarget::UpperCase,
times,
window,
cx,
),
Some(Operator::OppositeCase) => self.convert_object(
object,
scope,
ConvertTarget::OppositeCase,
times,
window,
cx,
),
Some(Operator::Rot13) => {
self.convert_object(object, scope, ConvertTarget::Rot13, times, window, cx)
}
Some(Operator::Rot47) => {
self.convert_object(object, scope, ConvertTarget::Rot47, times, window, cx)
}
Some(Operator::AddSurrounds { target: None }) => {
waiting_operator = Some(Operator::AddSurrounds {
target: Some(SurroundsType::Object(object, around)),
});
}
Some(Operator::ToggleComments) => {
self.toggle_comments_object(object, scope, times, window, cx)
}
Some(Operator::ReplaceWithRegister) => {
self.replace_with_register_object(object, scope, window, cx)
}
Some(Operator::Exchange) => self.exchange_object(object, scope, window, cx),
Some(Operator::HelixMatch) => {
self.select_current_object(object, scope, window, cx)
}
_ => {
// Can't do anything for namespace operators. Ignoring
}
}
Some(Operator::Outdent) => {
self.indent_object(object, around, IndentDirection::Out, times, window, cx)
}
Some(Operator::AutoIndent) => {
self.indent_object(object, around, IndentDirection::Auto, times, window, cx)
}
Some(Operator::ShellCommand) => {
self.shell_command_object(object, around, window, cx);
}
Some(Operator::Rewrap) => self.rewrap_object(object, around, times, window, cx),
Some(Operator::Lowercase) => {
self.convert_object(object, around, ConvertTarget::LowerCase, times, window, cx)
}
Some(Operator::Uppercase) => {
self.convert_object(object, around, ConvertTarget::UpperCase, times, window, cx)
}
Some(Operator::OppositeCase) => self.convert_object(
object,
around,
ConvertTarget::OppositeCase,
times,
window,
cx,
),
Some(Operator::Rot13) => {
self.convert_object(object, around, ConvertTarget::Rot13, times, window, cx)
}
Some(Operator::Rot47) => {
self.convert_object(object, around, ConvertTarget::Rot47, times, window, cx)
}
Some(Operator::AddSurrounds { target: None }) => {
waiting_operator = Some(Operator::AddSurrounds {
target: Some(SurroundsType::Object(object, around)),
});
}
Some(Operator::ToggleComments) => {
self.toggle_comments_object(object, around, times, window, cx)
}
Some(Operator::ReplaceWithRegister) => {
self.replace_with_register_object(object, around, window, cx)
}
Some(Operator::Exchange) => self.exchange_object(object, around, window, cx),
Some(Operator::HelixMatch) => {
self.select_current_object(object, around, window, cx)
}
_ => {
// Can't do anything for namespace operators. Ignoring
}
},
}
Some(Operator::HelixNext { around }) => {
self.select_next_object(object, around, window, cx);
}

View File

@@ -2,7 +2,7 @@ use crate::{
Vim,
motion::{self, Motion, MotionKind},
object::Object,
state::Mode,
state::{Mode, ObjectScope},
};
use editor::{
Bias, DisplayPoint,
@@ -105,7 +105,7 @@ impl Vim {
pub fn change_object(
&mut self,
object: Object,
around: bool,
scope: ObjectScope,
times: Option<usize>,
window: &mut Window,
cx: &mut Context<Self>,
@@ -117,7 +117,7 @@ impl Vim {
editor.transact(window, cx, |editor, window, cx| {
editor.change_selections(Default::default(), window, cx, |s| {
s.move_with(|map, selection| {
objects_found |= object.expand_selection(map, selection, around, times);
objects_found |= object.expand_selection(map, selection, &scope, times);
});
});
if objects_found {

View File

@@ -9,7 +9,7 @@ use crate::{
motion::Motion,
normal::{ChangeCase, ConvertToLowerCase, ConvertToRot13, ConvertToRot47, ConvertToUpperCase},
object::Object,
state::Mode,
state::{Mode, ObjectScope},
};
pub enum ConvertTarget {
@@ -80,7 +80,7 @@ impl Vim {
pub fn convert_object(
&mut self,
object: Object,
around: bool,
scope: ObjectScope,
mode: ConvertTarget,
times: Option<usize>,
window: &mut Window,
@@ -93,7 +93,7 @@ impl Vim {
let mut original_positions: HashMap<_, _> = Default::default();
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.move_with(|map, selection| {
object.expand_selection(map, selection, around, times);
object.expand_selection(map, selection, &scope, times);
original_positions.insert(
selection.id,
map.display_point_to_anchor(selection.start, Bias::Left),

View File

@@ -1,5 +1,5 @@
use crate::{
Vim,
ObjectScope, Vim,
motion::{Motion, MotionKind},
object::Object,
state::Mode,
@@ -92,7 +92,7 @@ impl Vim {
pub fn delete_object(
&mut self,
object: Object,
around: bool,
scope: ObjectScope,
times: Option<usize>,
window: &mut Window,
cx: &mut Context<Self>,
@@ -109,7 +109,7 @@ impl Vim {
// to the same column it was before deletion if the line is not empty or only
// contains whitespace
let mut column_before_move: HashMap<_, _> = Default::default();
let target_mode = object.target_visual_mode(vim.mode, around);
let target_mode = object.target_visual_mode(vim.mode, &scope);
editor.change_selections(Default::default(), window, cx, |s| {
s.move_with(|map, selection| {
@@ -118,7 +118,7 @@ impl Vim {
column_before_move.insert(selection.id, cursor_point.column);
}
object.expand_selection(map, selection, around, times);
object.expand_selection(map, selection, &scope, times);
let offset_range = selection.map(|p| p.to_offset(map, Bias::Left)).range();
let mut move_selection_start_to_previous_line =
|map: &DisplaySnapshot, selection: &mut Selection<DisplayPoint>| {
@@ -145,7 +145,10 @@ impl Vim {
// If expanded range contains only newlines and
// the object is around or sentence, expand to include a newline
// at the end or start
if (around || object == Object::Sentence) && contains_only_newlines {
if (matches!(scope, ObjectScope::AroundTrimmed | ObjectScope::Around)
|| object == Object::Sentence)
&& contains_only_newlines
{
if end_at_newline {
move_selection_end_to_next_line(map, selection);
} else {
@@ -155,7 +158,9 @@ impl Vim {
// Does post-processing for the trailing newline and EOF
// when not cancelled.
let cancelled = around && selection.start == selection.end;
let cancelled =
matches!(scope, ObjectScope::AroundTrimmed | ObjectScope::Around)
&& selection.start == selection.end;
if object == Object::Paragraph && !cancelled {
// EOF check should be done before including a trailing newline.
if ends_at_eof(map, selection) {
@@ -209,9 +214,11 @@ fn ends_at_eof(map: &DisplaySnapshot, selection: &mut Selection<DisplayPoint>) -
#[cfg(test)]
mod test {
use gpui::KeyBinding;
use indoc::indoc;
use crate::{
PushObject,
state::Mode,
test::{NeovimBackedTestContext, VimTestContext},
};
@@ -760,4 +767,24 @@ mod test {
.await
.assert_matches();
}
#[gpui::test]
async fn test_delete_object_scope(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestContext::new(cx, true).await;
cx.update(|_, cx| {
cx.bind_keys([KeyBinding::new(
"a",
PushObject {
around: true,
whitespace: false,
},
Some("VimControl && !menu"),
)]);
});
cx.set_state("some 'ˇquotes' here", Mode::Normal);
cx.simulate_keystrokes("d a '");
cx.assert_editor_state("some ˇ here");
}
}

View File

@@ -11,7 +11,7 @@ use crate::{
Vim,
motion::{Motion, MotionKind},
object::Object,
state::{Mode, Register},
state::{Mode, ObjectScope, Register},
};
/// Pastes text from the specified register at the cursor position.
@@ -233,7 +233,7 @@ impl Vim {
pub fn replace_with_register_object(
&mut self,
object: Object,
around: bool,
scope: ObjectScope,
window: &mut Window,
cx: &mut Context<Self>,
) {
@@ -244,7 +244,7 @@ impl Vim {
editor.set_clip_at_line_ends(false, cx);
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.move_with(|map, selection| {
object.expand_selection(map, selection, around, None);
object.expand_selection(map, selection, &scope, None);
});
});

View File

@@ -1,4 +1,4 @@
use crate::{Vim, motion::Motion, object::Object};
use crate::{ObjectScope, Vim, motion::Motion, object::Object};
use collections::HashMap;
use editor::{Bias, SelectionEffects, display_map::ToDisplayPoint};
use gpui::{Context, Window};
@@ -45,7 +45,7 @@ impl Vim {
pub fn toggle_comments_object(
&mut self,
object: Object,
around: bool,
scope: ObjectScope,
times: Option<usize>,
window: &mut Window,
cx: &mut Context<Self>,
@@ -58,7 +58,7 @@ impl Vim {
s.move_with(|map, selection| {
let anchor = map.display_point_to_anchor(selection.head(), Bias::Right);
original_positions.insert(selection.id, anchor);
object.expand_selection(map, selection, around, times);
object.expand_selection(map, selection, &scope, times);
});
});
editor.toggle_comments(&Default::default(), window, cx);

View File

@@ -4,7 +4,7 @@ use crate::{
Vim, VimSettings,
motion::{Motion, MotionKind},
object::Object,
state::{Mode, Register},
state::{Mode, ObjectScope, Register},
};
use collections::HashMap;
use editor::{ClipboardSelection, Editor, SelectionEffects};
@@ -65,7 +65,7 @@ impl Vim {
pub fn yank_object(
&mut self,
object: Object,
around: bool,
scope: ObjectScope,
times: Option<usize>,
window: &mut Window,
cx: &mut Context<Self>,
@@ -76,7 +76,7 @@ impl Vim {
let mut start_positions: HashMap<_, _> = Default::default();
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.move_with(|map, selection| {
object.expand_selection(map, selection, around, times);
object.expand_selection(map, selection, &scope, times);
let start_position = (selection.start, selection.goal);
start_positions.insert(selection.id, start_position);
});

View File

@@ -3,7 +3,7 @@ use std::ops::Range;
use crate::{
Vim,
motion::right,
state::{Mode, Operator},
state::{Mode, ObjectScope, Operator},
};
use editor::{
Bias, DisplayPoint, Editor, ToOffset,
@@ -204,9 +204,10 @@ impl DelimiterRange {
fn find_mini_delimiters(
map: &DisplaySnapshot,
display_point: DisplayPoint,
around: bool,
scope: &ObjectScope,
is_valid_delimiter: &DelimiterPredicate,
) -> Option<Range<DisplayPoint>> {
let around = matches!(scope, ObjectScope::Around | ObjectScope::AroundTrimmed);
let point = map.clip_at_line_end(display_point).to_point(map);
let offset = point.to_offset(&map.buffer_snapshot());
@@ -278,17 +279,17 @@ fn is_bracket_delimiter(buffer: &BufferSnapshot, start: usize, _end: usize) -> b
fn find_mini_quotes(
map: &DisplaySnapshot,
display_point: DisplayPoint,
around: bool,
scope: &ObjectScope,
) -> Option<Range<DisplayPoint>> {
find_mini_delimiters(map, display_point, around, &is_quote_delimiter)
find_mini_delimiters(map, display_point, scope, &is_quote_delimiter)
}
fn find_mini_brackets(
map: &DisplaySnapshot,
display_point: DisplayPoint,
around: bool,
scope: &ObjectScope,
) -> Option<Range<DisplayPoint>> {
find_mini_delimiters(map, display_point, around, &is_bracket_delimiter)
find_mini_delimiters(map, display_point, scope, &is_bracket_delimiter)
}
actions!(
@@ -406,7 +407,13 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
});
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.push_operator(
Operator::Object {
scope: ObjectScope::Around,
},
window,
cx,
);
}
vim.object(Object::Comment, window, cx)
});
@@ -504,7 +511,7 @@ impl Object {
}
}
pub fn target_visual_mode(self, current_mode: Mode, around: bool) -> Mode {
pub fn target_visual_mode(self, current_mode: Mode, scope: &ObjectScope) -> Mode {
match self {
Object::Word { .. }
| Object::Subword { .. }
@@ -531,13 +538,10 @@ impl Object {
| Object::Comment
| Object::Argument
| Object::IndentObj { .. } => Mode::Visual,
Object::Method | Object::Class => {
if around {
Mode::VisualLine
} else {
Mode::Visual
}
}
Object::Method | Object::Class => match scope {
ObjectScope::Around | ObjectScope::AroundTrimmed => Mode::VisualLine,
ObjectScope::Inside => Mode::Visual,
},
Object::Paragraph | Object::EntireFile => Mode::VisualLine,
}
}
@@ -546,33 +550,31 @@ impl Object {
self,
map: &DisplaySnapshot,
selection: Selection<DisplayPoint>,
around: bool,
scope: &ObjectScope,
times: Option<usize>,
) -> Option<Range<DisplayPoint>> {
let relative_to = selection.head();
match self {
Object::Word { ignore_punctuation } => {
if around {
Object::Word { ignore_punctuation } => match scope {
ObjectScope::Around | ObjectScope::AroundTrimmed => {
around_word(map, relative_to, ignore_punctuation)
} else {
in_word(map, relative_to, ignore_punctuation)
}
}
Object::Subword { ignore_punctuation } => {
if around {
ObjectScope::Inside => in_word(map, relative_to, ignore_punctuation),
},
Object::Subword { ignore_punctuation } => match scope {
ObjectScope::Around | ObjectScope::AroundTrimmed => {
around_subword(map, relative_to, ignore_punctuation)
} else {
in_subword(map, relative_to, ignore_punctuation)
}
}
Object::Sentence => sentence(map, relative_to, around),
ObjectScope::Inside => in_subword(map, relative_to, ignore_punctuation),
},
Object::Sentence => sentence(map, relative_to, scope),
//change others later
Object::Paragraph => paragraph(map, relative_to, around, times.unwrap_or(1)),
Object::Paragraph => paragraph(map, relative_to, scope, times.unwrap_or(1)),
Object::Quotes => {
surrounding_markers(map, relative_to, around, self.is_multiline(), '\'', '\'')
surrounding_markers(map, relative_to, scope, self.is_multiline(), '\'', '\'')
}
Object::BackQuotes => {
surrounding_markers(map, relative_to, around, self.is_multiline(), '`', '`')
surrounding_markers(map, relative_to, scope, self.is_multiline(), '`', '`')
}
Object::AnyQuotes => {
let quote_types = ['\'', '"', '`'];
@@ -587,7 +589,7 @@ impl Object {
if let Some(range) = surrounding_markers(
map,
relative_to,
around,
scope,
self.is_multiline(),
quote,
quote,
@@ -616,7 +618,7 @@ impl Object {
surrounding_markers(
map,
relative_to,
around,
scope,
self.is_multiline(),
quote,
quote,
@@ -634,20 +636,20 @@ impl Object {
}
})
}
Object::MiniQuotes => find_mini_quotes(map, relative_to, around),
Object::MiniQuotes => find_mini_quotes(map, relative_to, scope),
Object::DoubleQuotes => {
surrounding_markers(map, relative_to, around, self.is_multiline(), '"', '"')
surrounding_markers(map, relative_to, scope, self.is_multiline(), '"', '"')
}
Object::VerticalBars => {
surrounding_markers(map, relative_to, around, self.is_multiline(), '|', '|')
surrounding_markers(map, relative_to, scope, self.is_multiline(), '|', '|')
}
Object::Parentheses => {
surrounding_markers(map, relative_to, around, self.is_multiline(), '(', ')')
surrounding_markers(map, relative_to, scope, self.is_multiline(), '(', ')')
}
Object::Tag => {
let head = selection.head();
let range = selection.range();
surrounding_html_tag(map, head, range, around)
surrounding_html_tag(map, head, range, scope)
}
Object::AnyBrackets => {
let bracket_pairs = [('(', ')'), ('[', ']'), ('{', '}'), ('<', '>')];
@@ -661,7 +663,7 @@ impl Object {
if let Some(range) = surrounding_markers(
map,
relative_to,
around,
scope,
self.is_multiline(),
open,
close,
@@ -690,7 +692,7 @@ impl Object {
surrounding_markers(
map,
relative_to,
around,
scope,
self.is_multiline(),
open,
close,
@@ -708,45 +710,42 @@ impl Object {
}
})
}
Object::MiniBrackets => find_mini_brackets(map, relative_to, around),
Object::MiniBrackets => find_mini_brackets(map, relative_to, scope),
Object::SquareBrackets => {
surrounding_markers(map, relative_to, around, self.is_multiline(), '[', ']')
surrounding_markers(map, relative_to, scope, self.is_multiline(), '[', ']')
}
Object::CurlyBrackets => {
surrounding_markers(map, relative_to, around, self.is_multiline(), '{', '}')
surrounding_markers(map, relative_to, scope, self.is_multiline(), '{', '}')
}
Object::AngleBrackets => {
surrounding_markers(map, relative_to, around, self.is_multiline(), '<', '>')
surrounding_markers(map, relative_to, scope, self.is_multiline(), '<', '>')
}
Object::Method => text_object(
map,
relative_to,
if around {
TextObject::AroundFunction
} else {
TextObject::InsideFunction
match scope {
ObjectScope::Around | ObjectScope::AroundTrimmed => TextObject::AroundFunction,
ObjectScope::Inside => TextObject::InsideFunction,
},
),
Object::Comment => text_object(
map,
relative_to,
if around {
TextObject::AroundComment
} else {
TextObject::InsideComment
match scope {
ObjectScope::Around | ObjectScope::AroundTrimmed => TextObject::AroundComment,
ObjectScope::Inside => TextObject::InsideComment,
},
),
Object::Class => text_object(
map,
relative_to,
if around {
TextObject::AroundClass
} else {
TextObject::InsideClass
match scope {
ObjectScope::Around | ObjectScope::AroundTrimmed => TextObject::AroundClass,
ObjectScope::Inside => TextObject::InsideClass,
},
),
Object::Argument => argument(map, relative_to, around),
Object::IndentObj { include_below } => indent(map, relative_to, around, include_below),
Object::Argument => argument(map, relative_to, scope),
Object::IndentObj { include_below } => indent(map, relative_to, scope, include_below),
Object::EntireFile => entire_file(map),
}
}
@@ -755,10 +754,10 @@ impl Object {
self,
map: &DisplaySnapshot,
selection: &mut Selection<DisplayPoint>,
around: bool,
scope: &ObjectScope,
times: Option<usize>,
) -> bool {
if let Some(range) = self.range(map, selection.clone(), around, times) {
if let Some(range) = self.range(map, selection.clone(), scope, times) {
selection.start = range.start;
selection.end = range.end;
true
@@ -857,7 +856,7 @@ pub fn surrounding_html_tag(
map: &DisplaySnapshot,
head: DisplayPoint,
range: Range<DisplayPoint>,
around: bool,
scope: &ObjectScope,
) -> Option<Range<DisplayPoint>> {
fn read_tag(chars: impl Iterator<Item = char>) -> String {
chars
@@ -909,7 +908,8 @@ pub fn surrounding_html_tag(
&& range.end.to_offset(map, Bias::Left) <= last_child.start_byte() + 1
};
if open_tag.is_some() && open_tag == close_tag && is_valid {
let range = if around {
let range = if matches!(scope, ObjectScope::Around | ObjectScope::AroundTrimmed)
{
first_child.byte_range().start..last_child.byte_range().end
} else {
first_child.byte_range().end..last_child.byte_range().start
@@ -1110,7 +1110,7 @@ fn text_object(
fn argument(
map: &DisplaySnapshot,
relative_to: DisplayPoint,
around: bool,
scope: &ObjectScope,
) -> Option<Range<DisplayPoint>> {
let snapshot = &map.buffer_snapshot();
let offset = relative_to.to_offset(map, Bias::Left);
@@ -1246,7 +1246,9 @@ fn argument(
Some(start..end)
}
let result = comma_delimited_range_at(buffer, excerpt.map_offset_to_buffer(offset), around)?;
let include_comma = matches!(scope, ObjectScope::Around | ObjectScope::AroundTrimmed);
let result =
comma_delimited_range_at(buffer, excerpt.map_offset_to_buffer(offset), include_comma)?;
if excerpt.contains_buffer_range(result.clone()) {
let result = excerpt.map_range_from_buffer(result);
@@ -1259,9 +1261,10 @@ fn argument(
fn indent(
map: &DisplaySnapshot,
relative_to: DisplayPoint,
around: bool,
scope: &ObjectScope,
include_below: bool,
) -> Option<Range<DisplayPoint>> {
let around = matches!(scope, ObjectScope::Around | ObjectScope::AroundTrimmed);
let point = relative_to.to_point(map);
let row = point.row;
@@ -1311,13 +1314,13 @@ fn indent(
fn sentence(
map: &DisplaySnapshot,
relative_to: DisplayPoint,
around: bool,
scope: &ObjectScope,
) -> 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();
let around = matches!(scope, ObjectScope::Around | ObjectScope::AroundTrimmed);
// Search backwards for the previous sentence end or current sentence start. Include the character under relative_to
for (char, offset) in chars
@@ -1463,7 +1466,7 @@ pub fn expand_to_include_whitespace(
fn paragraph(
map: &DisplaySnapshot,
relative_to: DisplayPoint,
around: bool,
scope: &ObjectScope,
times: usize,
) -> Option<Range<DisplayPoint>> {
let mut paragraph_start = start_of_paragraph(map, relative_to);
@@ -1477,7 +1480,7 @@ fn paragraph(
.buffer_snapshot()
.is_line_blank(MultiBufferRow(point.row));
if around {
if matches!(scope, ObjectScope::Around | ObjectScope::AroundTrimmed) {
if paragraph_ends_with_eof {
if current_line_is_empty {
return None;
@@ -1558,12 +1561,13 @@ pub fn end_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) -> D
pub fn surrounding_markers(
map: &DisplaySnapshot,
relative_to: DisplayPoint,
around: bool,
scope: &ObjectScope,
search_across_lines: bool,
open_marker: char,
close_marker: char,
) -> Option<Range<DisplayPoint>> {
let point = relative_to.to_offset(map, Bias::Left);
let around = matches!(scope, ObjectScope::Around | ObjectScope::AroundTrimmed);
let mut matched_closes = 0;
let mut opening = None;
@@ -1669,7 +1673,12 @@ pub fn surrounding_markers(
for (ch, range) in movement::chars_after(map, closing.end) {
if ch.is_whitespace() && ch != '\n' {
found = true;
closing.end = range.end;
// Only update closing range's `end` value if whitespace is
// meant to be included.
if *scope == ObjectScope::Around {
closing.end = range.end;
}
} else {
break;
}
@@ -1678,7 +1687,11 @@ pub fn surrounding_markers(
if !found {
for (ch, range) in movement::chars_before(map, opening.start) {
if ch.is_whitespace() && ch != '\n' {
opening.start = range.start
// Only update closing range's `start` value if whitespace
// is meant to be included.
if *scope == ObjectScope::Around {
opening.start = range.start
}
} else {
break;
}

View File

@@ -2,7 +2,7 @@ use crate::{
Vim,
motion::{self, Motion},
object::Object,
state::Mode,
state::{Mode, ObjectScope},
};
use editor::{
Anchor, Bias, Editor, EditorSnapshot, SelectionEffects, ToOffset, ToPoint,
@@ -143,7 +143,7 @@ impl Vim {
pub fn exchange_object(
&mut self,
object: Object,
around: bool,
scope: ObjectScope,
window: &mut Window,
cx: &mut Context<Self>,
) {
@@ -154,7 +154,7 @@ impl Vim {
.selections
.newest_display(&editor.display_snapshot(cx));
let snapshot = editor.snapshot(window, cx);
object.expand_selection(&snapshot, &mut selection, around, None);
object.expand_selection(&snapshot, &mut selection, &scope, None);
let start = snapshot
.buffer_snapshot()
.anchor_before(selection.start.to_point(&snapshot));

View File

@@ -1,4 +1,9 @@
use crate::{Vim, motion::Motion, object::Object, state::Mode};
use crate::{
Vim,
motion::Motion,
object::Object,
state::{Mode, ObjectScope},
};
use collections::HashMap;
use editor::{Bias, Editor, RewrapOptions, SelectionEffects, display_map::ToDisplayPoint};
use gpui::{Context, Window, actions};
@@ -94,7 +99,7 @@ impl Vim {
pub(crate) fn rewrap_object(
&mut self,
object: Object,
around: bool,
scope: ObjectScope,
times: Option<usize>,
window: &mut Window,
cx: &mut Context<Self>,
@@ -107,7 +112,7 @@ impl Vim {
s.move_with(|map, selection| {
let anchor = map.display_point_to_anchor(selection.head(), Bias::Right);
original_positions.insert(selection.id, anchor);
object.expand_selection(map, selection, around, times);
object.expand_selection(map, selection, &scope, times);
});
});
editor.rewrap_impl(

View File

@@ -2,7 +2,10 @@ use crate::command::command_interceptor;
use crate::motion::MotionKind;
use crate::normal::repeat::Replayer;
use crate::surrounds::SurroundsType;
use crate::{ToggleMarksView, ToggleRegistersView, UseSystemClipboard, Vim, VimAddon, VimSettings};
use crate::{
PushObject, ToggleMarksView, ToggleRegistersView, UseSystemClipboard, Vim, VimAddon,
VimSettings,
};
use crate::{motion::Motion, object::Object};
use anyhow::Result;
use collections::HashMap;
@@ -87,7 +90,7 @@ pub enum Operator {
Yank,
Replace,
Object {
around: bool,
scope: ObjectScope,
},
FindForward {
before: bool,
@@ -149,6 +152,40 @@ pub enum Operator {
},
}
/// Controls how the object interacts with its delimiters and the surrounding
/// whitespace.
#[derive(Clone, Debug, PartialEq)]
pub(crate) enum ObjectScope {
/// Inside the delimiters, excluding whitespace.
///
/// Used by the `i` operator (e.g., `diw` for "delete inner word").
/// Selects only the content between delimiters without including
/// the delimiters themselves or surrounding whitespace.
Inside,
/// Around the delimiters, including surrounding whitespace.
///
/// Used by the `a` operator (e.g., `daw` for "delete a word").
/// Selects the content, the delimiters, and any surrounding whitespace.
Around,
/// Around the delimiters, excluding surrounding whitespace.
///
/// Similar to `Around`, but does not include whitespace adjacent to
/// the delimiters.
AroundTrimmed,
}
impl ObjectScope {
/// Create the `ObjectScope` from a `PushObject` action, taking into account
/// its `around` and `whitespace` values.
pub(crate) fn from_action(action: &PushObject) -> Self {
match (action.around, action.whitespace) {
(false, _) => ObjectScope::Inside,
(true, true) => ObjectScope::Around,
(true, false) => ObjectScope::AroundTrimmed,
}
}
}
#[derive(Default, Clone, Debug)]
pub enum RecordedSelection {
#[default]
@@ -996,8 +1033,12 @@ pub struct SearchState {
impl Operator {
pub fn id(&self) -> &'static str {
match self {
Operator::Object { around: false } => "i",
Operator::Object { around: true } => "a",
Operator::Object {
scope: ObjectScope::Inside,
} => "i",
Operator::Object {
scope: ObjectScope::Around | ObjectScope::AroundTrimmed,
} => "a",
Operator::Change => "c",
Operator::Delete => "d",
Operator::Yank => "y",

View File

@@ -2,7 +2,7 @@ use crate::{
Vim,
motion::{self, Motion},
object::{Object, surrounding_markers},
state::Mode,
state::{Mode, ObjectScope},
};
use editor::{Bias, movement};
use gpui::{Context, Window};
@@ -53,7 +53,13 @@ impl Vim {
for selection in &display_selections {
let range = match &target {
SurroundsType::Object(object, around) => {
object.range(&display_map, selection.clone(), *around, None)
// TODO!: Should `SurroundsType::Object` be updated to also leverage `ObjectScope`?
let scope = match around {
true => ObjectScope::Around,
false => ObjectScope::Inside,
};
object.range(&display_map, selection.clone(), &scope, None)
}
SurroundsType::Motion(motion) => {
motion
@@ -152,8 +158,9 @@ impl Vim {
for selection in &display_selections {
let start = selection.start.to_offset(&display_map, Bias::Left);
let scope = ObjectScope::Around;
if let Some(range) =
pair_object.range(&display_map, selection.clone(), true, None)
pair_object.range(&display_map, selection.clone(), &scope, None)
{
// If the current parenthesis object is single-line,
// then we need to filter whether it is the current line or not
@@ -265,8 +272,9 @@ impl Vim {
for selection in &selections {
let start = selection.start.to_offset(&display_map, Bias::Left);
let scope = ObjectScope::Around;
if let Some(range) =
target.range(&display_map, selection.clone(), true, None)
target.range(&display_map, selection.clone(), &scope, None)
{
if !target.is_multiline() {
let is_same_row = selection.start.row() == range.start.row()
@@ -391,8 +399,9 @@ impl Vim {
for selection in &selections {
let start = selection.start.to_offset(&display_map, Bias::Left);
let scope = ObjectScope::Around;
if let Some(range) =
object.range(&display_map, selection.clone(), true, None)
object.range(&display_map, selection.clone(), &scope, None)
{
// If the current parenthesis object is single-line,
// then we need to filter whether it is the current line or not
@@ -533,7 +542,7 @@ impl Vim {
if let Some(range) = surrounding_markers(
&display_map,
relative_to,
true,
&ObjectScope::Around,
false,
open,
close,

View File

@@ -42,11 +42,12 @@ use serde::Deserialize;
pub use settings::{
ModeContent, Settings, SettingsStore, UseSystemClipboard, update_settings_file,
};
use state::{Mode, Operator, RecordedSelection, SearchState, VimGlobals};
use state::{Mode, ObjectScope, Operator, RecordedSelection, SearchState, VimGlobals};
use std::{mem, ops::Range, sync::Arc};
use surrounds::SurroundsType;
use theme::ThemeSettings;
use ui::{IntoElement, SharedString, px};
use util::serde::default_true;
use vim_mode_setting::HelixModeSetting;
use vim_mode_setting::VimModeSetting;
use workspace::{self, Pane, Workspace};
@@ -71,6 +72,8 @@ struct SelectRegister(String);
#[serde(deny_unknown_fields)]
struct PushObject {
around: bool,
#[serde(default = "default_true")]
whitespace: bool,
}
#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
@@ -659,13 +662,8 @@ impl Vim {
Vim::globals(cx).forced_motion = true;
});
Vim::action(editor, cx, |vim, action: &PushObject, window, cx| {
vim.push_operator(
Operator::Object {
around: action.around,
},
window,
cx,
)
let scope = ObjectScope::from_action(action);
vim.push_operator(Operator::Object { scope }, window, cx)
});
Vim::action(editor, cx, |vim, action: &PushFindForward, window, cx| {

View File

@@ -426,10 +426,10 @@ impl Vim {
window: &mut Window,
cx: &mut Context<Vim>,
) {
if let Some(Operator::Object { around }) = self.active_operator() {
if let Some(Operator::Object { scope }) = self.active_operator() {
self.pop_operator(window, cx);
let current_mode = self.mode;
let target_mode = object.target_visual_mode(current_mode, around);
let target_mode = object.target_visual_mode(current_mode, &scope);
if target_mode != current_mode {
self.switch_mode(target_mode, true, window, cx);
}
@@ -453,7 +453,7 @@ impl Vim {
let original_point = selection.tail().to_point(map);
if let Some(range) = object.range(map, mut_selection, around, count) {
if let Some(range) = object.range(map, mut_selection, &scope, count) {
if !range.is_empty() {
let expand_both_ways = object.always_expands_both_ways()
|| selection.is_empty()
@@ -465,7 +465,7 @@ impl Vim {
&& object.always_expands_both_ways()
{
if let Some(range) =
object.range(map, selection.clone(), around, count)
object.range(map, selection.clone(), &scope, count)
{
selection.start = range.start;
selection.end = range.end;