Vim visual block mode
This isn't quite an exact emulation, as instead of using one selection that is magically in "column mode", we emulate it with a bunch of zed multi-selections (one per line). I think this is better, as it requires fewer changes to the codebase, and lets you see the impact of any changes immediately on all lines. Fixes: zed-industries/community#984
This commit is contained in:
parent
1cc0798aea
commit
1b4dd49b1d
9 changed files with 518 additions and 135 deletions
|
@ -1,5 +1,8 @@
|
|||
#[cfg(feature = "neovim")]
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::{
|
||||
cmp,
|
||||
ops::{Deref, DerefMut},
|
||||
};
|
||||
use std::{ops::Range, path::PathBuf};
|
||||
|
||||
#[cfg(feature = "neovim")]
|
||||
|
@ -135,7 +138,7 @@ impl NeovimConnection {
|
|||
|
||||
#[cfg(feature = "neovim")]
|
||||
pub async fn set_state(&mut self, marked_text: &str) {
|
||||
let (text, selection) = parse_state(&marked_text);
|
||||
let (text, selections) = parse_state(&marked_text);
|
||||
|
||||
let nvim_buffer = self
|
||||
.nvim
|
||||
|
@ -167,6 +170,11 @@ impl NeovimConnection {
|
|||
.await
|
||||
.expect("Could not get neovim window");
|
||||
|
||||
if selections.len() != 1 {
|
||||
panic!("must have one selection");
|
||||
}
|
||||
let selection = &selections[0];
|
||||
|
||||
let cursor = selection.start;
|
||||
nvim_window
|
||||
.set_cursor((cursor.row as i64 + 1, cursor.column as i64))
|
||||
|
@ -224,7 +232,7 @@ impl NeovimConnection {
|
|||
}
|
||||
|
||||
#[cfg(feature = "neovim")]
|
||||
pub async fn state(&mut self) -> (Option<Mode>, String, Range<Point>) {
|
||||
pub async fn state(&mut self) -> (Option<Mode>, String, Vec<Range<Point>>) {
|
||||
let nvim_buffer = self
|
||||
.nvim
|
||||
.get_current_buf()
|
||||
|
@ -263,14 +271,48 @@ impl NeovimConnection {
|
|||
"n" => Some(Mode::Normal),
|
||||
"v" => Some(Mode::Visual),
|
||||
"V" => Some(Mode::VisualLine),
|
||||
"CTRL-V" => Some(Mode::VisualBlock),
|
||||
"\x16" => Some(Mode::VisualBlock),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let mut selections = Vec::new();
|
||||
// Vim uses the index of the first and last character in the selection
|
||||
// Zed uses the index of the positions between the characters, so we need
|
||||
// to add one to the end in visual mode.
|
||||
match mode {
|
||||
Some(Mode::VisualBlock) if selection_row != cursor_row => {
|
||||
// in zed we fake a block selecrtion by using multiple cursors (one per line)
|
||||
// this code emulates that.
|
||||
// to deal with casees where the selection is not perfectly rectangular we extract
|
||||
// the content of the selection via the "a register to get the shape correctly.
|
||||
self.nvim.input("\"aygv").await.unwrap();
|
||||
let content = self.nvim.command_output("echo getreg('a')").await.unwrap();
|
||||
let lines = content.split("\n").collect::<Vec<_>>();
|
||||
let top = cmp::min(selection_row, cursor_row);
|
||||
let left = cmp::min(selection_col, cursor_col);
|
||||
for row in top..=cmp::max(selection_row, cursor_row) {
|
||||
let content = if row - top >= lines.len() as u32 {
|
||||
""
|
||||
} else {
|
||||
lines[(row - top) as usize]
|
||||
};
|
||||
let line_len = self
|
||||
.read_position(format!("echo strlen(getline({}))", row + 1).as_str())
|
||||
.await;
|
||||
|
||||
if left > line_len {
|
||||
continue;
|
||||
}
|
||||
|
||||
let start = Point::new(row, left);
|
||||
let end = Point::new(row, left + content.len() as u32);
|
||||
if cursor_col >= selection_col {
|
||||
selections.push(start..end)
|
||||
} else {
|
||||
selections.push(end..start)
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(Mode::Visual) | Some(Mode::VisualLine) | Some(Mode::VisualBlock) => {
|
||||
if selection_col > cursor_col {
|
||||
let selection_line_length =
|
||||
|
@ -291,38 +333,37 @@ impl NeovimConnection {
|
|||
cursor_row += 1;
|
||||
}
|
||||
}
|
||||
selections.push(
|
||||
Point::new(selection_row, selection_col)..Point::new(cursor_row, cursor_col),
|
||||
)
|
||||
}
|
||||
Some(Mode::Insert) | Some(Mode::Normal) | None => {}
|
||||
Some(Mode::Insert) | Some(Mode::Normal) | None => selections
|
||||
.push(Point::new(selection_row, selection_col)..Point::new(cursor_row, cursor_col)),
|
||||
}
|
||||
|
||||
let (start, end) = (
|
||||
Point::new(selection_row, selection_col),
|
||||
Point::new(cursor_row, cursor_col),
|
||||
);
|
||||
|
||||
let state = NeovimData::Get {
|
||||
mode,
|
||||
state: encode_range(&text, start..end),
|
||||
state: encode_ranges(&text, &selections),
|
||||
};
|
||||
|
||||
if self.data.back() != Some(&state) {
|
||||
self.data.push_back(state.clone());
|
||||
}
|
||||
|
||||
(mode, text, start..end)
|
||||
(mode, text, selections)
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "neovim"))]
|
||||
pub async fn state(&mut self) -> (Option<Mode>, String, Range<Point>) {
|
||||
pub async fn state(&mut self) -> (Option<Mode>, String, Vec<Range<Point>>) {
|
||||
if let Some(NeovimData::Get { state: text, mode }) = self.data.front() {
|
||||
let (text, range) = parse_state(text);
|
||||
(*mode, text, range)
|
||||
let (text, ranges) = parse_state(text);
|
||||
(*mode, text, ranges)
|
||||
} else {
|
||||
panic!("operation does not match recorded script. re-record with --features=neovim");
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn selection(&mut self) -> Range<Point> {
|
||||
pub async fn selections(&mut self) -> Vec<Range<Point>> {
|
||||
self.state().await.2
|
||||
}
|
||||
|
||||
|
@ -422,51 +463,63 @@ impl Handler for NvimHandler {
|
|||
}
|
||||
}
|
||||
|
||||
fn parse_state(marked_text: &str) -> (String, Range<Point>) {
|
||||
fn parse_state(marked_text: &str) -> (String, Vec<Range<Point>>) {
|
||||
let (text, ranges) = util::test::marked_text_ranges(marked_text, true);
|
||||
let byte_range = ranges[0].clone();
|
||||
let mut point_range = Point::zero()..Point::zero();
|
||||
let mut ix = 0;
|
||||
let mut position = Point::zero();
|
||||
for c in text.chars().chain(['\0']) {
|
||||
if ix == byte_range.start {
|
||||
point_range.start = position;
|
||||
}
|
||||
if ix == byte_range.end {
|
||||
point_range.end = position;
|
||||
}
|
||||
let len_utf8 = c.len_utf8();
|
||||
ix += len_utf8;
|
||||
if c == '\n' {
|
||||
position.row += 1;
|
||||
position.column = 0;
|
||||
} else {
|
||||
position.column += len_utf8 as u32;
|
||||
}
|
||||
}
|
||||
(text, point_range)
|
||||
let point_ranges = ranges
|
||||
.into_iter()
|
||||
.map(|byte_range| {
|
||||
let mut point_range = Point::zero()..Point::zero();
|
||||
let mut ix = 0;
|
||||
let mut position = Point::zero();
|
||||
for c in text.chars().chain(['\0']) {
|
||||
if ix == byte_range.start {
|
||||
point_range.start = position;
|
||||
}
|
||||
if ix == byte_range.end {
|
||||
point_range.end = position;
|
||||
}
|
||||
let len_utf8 = c.len_utf8();
|
||||
ix += len_utf8;
|
||||
if c == '\n' {
|
||||
position.row += 1;
|
||||
position.column = 0;
|
||||
} else {
|
||||
position.column += len_utf8 as u32;
|
||||
}
|
||||
}
|
||||
point_range
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
(text, point_ranges)
|
||||
}
|
||||
|
||||
#[cfg(feature = "neovim")]
|
||||
fn encode_range(text: &str, range: Range<Point>) -> String {
|
||||
let mut byte_range = 0..0;
|
||||
let mut ix = 0;
|
||||
let mut position = Point::zero();
|
||||
for c in text.chars().chain(['\0']) {
|
||||
if position == range.start {
|
||||
byte_range.start = ix;
|
||||
}
|
||||
if position == range.end {
|
||||
byte_range.end = ix;
|
||||
}
|
||||
let len_utf8 = c.len_utf8();
|
||||
ix += len_utf8;
|
||||
if c == '\n' {
|
||||
position.row += 1;
|
||||
position.column = 0;
|
||||
} else {
|
||||
position.column += len_utf8 as u32;
|
||||
}
|
||||
}
|
||||
util::test::generate_marked_text(text, &[byte_range], true)
|
||||
fn encode_ranges(text: &str, point_ranges: &Vec<Range<Point>>) -> String {
|
||||
let byte_ranges = point_ranges
|
||||
.into_iter()
|
||||
.map(|range| {
|
||||
let mut byte_range = 0..0;
|
||||
let mut ix = 0;
|
||||
let mut position = Point::zero();
|
||||
for c in text.chars().chain(['\0']) {
|
||||
if position == range.start {
|
||||
byte_range.start = ix;
|
||||
}
|
||||
if position == range.end {
|
||||
byte_range.end = ix;
|
||||
}
|
||||
let len_utf8 = c.len_utf8();
|
||||
ix += len_utf8;
|
||||
if c == '\n' {
|
||||
position.row += 1;
|
||||
position.column = 0;
|
||||
} else {
|
||||
position.column += len_utf8 as u32;
|
||||
}
|
||||
}
|
||||
byte_range
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let ret = util::test::generate_marked_text(text, &byte_ranges[..], true);
|
||||
ret
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue