Merge branch 'main' into helix-match-upstream-3

This commit is contained in:
fantacell 2025-08-19 14:22:55 +02:00
commit 85f3e16755
817 changed files with 20299 additions and 16891 deletions

View file

@ -95,7 +95,7 @@ impl VimOption {
}
}
Self::possibilities(&prefix)
Self::possibilities(prefix)
.map(|possible| {
let mut options = prefix_of_options.clone();
options.push(possible);
@ -299,7 +299,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
Vim::action(editor, cx, |vim, action: &VimSave, window, cx| {
vim.update_editor(cx, |_, editor, cx| {
let Some(project) = editor.project.clone() else {
let Some(project) = editor.project().cloned() else {
return;
};
let Some(worktree) = project.read(cx).visible_worktrees(cx).next() else {
@ -436,7 +436,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
let Some(workspace) = vim.workspace(window) else {
return;
};
let Some(project) = editor.project.clone() else {
let Some(project) = editor.project().cloned() else {
return;
};
let Some(worktree) = project.read(cx).visible_worktrees(cx).next() else {

View file

@ -2,12 +2,14 @@ mod boundary;
mod object;
mod select;
use editor::display_map::DisplaySnapshot;
use editor::{DisplayPoint, Editor, SelectionEffects, ToOffset, ToPoint, movement};
use gpui::{Action, actions};
use gpui::{Context, Window};
use language::{CharClassifier, CharKind};
use text::{Bias, SelectionGoal};
use crate::motion;
use crate::{
Vim,
motion::{Motion, right},
@ -19,6 +21,8 @@ actions!(
[
/// Switches to normal mode after the cursor (Helix-style).
HelixNormalAfter,
/// Yanks the current selection or character if no selection.
HelixYank,
/// Inserts at the beginning of the selection.
HelixInsert,
/// Appends at the end of the selection.
@ -30,6 +34,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
Vim::action(editor, cx, Vim::helix_normal_after);
Vim::action(editor, cx, Vim::helix_insert);
Vim::action(editor, cx, Vim::helix_append);
Vim::action(editor, cx, Vim::helix_yank);
}
impl Vim {
@ -59,6 +64,35 @@ impl Vim {
self.helix_move_cursor(motion, times, window, cx);
}
/// Updates all selections based on where the cursors are.
fn helix_new_selections(
&mut self,
window: &mut Window,
cx: &mut Context<Self>,
mut change: impl FnMut(
// the start of the cursor
DisplayPoint,
&DisplaySnapshot,
) -> Option<(DisplayPoint, DisplayPoint)>,
) {
self.update_editor(cx, |_, editor, cx| {
editor.change_selections(Default::default(), window, cx, |s| {
s.move_with(|map, selection| {
let cursor_start = if selection.reversed || selection.is_empty() {
selection.head()
} else {
movement::left(map, selection.head())
};
let Some((head, tail)) = change(cursor_start, map) else {
return;
};
selection.set_head_tail(head, tail, SelectionGoal::None);
});
});
});
}
fn helix_find_range_forward(
&mut self,
times: Option<usize>,
@ -66,49 +100,30 @@ impl Vim {
cx: &mut Context<Self>,
mut is_boundary: impl FnMut(char, char, &CharClassifier) -> bool,
) {
self.update_editor(cx, |_, editor, cx| {
editor.change_selections(Default::default(), window, cx, |s| {
s.move_with(|map, selection| {
let times = times.unwrap_or(1);
let new_goal = SelectionGoal::None;
let mut head = selection.head();
let mut tail = selection.tail();
let times = times.unwrap_or(1);
self.helix_new_selections(window, cx, |cursor, map| {
let mut head = movement::right(map, cursor);
let mut tail = cursor;
let classifier = map.buffer_snapshot.char_classifier_at(head.to_point(map));
if head == map.max_point() {
return None;
}
for _ in 0..times {
let (maybe_next_tail, next_head) =
movement::find_boundary_trail(map, head, |left, right| {
is_boundary(left, right, &classifier)
});
if head == map.max_point() {
return;
}
if next_head == head && maybe_next_tail.unwrap_or(next_head) == tail {
break;
}
// collapse to block cursor
if tail < head {
tail = movement::left(map, head);
} else {
tail = head;
head = movement::right(map, head);
}
// create a classifier
let classifier = map.buffer_snapshot.char_classifier_at(head.to_point(map));
for _ in 0..times {
let (maybe_next_tail, next_head) =
movement::find_boundary_trail(map, head, |left, right| {
is_boundary(left, right, &classifier)
});
if next_head == head && maybe_next_tail.unwrap_or(next_head) == tail {
break;
}
head = next_head;
if let Some(next_tail) = maybe_next_tail {
tail = next_tail;
}
}
selection.set_tail(tail, new_goal);
selection.set_head(head, new_goal);
});
});
head = next_head;
if let Some(next_tail) = maybe_next_tail {
tail = next_tail;
}
}
Some((head, tail))
});
}
@ -119,56 +134,33 @@ impl Vim {
cx: &mut Context<Self>,
mut is_boundary: impl FnMut(char, char, &CharClassifier) -> bool,
) {
self.update_editor(cx, |_, editor, cx| {
editor.change_selections(Default::default(), window, cx, |s| {
s.move_with(|map, selection| {
let times = times.unwrap_or(1);
let new_goal = SelectionGoal::None;
let mut head = selection.head();
let mut tail = selection.tail();
let times = times.unwrap_or(1);
self.helix_new_selections(window, cx, |cursor, map| {
let mut head = cursor;
// The original cursor was one character wide,
// but the search starts from the left side of it,
// so to include that space the selection must end one character to the right.
let mut tail = movement::right(map, cursor);
let classifier = map.buffer_snapshot.char_classifier_at(head.to_point(map));
if head == DisplayPoint::zero() {
return None;
}
for _ in 0..times {
let (maybe_next_tail, next_head) =
movement::find_preceding_boundary_trail(map, head, |left, right| {
is_boundary(left, right, &classifier)
});
if head == DisplayPoint::zero() {
return;
}
if next_head == head && maybe_next_tail.unwrap_or(next_head) == tail {
break;
}
// collapse to block cursor
if tail < head {
tail = movement::left(map, head);
} else {
tail = head;
head = movement::right(map, head);
}
selection.set_head(head, new_goal);
selection.set_tail(tail, new_goal);
// flip the selection
selection.swap_head_tail();
head = selection.head();
tail = selection.tail();
// create a classifier
let classifier = map.buffer_snapshot.char_classifier_at(head.to_point(map));
for _ in 0..times {
let (maybe_next_tail, next_head) =
movement::find_preceding_boundary_trail(map, head, |left, right| {
is_boundary(left, right, &classifier)
});
if next_head == head && maybe_next_tail.unwrap_or(next_head) == tail {
break;
}
head = next_head;
if let Some(next_tail) = maybe_next_tail {
tail = next_tail;
}
}
selection.set_tail(tail, new_goal);
selection.set_head(head, new_goal);
});
})
head = next_head;
if let Some(next_tail) = maybe_next_tail {
tail = next_tail;
}
}
Some((head, tail))
});
}
@ -256,64 +248,100 @@ impl Vim {
found
})
}
Motion::FindForward { .. } => {
self.update_editor(cx, |_, editor, cx| {
let text_layout_details = editor.text_layout_details(window);
editor.change_selections(Default::default(), window, cx, |s| {
s.move_with(|map, selection| {
let goal = selection.goal;
let cursor = if selection.is_empty() || selection.reversed {
selection.head()
} else {
movement::left(map, selection.head())
};
let (point, goal) = motion
.move_point(
map,
cursor,
selection.goal,
times,
&text_layout_details,
)
.unwrap_or((cursor, goal));
selection.set_tail(selection.head(), goal);
selection.set_head(movement::right(map, point), goal);
})
});
Motion::FindForward {
before,
char,
mode,
smartcase,
} => {
self.helix_new_selections(window, cx, |cursor, map| {
let start = cursor;
let mut last_boundary = start;
for _ in 0..times.unwrap_or(1) {
last_boundary = movement::find_boundary(
map,
movement::right(map, last_boundary),
mode,
|left, right| {
let current_char = if before { right } else { left };
motion::is_character_match(char, current_char, smartcase)
},
);
}
Some((last_boundary, start))
});
}
Motion::FindBackward { .. } => {
self.update_editor(cx, |_, editor, cx| {
let text_layout_details = editor.text_layout_details(window);
editor.change_selections(Default::default(), window, cx, |s| {
s.move_with(|map, selection| {
let goal = selection.goal;
let cursor = if selection.is_empty() || selection.reversed {
selection.head()
} else {
movement::left(map, selection.head())
};
let (point, goal) = motion
.move_point(
map,
cursor,
selection.goal,
times,
&text_layout_details,
)
.unwrap_or((cursor, goal));
selection.set_tail(selection.head(), goal);
selection.set_head(point, goal);
})
});
Motion::FindBackward {
after,
char,
mode,
smartcase,
} => {
self.helix_new_selections(window, cx, |cursor, map| {
let start = cursor;
let mut last_boundary = start;
for _ in 0..times.unwrap_or(1) {
last_boundary = movement::find_preceding_boundary_display_point(
map,
last_boundary,
mode,
|left, right| {
let current_char = if after { left } else { right };
motion::is_character_match(char, current_char, smartcase)
},
);
}
// The original cursor was one character wide,
// but the search started from the left side of it,
// so to include that space the selection must end one character to the right.
Some((last_boundary, movement::right(map, start)))
});
}
_ => self.helix_move_and_collapse(motion, times, window, cx),
}
}
pub fn helix_yank(&mut self, _: &HelixYank, window: &mut Window, cx: &mut Context<Self>) {
self.update_editor(cx, |vim, editor, cx| {
let has_selection = editor
.selections
.all_adjusted(cx)
.iter()
.any(|selection| !selection.is_empty());
if !has_selection {
// If no selection, expand to current character (like 'v' does)
editor.change_selections(Default::default(), window, cx, |s| {
s.move_with(|map, selection| {
let head = selection.head();
let new_head = movement::saturating_right(map, head);
selection.set_tail(head, SelectionGoal::None);
selection.set_head(new_head, SelectionGoal::None);
});
});
vim.yank_selections_content(
editor,
crate::motion::MotionKind::Exclusive,
window,
cx,
);
editor.change_selections(Default::default(), window, cx, |s| {
s.move_with(|_map, selection| {
selection.collapse_to(selection.start, SelectionGoal::None);
});
});
} else {
// Yank the selection(s)
vim.yank_selections_content(
editor,
crate::motion::MotionKind::Exclusive,
window,
cx,
);
}
});
}
fn helix_insert(&mut self, _: &HelixInsert, window: &mut Window, cx: &mut Context<Self>) {
self.start_recording(cx);
self.update_editor(cx, |_, editor, cx| {
@ -590,13 +618,33 @@ mod test {
Mode::HelixNormal,
);
cx.simulate_keystrokes("2 T r");
cx.simulate_keystrokes("F e F e");
cx.assert_state(
indoc! {"
The quick br«ˇown
fox jumps over
the laz»y dog."},
The quick brown
fox jumps ov«ˇer
the» lazy dog."},
Mode::HelixNormal,
);
cx.simulate_keystrokes("e 2 F e");
cx.assert_state(
indoc! {"
Th«ˇe quick brown
fox jumps over»
the lazy dog."},
Mode::HelixNormal,
);
cx.simulate_keystrokes("t r t r");
cx.assert_state(
indoc! {"
The quick «brown
fox jumps oveˇ»r
the lazy dog."},
Mode::HelixNormal,
);
}
@ -707,4 +755,29 @@ mod test {
cx.assert_state("«xxˇ»", Mode::HelixNormal);
}
#[gpui::test]
async fn test_helix_yank(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestContext::new(cx, true).await;
cx.enable_helix();
// Test yanking current character with no selection
cx.set_state("hello ˇworld", Mode::HelixNormal);
cx.simulate_keystrokes("y");
// Test cursor remains at the same position after yanking single character
cx.assert_state("hello ˇworld", Mode::HelixNormal);
cx.shared_clipboard().assert_eq("w");
// Move cursor and yank another character
cx.simulate_keystrokes("l");
cx.simulate_keystrokes("y");
cx.shared_clipboard().assert_eq("o");
// Test yanking with existing selection
cx.set_state("hello «worlˇ»d", Mode::HelixNormal);
cx.simulate_keystrokes("y");
cx.shared_clipboard().assert_eq("worl");
cx.assert_state("hello «worlˇ»d", Mode::HelixNormal);
}
}

View file

@ -21,7 +21,7 @@ impl Vim {
return;
};
selection.set_tail_head(range.start, range.end, SelectionGoal::None);
selection.set_head_tail(range.end, range.start, SelectionGoal::None);
});
});
});
@ -45,7 +45,7 @@ impl Vim {
return;
};
selection.set_tail_head(range.start, range.end, SelectionGoal::None);
selection.set_head_tail(range.end, range.start, SelectionGoal::None);
});
});
});
@ -69,7 +69,7 @@ impl Vim {
return;
};
selection.set_tail_head(range.start, range.end, SelectionGoal::None);
selection.set_head_tail(range.end, range.start, SelectionGoal::None);
});
});
});

View file

@ -20,7 +20,7 @@ impl ModeIndicator {
})
.detach();
let handle = cx.entity().clone();
let handle = cx.entity();
let window_handle = window.window_handle();
cx.observe_new::<Vim>(move |_, window, cx| {
let Some(window) = window else {
@ -29,7 +29,7 @@ impl ModeIndicator {
if window.window_handle() != window_handle {
return;
}
let vim = cx.entity().clone();
let vim = cx.entity();
handle.update(cx, |_, cx| {
cx.subscribe(&vim, |mode_indicator, vim, event, cx| match event {
VimEvent::Focused => {

View file

@ -2280,8 +2280,8 @@ fn go_to_line(map: &DisplaySnapshot, display_point: DisplayPoint, line: usize) -
}
let mut last_position = None;
for (excerpt, buffer, range) in map.buffer_snapshot.excerpts() {
let excerpt_range = language::ToOffset::to_offset(&range.context.start, &buffer)
..language::ToOffset::to_offset(&range.context.end, &buffer);
let excerpt_range = language::ToOffset::to_offset(&range.context.start, buffer)
..language::ToOffset::to_offset(&range.context.end, buffer);
if offset >= excerpt_range.start && offset <= excerpt_range.end {
let text_anchor = buffer.anchor_after(offset);
let anchor = Anchor::in_buffer(excerpt, buffer.remote_id(), text_anchor);
@ -2639,7 +2639,8 @@ fn find_backward(
}
}
fn is_character_match(target: char, other: char, smartcase: bool) -> bool {
/// Returns true if one char is equal to the other or its uppercase variant (if smartcase is true).
pub fn is_character_match(target: char, other: char, smartcase: bool) -> bool {
if smartcase {
if target.is_uppercase() {
target == other
@ -2881,7 +2882,7 @@ fn method_motion(
} else {
possibilities.min().unwrap_or(offset)
};
let new_point = map.clip_point(dest.to_display_point(&map), Bias::Left);
let new_point = map.clip_point(dest.to_display_point(map), Bias::Left);
if new_point == display_point {
break;
}
@ -2935,7 +2936,7 @@ fn comment_motion(
} else {
possibilities.min().unwrap_or(offset)
};
let new_point = map.clip_point(dest.to_display_point(&map), Bias::Left);
let new_point = map.clip_point(dest.to_display_point(map), Bias::Left);
if new_point == display_point {
break;
}
@ -3002,7 +3003,7 @@ fn section_motion(
possibilities.min().unwrap_or(map.buffer_snapshot.len())
};
let new_point = map.clip_point(offset.to_display_point(&map), Bias::Left);
let new_point = map.clip_point(offset.to_display_point(map), Bias::Left);
if new_point == display_point {
break;
}

View file

@ -221,7 +221,7 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
return;
};
let anchors = last_change.iter().cloned().collect::<Vec<_>>();
let anchors = last_change.to_vec();
let mut last_row = None;
let ranges: Vec<_> = anchors
.iter()

View file

@ -2,6 +2,7 @@ use crate::{
Vim,
motion::{Motion, MotionKind},
object::Object,
state::Mode,
};
use collections::{HashMap, HashSet};
use editor::{
@ -102,8 +103,20 @@ impl Vim {
// Emulates behavior in vim where if we expanded backwards to include a newline
// the cursor gets set back to the start of the line
let mut should_move_to_start: HashSet<_> = Default::default();
// Emulates behavior in vim where after deletion the cursor should try to move
// 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);
editor.change_selections(Default::default(), window, cx, |s| {
s.move_with(|map, selection| {
let cursor_point = selection.head().to_point(map);
if target_mode == Mode::VisualLine {
column_before_move.insert(selection.id, cursor_point.column);
}
object.expand_selection(map, selection, around, times);
let offset_range = selection.map(|p| p.to_offset(map, Bias::Left)).range();
let mut move_selection_start_to_previous_line =
@ -164,6 +177,15 @@ impl Vim {
let mut cursor = selection.head();
if should_move_to_start.contains(&selection.id) {
*cursor.column_mut() = 0;
} else if let Some(column) = column_before_move.get(&selection.id)
&& *column > 0
{
let mut cursor_point = cursor.to_point(map);
cursor_point.column = *column;
cursor = map
.buffer_snapshot
.clip_point(cursor_point, Bias::Left)
.to_display_point(map);
}
cursor = map.clip_point(cursor, Bias::Left);
selection.collapse_to(cursor, selection.goal)

View file

@ -155,7 +155,7 @@ fn increment_decimal_string(num: &str, delta: i64) -> String {
}
fn increment_hex_string(num: &str, delta: i64) -> String {
let result = if let Ok(val) = u64::from_str_radix(&num, 16) {
let result = if let Ok(val) = u64::from_str_radix(num, 16) {
val.wrapping_add_signed(delta)
} else {
u64::MAX
@ -181,7 +181,7 @@ fn should_use_lowercase(num: &str) -> bool {
}
fn increment_binary_string(num: &str, delta: i64) -> String {
let result = if let Ok(val) = u64::from_str_radix(&num, 2) {
let result = if let Ok(val) = u64::from_str_radix(num, 2) {
val.wrapping_add_signed(delta)
} else {
u64::MAX

View file

@ -549,7 +549,7 @@ mod test {
cx.set_neovim_option("nowrap").await;
let content = "ˇ01234567890123456789";
cx.set_shared_state(&content).await;
cx.set_shared_state(content).await;
cx.simulate_shared_keystrokes("z shift-l").await;
cx.shared_state().await.assert_eq("012345ˇ67890123456789");
@ -560,7 +560,7 @@ mod test {
cx.shared_state().await.assert_eq("012345ˇ67890123456789");
let content = "ˇ01234567890123456789";
cx.set_shared_state(&content).await;
cx.set_shared_state(content).await;
cx.simulate_shared_keystrokes("z l").await;
cx.shared_state().await.assert_eq("0ˇ1234567890123456789");

View file

@ -332,7 +332,7 @@ impl Vim {
Vim::take_forced_motion(cx);
let prior_selections = self.editor_selections(window, cx);
let cursor_word = self.editor_cursor_word(window, cx);
let vim = cx.entity().clone();
let vim = cx.entity();
let searched = pane.update(cx, |pane, cx| {
self.search.direction = direction;

View file

@ -1444,14 +1444,15 @@ fn paragraph(
return None;
}
let paragraph_start_row = paragraph_start.row();
if paragraph_start_row.0 != 0 {
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_row.0 - 1, 0).to_display_point(map);
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 mut start_row = paragraph_end_row.0 + 1;
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;
}
@ -1903,6 +1904,90 @@ mod test {
}
}
#[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`

View file

@ -547,7 +547,7 @@ impl MarksState {
cx: &mut Context<Self>,
) {
let buffer = multibuffer.read(cx).as_singleton();
let abs_path = buffer.as_ref().and_then(|b| self.path_for_buffer(&b, cx));
let abs_path = buffer.as_ref().and_then(|b| self.path_for_buffer(b, cx));
let Some(abs_path) = abs_path else {
self.multibuffer_marks
@ -613,7 +613,7 @@ impl MarksState {
match target? {
MarkLocation::Buffer(entity_id) => {
let anchors = self.multibuffer_marks.get(&entity_id)?;
let anchors = self.multibuffer_marks.get(entity_id)?;
return Some(Mark::Buffer(*entity_id, anchors.get(name)?.clone()));
}
MarkLocation::Path(path) => {
@ -643,7 +643,7 @@ impl MarksState {
match target {
MarkLocation::Buffer(entity_id) => {
self.multibuffer_marks
.get_mut(&entity_id)
.get_mut(entity_id)
.map(|m| m.remove(&mark_name.clone()));
return;
}
@ -1038,13 +1038,21 @@ impl Operator {
}
pub fn status(&self) -> String {
fn make_visible(c: &str) -> &str {
match c {
"\n" => "enter",
"\t" => "tab",
" " => "space",
c => c,
}
}
match self {
Operator::Digraph {
first_char: Some(first_char),
} => format!("^K{first_char}"),
} => format!("^K{}", make_visible(&first_char.to_string())),
Operator::Literal {
prefix: Some(prefix),
} => format!("^V{prefix}"),
} => format!("^V{}", make_visible(prefix)),
Operator::AutoIndent => "=".to_string(),
Operator::ShellCommand => "=".to_string(),
Operator::HelixMatch => "m".to_string(),

View file

@ -67,7 +67,7 @@ impl NeovimConnection {
// Ensure we don't create neovim connections in parallel
let _lock = NEOVIM_LOCK.lock();
let (nvim, join_handle, child) = new_child_cmd(
&mut Command::new("nvim")
Command::new("nvim")
.arg("--embed")
.arg("--clean")
// disable swap (otherwise after about 1000 test runs you run out of swap file names)
@ -161,7 +161,7 @@ impl NeovimConnection {
#[cfg(feature = "neovim")]
pub async fn set_state(&mut self, marked_text: &str) {
let (text, selections) = parse_state(&marked_text);
let (text, selections) = parse_state(marked_text);
let nvim_buffer = self
.nvim

View file

@ -143,6 +143,16 @@ impl VimTestContext {
})
}
pub fn enable_helix(&mut self) {
self.cx.update(|_, cx| {
SettingsStore::update_global(cx, |store, cx| {
store.update_user_settings::<vim_mode_setting::HelixModeSetting>(cx, |s| {
*s = Some(true)
});
});
})
}
pub fn mode(&mut self) -> Mode {
self.update_editor(|editor, _, cx| editor.addon::<VimAddon>().unwrap().entity.read(cx).mode)
}
@ -210,6 +220,26 @@ impl VimTestContext {
assert_eq!(self.mode(), Mode::Normal, "{}", self.assertion_context());
assert_eq!(self.active_operator(), None, "{}", self.assertion_context());
}
pub fn shared_clipboard(&mut self) -> VimClipboard {
VimClipboard {
editor: self
.read_from_clipboard()
.map(|item| item.text().unwrap().to_string())
.unwrap_or_default(),
}
}
}
pub struct VimClipboard {
editor: String,
}
impl VimClipboard {
#[track_caller]
pub fn assert_eq(&self, expected: &str) {
assert_eq!(self.editor, expected);
}
}
impl Deref for VimTestContext {

View file

@ -283,7 +283,7 @@ pub fn init(cx: &mut App) {
workspace.register_action(|workspace, _: &MaximizePane, window, cx| {
let pane = workspace.active_pane();
let Some(size) = workspace.bounding_box_for_pane(&pane) else {
let Some(size) = workspace.bounding_box_for_pane(pane) else {
return;
};
@ -302,9 +302,7 @@ pub fn init(cx: &mut App) {
let count = Vim::take_count(cx).unwrap_or(1) as f32;
Vim::take_forced_motion(cx);
let theme = ThemeSettings::get_global(cx);
let Ok(font_id) = window.text_system().font_id(&theme.buffer_font) else {
return;
};
let font_id = window.text_system().resolve_font(&theme.buffer_font);
let Ok(width) = window
.text_system()
.advance(font_id, theme.buffer_font_size(cx), 'm')
@ -318,9 +316,7 @@ pub fn init(cx: &mut App) {
let count = Vim::take_count(cx).unwrap_or(1) as f32;
Vim::take_forced_motion(cx);
let theme = ThemeSettings::get_global(cx);
let Ok(font_id) = window.text_system().font_id(&theme.buffer_font) else {
return;
};
let font_id = window.text_system().resolve_font(&theme.buffer_font);
let Ok(width) = window
.text_system()
.advance(font_id, theme.buffer_font_size(cx), 'm')
@ -424,7 +420,7 @@ impl Vim {
const NAMESPACE: &'static str = "vim";
pub fn new(window: &mut Window, cx: &mut Context<Editor>) -> Entity<Self> {
let editor = cx.entity().clone();
let editor = cx.entity();
let mut initial_mode = VimSettings::get_global(cx).default_mode;
if initial_mode == Mode::Normal && HelixModeSetting::get_global(cx).0 {
@ -1642,7 +1638,7 @@ impl Vim {
second_char,
smartcase: VimSettings::get_global(cx).use_smartcase_find,
};
Vim::globals(cx).last_find = Some((&sneak).clone());
Vim::globals(cx).last_find = Some(sneak.clone());
self.motion(sneak, window, cx)
}
} else {
@ -1659,7 +1655,7 @@ impl Vim {
second_char,
smartcase: VimSettings::get_global(cx).use_smartcase_find,
};
Vim::globals(cx).last_find = Some((&sneak).clone());
Vim::globals(cx).last_find = Some(sneak.clone());
self.motion(sneak, window, cx)
}
} else {

View file

@ -414,6 +414,8 @@ impl Vim {
);
}
let original_point = selection.tail().to_point(map);
if let Some(range) = object.range(map, mut_selection, around, count) {
if !range.is_empty() {
let expand_both_ways = object.always_expands_both_ways()
@ -462,6 +464,37 @@ impl Vim {
};
selection.end = new_selection_end.to_display_point(map);
}
// To match vim, if the range starts of the same line as it originally
// did, we keep the tail of the selection in the same place instead of
// snapping it to the start of the line
if target_mode == Mode::VisualLine {
let new_start_point = selection.start.to_point(map);
if new_start_point.row == original_point.row {
if selection.end.to_point(map).row > new_start_point.row {
if original_point.column
== map
.buffer_snapshot
.line_len(MultiBufferRow(original_point.row))
{
selection.start = movement::saturating_left(
map,
original_point.to_display_point(map),
)
} else {
selection.start = original_point.to_display_point(map)
}
} else {
selection.end = movement::saturating_right(
map,
original_point.to_display_point(map),
);
if original_point.column > 0 {
selection.reversed = true
}
}
}
}
}
});
});