Compare commits
3 Commits
debug-shel
...
fix-assumi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b06459ee00 | ||
|
|
db7425e264 | ||
|
|
704bcf96ba |
@@ -339,6 +339,10 @@
|
||||
"w": "vim::NextWordStart",
|
||||
"e": "vim::NextWordEnd",
|
||||
"b": "vim::PreviousWordStart",
|
||||
"x": "vim::CurrentLine",
|
||||
"X": "vim::CurrentLine",
|
||||
"y": "vim::HelixYank",
|
||||
"p": "vim::HelixPaste",
|
||||
|
||||
"h": "vim::Left",
|
||||
"j": "vim::Down",
|
||||
|
||||
@@ -1,16 +1,32 @@
|
||||
use editor::display_map::ToDisplayPoint;
|
||||
use editor::{DisplayPoint, Editor, movement, scroll::Autoscroll};
|
||||
use gpui::{Action, actions};
|
||||
use gpui::{Action, actions, impl_actions};
|
||||
use gpui::{Context, Window};
|
||||
use language::{CharClassifier, CharKind};
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::motion::MotionKind;
|
||||
use crate::{Vim, motion::Motion, state::Mode};
|
||||
|
||||
actions!(vim, [HelixNormalAfter, HelixDelete]);
|
||||
actions!(vim, [HelixNormalAfter, HelixDelete, HelixYank]);
|
||||
|
||||
#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct HelixPaste {
|
||||
#[serde(default)]
|
||||
before: bool,
|
||||
#[serde(default)]
|
||||
preserve_clipboard: bool,
|
||||
}
|
||||
|
||||
impl_actions!(vim, [HelixPaste]);
|
||||
|
||||
pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
|
||||
Vim::action(editor, cx, Vim::helix_normal_after);
|
||||
Vim::action(editor, cx, Vim::helix_delete);
|
||||
Vim::action(editor, cx, Vim::helix_yank);
|
||||
Vim::action(editor, cx, Vim::helix_paste);
|
||||
}
|
||||
|
||||
impl Vim {
|
||||
@@ -235,6 +251,25 @@ impl Vim {
|
||||
found
|
||||
})
|
||||
}
|
||||
Motion::CurrentLine => {
|
||||
self.update_editor(window, cx, |vim, editor, window, cx| {
|
||||
let text_layout_details = editor.text_layout_details(window);
|
||||
editor.transact(window, cx, |editor, window, cx| {
|
||||
editor.set_clip_at_line_ends(false, cx);
|
||||
editor.change_selections(None, window, cx, |s| {
|
||||
s.move_with(|map, selection| {
|
||||
motion.expand_selection(
|
||||
map,
|
||||
selection,
|
||||
times,
|
||||
&text_layout_details,
|
||||
);
|
||||
})
|
||||
});
|
||||
editor.selections.line_mode = true;
|
||||
});
|
||||
});
|
||||
}
|
||||
_ => self.helix_move_and_collapse(motion, times, window, cx),
|
||||
}
|
||||
}
|
||||
@@ -242,23 +277,212 @@ impl Vim {
|
||||
pub fn helix_delete(&mut self, _: &HelixDelete, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.store_visual_marks(window, cx);
|
||||
self.update_editor(window, cx, |vim, editor, window, cx| {
|
||||
// Fixup selections so they have helix's semantics.
|
||||
// Specifically:
|
||||
// - Make sure that each cursor acts as a 1 character wide selection
|
||||
editor.transact(window, cx, |editor, window, cx| {
|
||||
editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
|
||||
s.move_with(|map, selection| {
|
||||
if selection.is_empty() && !selection.reversed {
|
||||
selection.end = movement::right(map, selection.end);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
fixup_selections(editor, window, cx);
|
||||
vim.copy_selections_content(editor, MotionKind::Exclusive, window, cx);
|
||||
editor.insert("", window, cx);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn helix_yank(&mut self, _: &HelixYank, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.store_visual_marks(window, cx);
|
||||
self.update_editor(window, cx, |vim, editor, window, cx| {
|
||||
fixup_selections(editor, window, cx);
|
||||
let motion_kind = if editor.selections.line_mode {
|
||||
MotionKind::Linewise
|
||||
} else {
|
||||
MotionKind::Exclusive
|
||||
};
|
||||
vim.copy_ranges(
|
||||
editor,
|
||||
motion_kind,
|
||||
true,
|
||||
editor
|
||||
.selections
|
||||
.all_adjusted(cx)
|
||||
.iter()
|
||||
.map(|s| s.range())
|
||||
.collect(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn helix_paste(
|
||||
&mut self,
|
||||
action: &HelixPaste,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if self.mode != Mode::HelixNormal {
|
||||
return;
|
||||
}
|
||||
self.record_current_action(cx);
|
||||
self.store_visual_marks(window, cx);
|
||||
let count = Vim::take_count(cx).unwrap_or(1);
|
||||
|
||||
self.update_editor(window, cx, |vim, editor, window, cx| {
|
||||
let text_layout_details = editor.text_layout_details(window);
|
||||
_ = editor.transact(window, cx, |editor, window, cx| {
|
||||
editor.set_clip_at_line_ends(false, cx);
|
||||
|
||||
let selected_register = vim.selected_register.take();
|
||||
|
||||
let Some(crate::state::Register {
|
||||
text,
|
||||
clipboard_selections,
|
||||
}) = Vim::update_globals(cx, |globals, cx| {
|
||||
globals.read_register(selected_register, Some(editor), cx)
|
||||
})
|
||||
.filter(|reg| !reg.text.is_empty())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let clipboard_selections = clipboard_selections
|
||||
.filter(|sel| sel.len() > 1 && vim.mode != Mode::VisualLine);
|
||||
|
||||
if !action.preserve_clipboard && vim.mode.is_visual() {
|
||||
vim.copy_selections_content(editor, MotionKind::for_mode(vim.mode), window, cx);
|
||||
}
|
||||
|
||||
let (display_map, current_selections) = editor.selections.all_adjusted_display(cx);
|
||||
|
||||
// unlike zed, if you have a multi-cursor selection from vim block mode,
|
||||
// pasting it will paste it on subsequent lines, even if you don't yet
|
||||
// have a cursor there.
|
||||
let mut selections_to_process = Vec::new();
|
||||
let mut i = 0;
|
||||
while i < current_selections.len() {
|
||||
selections_to_process
|
||||
.push((current_selections[i].start..current_selections[i].end, true));
|
||||
i += 1;
|
||||
}
|
||||
if let Some(clipboard_selections) = clipboard_selections.as_ref() {
|
||||
let left = current_selections
|
||||
.iter()
|
||||
.map(|selection| {
|
||||
std::cmp::min(selection.start.column(), selection.end.column())
|
||||
})
|
||||
.min()
|
||||
.unwrap();
|
||||
let mut row =
|
||||
editor::RowExt::next_row(¤t_selections.last().unwrap().end.row());
|
||||
while i < clipboard_selections.len() {
|
||||
let cursor =
|
||||
display_map.clip_point(DisplayPoint::new(row, left), text::Bias::Left);
|
||||
selections_to_process.push((cursor..cursor, false));
|
||||
i += 1;
|
||||
row.0 += 1;
|
||||
}
|
||||
}
|
||||
|
||||
let first_selection_indent_column =
|
||||
clipboard_selections.as_ref().and_then(|zed_selections| {
|
||||
zed_selections
|
||||
.first()
|
||||
.map(|selection| selection.first_line_indent)});
|
||||
let before = action.before || vim.mode == Mode::VisualLine;
|
||||
|
||||
let mut edits = Vec::new();
|
||||
let mut new_selections = Vec::new();
|
||||
let mut original_indent_columns = Vec::new();
|
||||
let mut start_offset = 0;
|
||||
|
||||
for (ix, (selection, preserve)) in selections_to_process.iter().enumerate() {
|
||||
let (mut to_insert, original_indent_column) =
|
||||
if let Some(clipboard_selections) = &clipboard_selections {
|
||||
if let Some(clipboard_selection) = clipboard_selections.get(ix) {
|
||||
let end_offset = start_offset + clipboard_selection.len;
|
||||
let text = text[start_offset..end_offset].to_string();
|
||||
start_offset = end_offset + 1;
|
||||
dbg!((text, Some(clipboard_selection.first_line_indent)))
|
||||
} else {
|
||||
dbg!(("".to_string(), first_selection_indent_column))
|
||||
}
|
||||
} else {
|
||||
dbg!((text.to_string(), first_selection_indent_column))
|
||||
};
|
||||
let line_mode = to_insert.ends_with('\n');
|
||||
|
||||
if line_mode && !before {
|
||||
if selection.is_empty() {
|
||||
to_insert =
|
||||
"\n".to_owned() + &to_insert[..to_insert.len() - "\n".len()];
|
||||
} else {
|
||||
to_insert = "\n".to_owned() + &to_insert;
|
||||
}
|
||||
} else if line_mode && vim.mode == Mode::VisualLine {
|
||||
to_insert.pop();
|
||||
}
|
||||
|
||||
let display_range = if line_mode {
|
||||
let point = if before {
|
||||
movement::line_beginning(&display_map, selection.start, false)
|
||||
} else {
|
||||
movement::line_end(&display_map, selection.end, false)
|
||||
};
|
||||
point..point
|
||||
} else {
|
||||
let point = if before {
|
||||
selection.start
|
||||
} else {
|
||||
movement::saturating_right(&display_map, selection.end)
|
||||
};
|
||||
point..point
|
||||
};
|
||||
|
||||
let point_range = display_range.start.to_point(&display_map)
|
||||
..display_range.end.to_point(&display_map);
|
||||
|
||||
let selection_beg =
|
||||
display_map.buffer_snapshot.anchor_before(point_range.start);
|
||||
let selection_end = display_map.buffer_snapshot.anchor_after(point_range.end);
|
||||
edits.push((point_range, to_insert.repeat(count)));
|
||||
new_selections.push((selection_beg, selection_end));
|
||||
original_indent_columns.push(original_indent_column);
|
||||
}
|
||||
|
||||
let cursor_offset = editor.selections.last::<usize>(cx).head();
|
||||
if editor
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.snapshot(cx)
|
||||
.language_settings_at(cursor_offset, cx)
|
||||
.auto_indent_on_paste
|
||||
{
|
||||
editor.edit_with_block_indent(edits, original_indent_columns, cx);
|
||||
} else {
|
||||
editor.edit(edits, cx);
|
||||
}
|
||||
|
||||
// in line_mode vim will insert the new text on the next (or previous if before) line
|
||||
// and put the cursor on the first non-blank character of the first inserted line (or at the end if the first line is blank).
|
||||
// otherwise vim will insert the next text at (or before) the current cursor position,
|
||||
// the cursor will go to the last (or first, if is_multiline) inserted character.
|
||||
editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
|
||||
let display_map = s.display_map();
|
||||
s.select_display_ranges(new_selections.into_iter().map(|(s, e)| {
|
||||
s.to_display_point(&display_map)..e.to_display_point(&display_map)
|
||||
}));
|
||||
})
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Fixup selections so they have helix's semantics.
|
||||
/// Specifically:
|
||||
/// - Make sure that each cursor acts as a 1 character wide selection
|
||||
fn fixup_selections(editor: &mut Editor, window: &mut Window, cx: &mut Context<Editor>) {
|
||||
editor.transact(window, cx, |editor, window, cx| {
|
||||
editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
|
||||
s.move_with(|map, selection| {
|
||||
if selection.is_empty() && !selection.reversed {
|
||||
selection.end = movement::right(map, selection.end);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -381,4 +605,48 @@ mod test {
|
||||
Mode::HelixNormal,
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_yank_and_paste(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
|
||||
cx.set_state(
|
||||
indoc! {"
|
||||
The quick brown
|
||||
fox jumps over
|
||||
the lazy dog.ˇ"},
|
||||
Mode::HelixNormal,
|
||||
);
|
||||
|
||||
cx.simulate_keystrokes("x");
|
||||
|
||||
cx.assert_state(
|
||||
indoc! {"
|
||||
The quick brown
|
||||
fox jumps over
|
||||
«the lazy dog.ˇ»"},
|
||||
Mode::HelixNormal,
|
||||
);
|
||||
|
||||
cx.simulate_keystrokes("y");
|
||||
|
||||
cx.assert_state(
|
||||
indoc! {"
|
||||
The quick brown
|
||||
fox jumps over
|
||||
«the lazy dog.ˇ»"},
|
||||
Mode::HelixNormal,
|
||||
);
|
||||
|
||||
cx.simulate_keystrokes("p");
|
||||
|
||||
cx.assert_state(
|
||||
indoc! {"
|
||||
The quick brown
|
||||
fox jumps over
|
||||
the lazy dog.
|
||||
«the lazy dog.ˇ»"},
|
||||
Mode::HelixNormal,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user