Merge branch 'main' into helix-match-upstream-3
This commit is contained in:
commit
85f3e16755
817 changed files with 20299 additions and 16891 deletions
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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`
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue