feat: add linewise Visual selection and paste
This commit is contained in:
+856
-43
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -22,13 +22,13 @@ Normal: i/a insert, I/A insert at first nonblank/end, o/O new line
|
||||
Normal/Visual: prefix motions with counts (5j, 2w, 3n); Esc cancels count
|
||||
u undo, U redo, : command, / search, n/N next/previous match
|
||||
Normal/Visual: . repeats last native edit at current targets (one undo group)
|
||||
v select, p/P paste fragments, Ctrl+k/Ctrl+e clone above/below
|
||||
v characterwise select, V linewise select, p/P paste fragments, Ctrl+k/Ctrl+e clone above/below
|
||||
y/d/c/p/P use the system clipboard by default; config.clipboard_mode = \"separate\" isolates it
|
||||
Esc keeps only the primary caret
|
||||
Visual: motions extend selections, y copy, d/x/Delete cut, c change
|
||||
I/A insert at each selected line's first nonblank/end
|
||||
Ctrl+l split rows, Ctrl+n add occurrence, Ctrl+s skip, Ctrl+a all
|
||||
v/Esc returns to Normal retaining carets
|
||||
v/V switches selection shape; same selector or Esc cancels retaining carets
|
||||
Insert: Esc returns to Normal retaining carets
|
||||
Search: Enter finds next literal match, Esc cancels (case-sensitive, wraps)
|
||||
Commands: :w [PATH], :q, :q!, :wq [PATH], :plugins, :PLUGIN [ARGS]
|
||||
|
||||
@@ -1527,6 +1527,39 @@ end
|
||||
assert_eq!(session.editor().lines(), &["base"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn configured_linewise_change_and_paste_preserve_rows_and_one_replacement_history() {
|
||||
let fixture = Fixture::new();
|
||||
fixture.file("buffer.txt", " alpha\n界beta\ntail");
|
||||
let mut session = fixture.open(
|
||||
r#"
|
||||
Vamp.configure do |config|
|
||||
config.clipboard_mode = "separate"
|
||||
config.bind "V", nil
|
||||
config.bind "s", config.core.select_linewise
|
||||
config.bind "s", config.core.select_linewise, mode: config.modes.visual
|
||||
config.bind "r", config.core.change, mode: config.modes.visual
|
||||
end
|
||||
"#,
|
||||
);
|
||||
for key in "llsjrnew".chars() {
|
||||
session.handle(Input::Char(key));
|
||||
}
|
||||
session.handle(Input::Escape);
|
||||
assert_eq!(session.editor().lines(), &["new", "tail"]);
|
||||
session.handle(Input::Char('p'));
|
||||
assert_eq!(
|
||||
session.editor().lines(),
|
||||
&["new", " alpha", "界beta", "tail"]
|
||||
);
|
||||
session.handle(Input::Char('u'));
|
||||
assert_eq!(session.editor().lines(), &["new", "tail"]);
|
||||
session.handle(Input::Char('u'));
|
||||
assert_eq!(session.editor().lines(), &[" alpha", "界beta", "tail"]);
|
||||
session.handle(Input::Char('U'));
|
||||
assert_eq!(session.editor().lines(), &["new", "tail"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn configured_selection_actions_edit_together_and_invoke_plugins_once() {
|
||||
let fixture = Fixture::new();
|
||||
|
||||
+93
-2
@@ -665,7 +665,18 @@ fn draw(
|
||||
buffer[(viewport.x + (column - *left) as u16, y)].set_style(style);
|
||||
}
|
||||
}
|
||||
for selection in &selections {
|
||||
let selection_style = Style::default().fg(Color::White).bg(Color::Blue);
|
||||
for rows in editor.linewise_rows() {
|
||||
let first = (*rows.start()).max(*top);
|
||||
let last = (*rows.end()).min(top.saturating_add(height - 1));
|
||||
for row in first..=last {
|
||||
let y = viewport.y + (row - *top) as u16;
|
||||
for x in viewport.x..viewport.x + viewport.width {
|
||||
frame.buffer_mut()[(x, y)].set_style(selection_style);
|
||||
}
|
||||
}
|
||||
}
|
||||
for selection in selections.iter().filter(|_| !editor.visual_linewise()) {
|
||||
let first = selection.start.row.max(*top);
|
||||
let last = selection.end.row.min(top.saturating_add(height - 1));
|
||||
let style = Style::default().fg(Color::White).bg(Color::Blue);
|
||||
@@ -712,7 +723,11 @@ fn draw(
|
||||
.chars()
|
||||
.map(|c| if c.is_control() { '�' } else { c })
|
||||
.collect();
|
||||
let mut status = format!(" {}", editor.mode());
|
||||
let mut status = if editor.visual_linewise() {
|
||||
" VISUAL LINE".into()
|
||||
} else {
|
||||
format!(" {}", editor.mode())
|
||||
};
|
||||
if let Some(sequence) = editor.leader_pending() {
|
||||
write!(status, " leader: {sequence}").unwrap();
|
||||
}
|
||||
@@ -5552,6 +5567,82 @@ end
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn linewise_selection_fills_blank_and_final_rows_without_coloring_gutter() {
|
||||
let mut editor = Editor::open(None, vec![]).unwrap();
|
||||
editor
|
||||
.apply(vec![crate::editor::Effect::Replace("long text\n\n".into())])
|
||||
.unwrap();
|
||||
editor.handle(Input::Char('V'));
|
||||
editor.handle(Input::Down);
|
||||
editor.handle(Input::Down);
|
||||
let before = (editor.lines().to_vec(), editor.cursor(), editor.carets());
|
||||
for width in [12, 3, 1] {
|
||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(width, 6)).unwrap();
|
||||
let (mut top, mut left) = (0, 0);
|
||||
terminal
|
||||
.draw(|frame| {
|
||||
draw(
|
||||
frame,
|
||||
&editor,
|
||||
&mut top,
|
||||
&mut left,
|
||||
&mut SyntaxCache::new(),
|
||||
&mut DocumentViewport::default(),
|
||||
&mut WheelPanels::default(),
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
let buffer = terminal.backend().buffer();
|
||||
if width == 12 {
|
||||
let status: String = (0..width).map(|x| buffer[(x, 4)].symbol()).collect();
|
||||
assert!(status.contains("VISUAL LINE"), "{status}");
|
||||
}
|
||||
let gutter = if width > 2 { 2 } else { 0 };
|
||||
for row in 0..3 {
|
||||
for x in 0..gutter {
|
||||
assert_ne!(buffer[(x, row)].bg, Color::Blue);
|
||||
}
|
||||
for x in gutter..width {
|
||||
assert_eq!(buffer[(x, row)].bg, Color::Blue);
|
||||
}
|
||||
}
|
||||
for x in 0..width {
|
||||
assert_ne!(buffer[(x, 3)].bg, Color::Blue);
|
||||
}
|
||||
}
|
||||
assert_eq!(
|
||||
before,
|
||||
(editor.lines().to_vec(), editor.cursor(), editor.carets())
|
||||
);
|
||||
editor.handle(Input::Up);
|
||||
editor.handle(Input::Up);
|
||||
editor.handle(Input::End);
|
||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(5, 3)).unwrap();
|
||||
let (mut top, mut left) = (0, 0);
|
||||
terminal
|
||||
.draw(|frame| {
|
||||
draw(
|
||||
frame,
|
||||
&editor,
|
||||
&mut top,
|
||||
&mut left,
|
||||
&mut SyntaxCache::new(),
|
||||
&mut DocumentViewport::default(),
|
||||
&mut WheelPanels::default(),
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
assert!(left > 0);
|
||||
let buffer = terminal.backend().buffer();
|
||||
for x in 0..2 {
|
||||
assert_ne!(buffer[(x, 0)].bg, Color::Blue);
|
||||
}
|
||||
for x in 2..5 {
|
||||
assert_eq!(buffer[(x, 0)].bg, Color::Blue);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn visual_selection_highlights_graphemes_newlines_and_clipped_ranges_over_search() {
|
||||
let mut editor = Editor::open(None, vec![]).unwrap();
|
||||
|
||||
Reference in New Issue
Block a user