logiguard fork v2: patched GPUI on verified 8c74db0 tree
Rebuilt from the exact zed-industries/zed @ 8c74db00c4053d6c0a9037863d7bd048658da4ca tree (previous orphan was based on a stale/wrong fetch). Same 3-file patch: - serial.rs: add SerialKind::KeyboardEnter - client.rs: store wl_keyboard.enter serial; add latest_serial_of() - window.rs: activate() uses keyboard-enter serial (Mutter focus gate) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
[package]
|
||||
name = "vim"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/vim.rs"
|
||||
doctest = false
|
||||
|
||||
[features]
|
||||
neovim = ["nvim-rs", "async-compat", "async-trait", "tokio"]
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
async-compat = { workspace = true, "optional" = true }
|
||||
async-trait = { workspace = true, "optional" = true }
|
||||
collections.workspace = true
|
||||
command_palette.workspace = true
|
||||
command_palette_hooks.workspace = true
|
||||
db.workspace = true
|
||||
editor.workspace = true
|
||||
env_logger.workspace = true
|
||||
futures.workspace = true
|
||||
fuzzy.workspace = true
|
||||
gpui.workspace = true
|
||||
itertools.workspace = true
|
||||
language.workspace = true
|
||||
log.workspace = true
|
||||
multi_buffer.workspace = true
|
||||
nvim-rs = { git = "https://github.com/KillTheMule/nvim-rs", rev = "764dd270c642f77f10f3e19d05cc178a6cbe69f3", features = ["use_tokio"], optional = true }
|
||||
picker.workspace = true
|
||||
project.workspace = true
|
||||
regex.workspace = true
|
||||
schemars.workspace = true
|
||||
search.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
task.workspace = true
|
||||
text.workspace = true
|
||||
theme.workspace = true
|
||||
theme_settings.workspace = true
|
||||
menu.workspace = true
|
||||
tokio = { version = "1.15", features = ["full"], optional = true }
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
util_macros.workspace = true
|
||||
vim_mode_setting.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_actions.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
command_palette = { workspace = true, features = ["test-support"] }
|
||||
editor = { workspace = true, features = ["test-support"] }
|
||||
git_ui = { workspace = true, features = ["test-support"] }
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
indoc.workspace = true
|
||||
language = { workspace = true, features = ["test-support"] }
|
||||
project = { workspace = true, features = ["test-support"] }
|
||||
lsp = { workspace = true, features = ["test-support"] }
|
||||
markdown_preview.workspace = true
|
||||
parking_lot.workspace = true
|
||||
project_panel.workspace = true
|
||||
outline_panel.workspace = true
|
||||
release_channel.workspace = true
|
||||
semver.workspace = true
|
||||
settings_ui.workspace = true
|
||||
settings.workspace = true
|
||||
perf.workspace = true
|
||||
util = { workspace = true, features = ["test-support"] }
|
||||
workspace = { workspace = true, features = ["test-support"] }
|
||||
search = { workspace = true, features = ["test-support"] }
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../../LICENSE-GPL
|
||||
@@ -0,0 +1,36 @@
|
||||
This contains the code for Zed's Vim emulation mode.
|
||||
|
||||
Vim mode in Zed is supposed to primarily "do what you expect": it mostly tries to copy vim exactly, but will use Zed-specific functionality when available to make things smoother. This means Zed will never be 100% vim compatible, but should be 100% vim familiar!
|
||||
|
||||
The backlog is maintained in the `#vim` channel notes.
|
||||
|
||||
## Testing against Neovim
|
||||
|
||||
If you are making a change to make Zed's behavior more closely match vim/nvim, you can create a test using the `NeovimBackedTestContext`.
|
||||
|
||||
For example, the following test checks that Zed and Neovim have the same behavior when running `*` in visual mode:
|
||||
|
||||
```rust
|
||||
#[gpui::test]
|
||||
async fn test_visual_star_hash(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_shared_state("ˇa.c. abcd a.c. abcd").await;
|
||||
cx.simulate_shared_keystrokes(["v", "3", "l", "*"]).await;
|
||||
cx.assert_shared_state("a.c. abcd ˇa.c. abcd").await;
|
||||
}
|
||||
```
|
||||
|
||||
To keep CI runs fast, by default the neovim tests use a cached JSON file that records what neovim did (see crates/vim/test_data),
|
||||
but while developing this test you'll need to run it with the neovim flag enabled:
|
||||
|
||||
```sh
|
||||
cargo test -p vim --features neovim test_visual_star_hash
|
||||
```
|
||||
|
||||
This will run your keystrokes against a headless neovim and cache the results in the test_data directory. Note that neovim must be installed and reachable on your $PATH in order to run the feature.
|
||||
|
||||
|
||||
## Testing zed-only behavior
|
||||
|
||||
Zed does more than vim/neovim in their default modes. The `VimTestContext` can be used instead. This lets you test integration with the language server and other parts of zed's UI that don't have a NeoVim equivalent.
|
||||
@@ -0,0 +1,220 @@
|
||||
use editor::{Bias, Direction, Editor, display_map::ToDisplayPoint, movement};
|
||||
use gpui::{Context, Window, actions};
|
||||
|
||||
use crate::{Vim, state::Mode};
|
||||
|
||||
actions!(
|
||||
vim,
|
||||
[
|
||||
/// Navigates to an older position in the change list.
|
||||
ChangeListOlder,
|
||||
/// Navigates to a newer position in the change list.
|
||||
ChangeListNewer
|
||||
]
|
||||
);
|
||||
|
||||
pub(crate) fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
|
||||
Vim::action(editor, cx, |vim, _: &ChangeListOlder, window, cx| {
|
||||
vim.move_to_change(Direction::Prev, window, cx);
|
||||
});
|
||||
Vim::action(editor, cx, |vim, _: &ChangeListNewer, window, cx| {
|
||||
vim.move_to_change(Direction::Next, window, cx);
|
||||
});
|
||||
}
|
||||
|
||||
impl Vim {
|
||||
fn move_to_change(
|
||||
&mut self,
|
||||
direction: Direction,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let count = Vim::take_count(cx).unwrap_or(1);
|
||||
Vim::take_forced_motion(cx);
|
||||
self.update_editor(cx, |_, editor, cx| {
|
||||
if let Some(selections) = editor
|
||||
.change_list
|
||||
.next_change(count, direction)
|
||||
.map(|s| s.to_vec())
|
||||
{
|
||||
editor.change_selections(Default::default(), window, cx, |s| {
|
||||
let map = s.display_snapshot();
|
||||
s.select_display_ranges(selections.iter().map(|a| {
|
||||
let point = a.to_display_point(&map);
|
||||
point..point
|
||||
}))
|
||||
})
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn push_to_change_list(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some((new_positions, buffer)) = self.update_editor(cx, |vim, editor, cx| {
|
||||
let display_map = editor.display_snapshot(cx);
|
||||
let selections = editor.selections.all_adjusted_display(&display_map);
|
||||
let buffer = editor.buffer().clone();
|
||||
|
||||
let pop_state = editor
|
||||
.change_list
|
||||
.last()
|
||||
.map(|previous| {
|
||||
previous.len() == selections.len()
|
||||
&& previous.iter().enumerate().all(|(ix, p)| {
|
||||
p.to_display_point(&display_map).row() == selections[ix].head().row()
|
||||
})
|
||||
})
|
||||
.unwrap_or(false);
|
||||
|
||||
let new_positions = selections
|
||||
.into_iter()
|
||||
.map(|s| {
|
||||
let point = if vim.mode == Mode::Insert {
|
||||
movement::saturating_left(&display_map, s.head())
|
||||
} else {
|
||||
s.head()
|
||||
};
|
||||
display_map.display_point_to_anchor(point, Bias::Left)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
editor
|
||||
.change_list
|
||||
.push_to_change_list(pop_state, new_positions.clone());
|
||||
|
||||
(new_positions, buffer)
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.set_mark(".".to_string(), new_positions, &buffer, window, cx)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use indoc::indoc;
|
||||
|
||||
use crate::{state::Mode, test::NeovimBackedTestContext};
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_change_list_insert(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_shared_state("ˇ").await;
|
||||
|
||||
cx.simulate_shared_keystrokes("i 1 1 escape shift-o 2 2 escape shift-g o 3 3 escape")
|
||||
.await;
|
||||
|
||||
cx.shared_state().await.assert_eq(indoc! {
|
||||
"22
|
||||
11
|
||||
3ˇ3"
|
||||
});
|
||||
|
||||
cx.simulate_shared_keystrokes("g ;").await;
|
||||
// NOTE: this matches nvim when I type it into it
|
||||
// but in tests, nvim always reports the column as 0...
|
||||
cx.assert_state(
|
||||
indoc! {
|
||||
"22
|
||||
11
|
||||
3ˇ3"
|
||||
},
|
||||
Mode::Normal,
|
||||
);
|
||||
cx.simulate_shared_keystrokes("g ;").await;
|
||||
cx.assert_state(
|
||||
indoc! {
|
||||
"2ˇ2
|
||||
11
|
||||
33"
|
||||
},
|
||||
Mode::Normal,
|
||||
);
|
||||
cx.simulate_shared_keystrokes("g ;").await;
|
||||
cx.assert_state(
|
||||
indoc! {
|
||||
"22
|
||||
1ˇ1
|
||||
33"
|
||||
},
|
||||
Mode::Normal,
|
||||
);
|
||||
cx.simulate_shared_keystrokes("g ,").await;
|
||||
cx.assert_state(
|
||||
indoc! {
|
||||
"2ˇ2
|
||||
11
|
||||
33"
|
||||
},
|
||||
Mode::Normal,
|
||||
);
|
||||
cx.simulate_shared_keystrokes("shift-g i 4 4 escape").await;
|
||||
cx.simulate_shared_keystrokes("g ;").await;
|
||||
cx.assert_state(
|
||||
indoc! {
|
||||
"22
|
||||
11
|
||||
34ˇ43"
|
||||
},
|
||||
Mode::Normal,
|
||||
);
|
||||
cx.simulate_shared_keystrokes("g ;").await;
|
||||
cx.assert_state(
|
||||
indoc! {
|
||||
"2ˇ2
|
||||
11
|
||||
3443"
|
||||
},
|
||||
Mode::Normal,
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_change_list_delete(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
cx.set_shared_state(indoc! {
|
||||
"one two
|
||||
three fˇour"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes("x k d i w ^ x").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {
|
||||
"ˇne•
|
||||
three fur"});
|
||||
cx.simulate_shared_keystrokes("2 g ;").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {
|
||||
"ne•
|
||||
three fˇur"});
|
||||
cx.simulate_shared_keystrokes("g ,").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {
|
||||
"ˇne•
|
||||
three fur"});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_gi(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
cx.set_shared_state(indoc! {
|
||||
"one two
|
||||
three fˇr"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes("i o escape k g i").await;
|
||||
cx.simulate_shared_keystrokes("u escape").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {
|
||||
"one two
|
||||
three foˇur"});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_dot_mark(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
cx.set_shared_state(indoc! {
|
||||
"one two
|
||||
three fˇr"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes("i o escape k ` .").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {
|
||||
"one two
|
||||
three fˇor"});
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,365 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use collections::HashMap;
|
||||
use editor::Editor;
|
||||
use gpui::{Action, App, Context, Keystroke, KeystrokeEvent, Window};
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use settings::Settings;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use crate::{Vim, VimSettings, state::Operator};
|
||||
|
||||
mod default;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, JsonSchema, PartialEq, Action)]
|
||||
#[action(namespace = vim)]
|
||||
struct Literal(String, char);
|
||||
|
||||
pub(crate) fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
|
||||
Vim::action(editor, cx, Vim::literal)
|
||||
}
|
||||
|
||||
static DEFAULT_DIGRAPHS_MAP: LazyLock<HashMap<String, Arc<str>>> = LazyLock::new(|| {
|
||||
let mut map = HashMap::default();
|
||||
for &(a, b, c) in default::DEFAULT_DIGRAPHS {
|
||||
let key = format!("{a}{b}");
|
||||
let value = char::from_u32(c).unwrap().to_string().into();
|
||||
map.insert(key, value);
|
||||
}
|
||||
map
|
||||
});
|
||||
|
||||
fn lookup_digraph(a: char, b: char, cx: &App) -> Arc<str> {
|
||||
let custom_digraphs = &VimSettings::get_global(cx).custom_digraphs;
|
||||
let input = format!("{a}{b}");
|
||||
let reversed = format!("{b}{a}");
|
||||
|
||||
custom_digraphs
|
||||
.get(&input)
|
||||
.or_else(|| DEFAULT_DIGRAPHS_MAP.get(&input))
|
||||
.or_else(|| custom_digraphs.get(&reversed))
|
||||
.or_else(|| DEFAULT_DIGRAPHS_MAP.get(&reversed))
|
||||
.cloned()
|
||||
.unwrap_or_else(|| b.to_string().into())
|
||||
}
|
||||
|
||||
impl Vim {
|
||||
pub fn insert_digraph(
|
||||
&mut self,
|
||||
first_char: char,
|
||||
second_char: char,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let text = lookup_digraph(first_char, second_char, cx);
|
||||
|
||||
self.pop_operator(window, cx);
|
||||
if self.editor_input_enabled() {
|
||||
self.update_editor(cx, |_, editor, cx| editor.insert(&text, window, cx));
|
||||
} else {
|
||||
self.input_ignored(text, window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn literal(&mut self, action: &Literal, window: &mut Window, cx: &mut Context<Self>) {
|
||||
match self.active_operator() {
|
||||
Some(Operator::Literal {
|
||||
prefix: Some(prefix),
|
||||
}) => {
|
||||
if let Some(keystroke) = Keystroke::parse(&action.0).ok() {
|
||||
window.defer(cx, |window, cx| {
|
||||
window.dispatch_keystroke(keystroke, cx);
|
||||
});
|
||||
}
|
||||
return self.handle_literal_input(prefix, "", window, cx);
|
||||
}
|
||||
Some(_) => self.insert_literal(Some(action.1), "", window, cx),
|
||||
None => log::error!(
|
||||
"Literal called when no operator was on the stack. This likely means there is an invalid keymap config"
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_literal_keystroke(
|
||||
&mut self,
|
||||
keystroke_event: &KeystrokeEvent,
|
||||
prefix: String,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
// handled by handle_literal_input
|
||||
if keystroke_event.keystroke.key_char.is_some() {
|
||||
return;
|
||||
};
|
||||
|
||||
if !prefix.is_empty() {
|
||||
self.handle_literal_input(prefix, "", window, cx);
|
||||
} else {
|
||||
self.pop_operator(window, cx);
|
||||
}
|
||||
|
||||
// give another chance to handle the binding outside
|
||||
// of waiting mode.
|
||||
if keystroke_event.action.is_none() {
|
||||
let keystroke = keystroke_event.keystroke.clone();
|
||||
window.defer(cx, |window, cx| {
|
||||
window.dispatch_keystroke(keystroke, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_literal_input(
|
||||
&mut self,
|
||||
mut prefix: String,
|
||||
text: &str,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let first = prefix.chars().next();
|
||||
let next = text.chars().next().unwrap_or(' ');
|
||||
match first {
|
||||
Some('o' | 'O') => {
|
||||
if next.is_digit(8) {
|
||||
prefix.push(next);
|
||||
if prefix.len() == 4 {
|
||||
let ch: char = u8::from_str_radix(&prefix[1..], 8).unwrap_or(255).into();
|
||||
return self.insert_literal(Some(ch), "", window, cx);
|
||||
}
|
||||
} else {
|
||||
let ch = if prefix.len() > 1 {
|
||||
Some(u8::from_str_radix(&prefix[1..], 8).unwrap_or(255).into())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
return self.insert_literal(ch, text, window, cx);
|
||||
}
|
||||
}
|
||||
Some('x' | 'X' | 'u' | 'U') => {
|
||||
let max_len = match first.unwrap() {
|
||||
'x' => 3,
|
||||
'X' => 3,
|
||||
'u' => 5,
|
||||
'U' => 9,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
if next.is_ascii_hexdigit() {
|
||||
prefix.push(next);
|
||||
if prefix.len() == max_len {
|
||||
let ch: char = u32::from_str_radix(&prefix[1..], 16)
|
||||
.ok()
|
||||
.and_then(|n| n.try_into().ok())
|
||||
.unwrap_or('\u{FFFD}');
|
||||
return self.insert_literal(Some(ch), "", window, cx);
|
||||
}
|
||||
} else {
|
||||
let ch = if prefix.len() > 1 {
|
||||
Some(
|
||||
u32::from_str_radix(&prefix[1..], 16)
|
||||
.ok()
|
||||
.and_then(|n| n.try_into().ok())
|
||||
.unwrap_or('\u{FFFD}'),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
return self.insert_literal(ch, text, window, cx);
|
||||
}
|
||||
}
|
||||
Some('0'..='9') => {
|
||||
if next.is_ascii_hexdigit() {
|
||||
prefix.push(next);
|
||||
if prefix.len() == 3 {
|
||||
let ch: char = u8::from_str_radix(&prefix, 10).unwrap_or(255).into();
|
||||
return self.insert_literal(Some(ch), "", window, cx);
|
||||
}
|
||||
} else {
|
||||
let ch: char = u8::from_str_radix(&prefix, 10).unwrap_or(255).into();
|
||||
return self.insert_literal(Some(ch), "", window, cx);
|
||||
}
|
||||
}
|
||||
None if matches!(next, 'o' | 'O' | 'x' | 'X' | 'u' | 'U' | '0'..='9') => {
|
||||
prefix.push(next)
|
||||
}
|
||||
_ => {
|
||||
return self.insert_literal(None, text, window, cx);
|
||||
}
|
||||
};
|
||||
|
||||
self.pop_operator(window, cx);
|
||||
self.push_operator(
|
||||
Operator::Literal {
|
||||
prefix: Some(prefix),
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
fn insert_literal(
|
||||
&mut self,
|
||||
ch: Option<char>,
|
||||
suffix: &str,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.pop_operator(window, cx);
|
||||
let mut text = String::new();
|
||||
if let Some(c) = ch {
|
||||
if c == '\n' {
|
||||
text.push('\x00')
|
||||
} else {
|
||||
text.push(c)
|
||||
}
|
||||
}
|
||||
text.push_str(suffix);
|
||||
|
||||
if self.editor_input_enabled() {
|
||||
self.update_editor(cx, |_, editor, cx| editor.insert(&text, window, cx));
|
||||
} else {
|
||||
self.input_ignored(text.into(), window, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use collections::HashMap;
|
||||
use settings::SettingsStore;
|
||||
|
||||
use crate::{
|
||||
state::Mode,
|
||||
test::{NeovimBackedTestContext, VimTestContext},
|
||||
};
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_digraph_insert_mode(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_shared_state("Hellˇo").await;
|
||||
cx.simulate_shared_keystrokes("a ctrl-k o : escape").await;
|
||||
cx.shared_state().await.assert_eq("Helloˇö");
|
||||
|
||||
cx.set_shared_state("Hellˇo").await;
|
||||
cx.simulate_shared_keystrokes("a ctrl-k : o escape").await;
|
||||
cx.shared_state().await.assert_eq("Helloˇö");
|
||||
|
||||
cx.set_shared_state("Hellˇo").await;
|
||||
cx.simulate_shared_keystrokes("i ctrl-k o : escape").await;
|
||||
cx.shared_state().await.assert_eq("Hellˇöo");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_digraph_insert_multicursor(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx: VimTestContext = VimTestContext::new(cx, true).await;
|
||||
|
||||
cx.set_state("Hellˇo wˇorld", Mode::Normal);
|
||||
cx.simulate_keystrokes("a ctrl-k o : escape");
|
||||
cx.assert_state("Helloˇö woˇörld", Mode::Normal);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_digraph_replace(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_shared_state("Hellˇo").await;
|
||||
cx.simulate_shared_keystrokes("r ctrl-k o :").await;
|
||||
cx.shared_state().await.assert_eq("Hellˇö");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_digraph_find(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_shared_state("ˇHellö world").await;
|
||||
cx.simulate_shared_keystrokes("f ctrl-k o :").await;
|
||||
cx.shared_state().await.assert_eq("Hellˇö world");
|
||||
|
||||
cx.set_shared_state("ˇHellö world").await;
|
||||
cx.simulate_shared_keystrokes("t ctrl-k o :").await;
|
||||
cx.shared_state().await.assert_eq("Helˇlö world");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_digraph_replace_mode(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_shared_state("ˇHello").await;
|
||||
cx.simulate_shared_keystrokes(
|
||||
"shift-r ctrl-k a ' ctrl-k e ` ctrl-k i : ctrl-k o ~ ctrl-k u - escape",
|
||||
)
|
||||
.await;
|
||||
cx.shared_state().await.assert_eq("áèïõˇū");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_digraph_custom(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx: VimTestContext = VimTestContext::new(cx, true).await;
|
||||
|
||||
cx.update_global(|store: &mut SettingsStore, cx| {
|
||||
store.update_user_settings(cx, |s| {
|
||||
let mut custom_digraphs = HashMap::default();
|
||||
custom_digraphs.insert("|-".into(), "⊢".into());
|
||||
custom_digraphs.insert(":)".into(), "👨💻".into());
|
||||
s.vim.get_or_insert_default().custom_digraphs = Some(custom_digraphs);
|
||||
});
|
||||
});
|
||||
|
||||
cx.set_state("ˇ", Mode::Normal);
|
||||
cx.simulate_keystrokes("a ctrl-k | - escape");
|
||||
cx.assert_state("ˇ⊢", Mode::Normal);
|
||||
|
||||
// Test support for multi-codepoint mappings
|
||||
cx.set_state("ˇ", Mode::Normal);
|
||||
cx.simulate_keystrokes("a ctrl-k : ) escape");
|
||||
cx.assert_state("ˇ👨💻", Mode::Normal);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_digraph_keymap_conflict(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_shared_state("Hellˇo").await;
|
||||
cx.simulate_shared_keystrokes("a ctrl-k s , escape").await;
|
||||
cx.shared_state().await.assert_eq("Helloˇş");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_ctrl_v(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_shared_state("ˇ").await;
|
||||
cx.simulate_shared_keystrokes("i ctrl-v 0 0 0").await;
|
||||
cx.shared_state().await.assert_eq("\x00ˇ");
|
||||
|
||||
cx.simulate_shared_keystrokes("ctrl-v j").await;
|
||||
cx.shared_state().await.assert_eq("\x00jˇ");
|
||||
cx.simulate_shared_keystrokes("ctrl-v x 6 5").await;
|
||||
cx.shared_state().await.assert_eq("\x00jeˇ");
|
||||
cx.simulate_shared_keystrokes("ctrl-v U 1 F 6 4 0 space")
|
||||
.await;
|
||||
cx.shared_state().await.assert_eq("\x00je🙀 ˇ");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_ctrl_v_escape(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await;
|
||||
cx.set_shared_state("ˇ").await;
|
||||
cx.simulate_shared_keystrokes("i ctrl-v 9 escape").await;
|
||||
cx.shared_state().await.assert_eq("ˇ\t");
|
||||
cx.simulate_shared_keystrokes("i ctrl-v escape").await;
|
||||
cx.shared_state().await.assert_eq("\x1bˇ\t");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_ctrl_v_control(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await;
|
||||
cx.set_shared_state("ˇ").await;
|
||||
cx.simulate_shared_keystrokes("i ctrl-v ctrl-d").await;
|
||||
cx.shared_state().await.assert_eq("\x04ˇ");
|
||||
cx.simulate_shared_keystrokes("ctrl-v ctrl-j").await;
|
||||
cx.shared_state().await.assert_eq("\x04\x00ˇ");
|
||||
cx.simulate_shared_keystrokes("ctrl-v tab").await;
|
||||
cx.shared_state().await.assert_eq("\x04\x00\x09ˇ");
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,741 @@
|
||||
use std::{cmp::Ordering, ops::Range};
|
||||
|
||||
use editor::{
|
||||
DisplayPoint, MultiBufferOffset,
|
||||
display_map::{DisplaySnapshot, ToDisplayPoint},
|
||||
movement,
|
||||
};
|
||||
use language::{CharClassifier, CharKind};
|
||||
use text::Bias;
|
||||
|
||||
use crate::helix::object::HelixTextObject;
|
||||
|
||||
/// Text objects (after helix definition) that can easily be
|
||||
/// found by reading a buffer and comparing two neighboring chars
|
||||
/// until a start / end is found
|
||||
trait BoundedObject {
|
||||
/// The next start since `from` (inclusive).
|
||||
/// If outer is true it is the start of "a" object (m a) rather than "inner" object (m i).
|
||||
fn next_start(&self, map: &DisplaySnapshot, from: Offset, outer: bool) -> Option<Offset>;
|
||||
/// The next end since `from` (inclusive).
|
||||
/// If outer is true it is the end of "a" object (m a) rather than "inner" object (m i).
|
||||
fn next_end(&self, map: &DisplaySnapshot, from: Offset, outer: bool) -> Option<Offset>;
|
||||
/// The previous start since `from` (inclusive).
|
||||
/// If outer is true it is the start of "a" object (m a) rather than "inner" object (m i).
|
||||
fn previous_start(&self, map: &DisplaySnapshot, from: Offset, outer: bool) -> Option<Offset>;
|
||||
/// The previous end since `from` (inclusive).
|
||||
/// If outer is true it is the end of "a" object (m a) rather than "inner" object (m i).
|
||||
fn previous_end(&self, map: &DisplaySnapshot, from: Offset, outer: bool) -> Option<Offset>;
|
||||
|
||||
/// Whether the range inside the object can be zero characters wide.
|
||||
/// If so, the trait assumes that these ranges can't be directly adjacent to each other.
|
||||
fn inner_range_can_be_zero_width(&self) -> bool;
|
||||
/// Whether the "ma" can exceed the "mi" range on both sides at the same time
|
||||
fn surround_on_both_sides(&self) -> bool;
|
||||
/// Whether the outer range of an object could overlap with the outer range of the neighboring
|
||||
/// object. If so, they can't be nested.
|
||||
fn ambiguous_outer(&self) -> bool;
|
||||
|
||||
fn can_be_zero_width(&self, around: bool) -> bool {
|
||||
if around {
|
||||
false
|
||||
} else {
|
||||
self.inner_range_can_be_zero_width()
|
||||
}
|
||||
}
|
||||
|
||||
/// Switches from an "mi" range to an "ma" one.
|
||||
/// Assumes the inner range is valid.
|
||||
fn around(&self, map: &DisplaySnapshot, inner_range: Range<Offset>) -> Range<Offset> {
|
||||
if self.surround_on_both_sides() {
|
||||
let start = self
|
||||
.previous_start(map, inner_range.start, true)
|
||||
.unwrap_or(inner_range.start);
|
||||
let end = self
|
||||
.next_end(map, inner_range.end, true)
|
||||
.unwrap_or(inner_range.end);
|
||||
|
||||
return start..end;
|
||||
}
|
||||
|
||||
let mut start = inner_range.start;
|
||||
let end = self
|
||||
.next_end(map, inner_range.end, true)
|
||||
.unwrap_or(inner_range.end);
|
||||
if end == inner_range.end {
|
||||
start = self
|
||||
.previous_start(map, inner_range.start, true)
|
||||
.unwrap_or(inner_range.start)
|
||||
}
|
||||
|
||||
start..end
|
||||
}
|
||||
/// Switches from an "ma" range to an "mi" one.
|
||||
/// Assumes the inner range is valid.
|
||||
fn inside(&self, map: &DisplaySnapshot, outer_range: Range<Offset>) -> Range<Offset> {
|
||||
let inner_start = self
|
||||
.next_start(map, outer_range.start, false)
|
||||
.unwrap_or_else(|| {
|
||||
log::warn!("The motion might not have found the text object correctly");
|
||||
outer_range.start
|
||||
});
|
||||
let inner_end = self
|
||||
.previous_end(map, outer_range.end, false)
|
||||
.unwrap_or_else(|| {
|
||||
log::warn!("The motion might not have found the text object correctly");
|
||||
outer_range.end
|
||||
});
|
||||
inner_start..inner_end
|
||||
}
|
||||
|
||||
/// The next end since `start` (inclusive) on the same nesting level.
|
||||
fn close_at_end(&self, start: Offset, map: &DisplaySnapshot, outer: bool) -> Option<Offset> {
|
||||
let mut end_search_start = if self.can_be_zero_width(outer) {
|
||||
start
|
||||
} else {
|
||||
start.next(map)?
|
||||
};
|
||||
let mut start_search_start = start.next(map)?;
|
||||
|
||||
loop {
|
||||
let next_end = self.next_end(map, end_search_start, outer)?;
|
||||
let maybe_next_start = self.next_start(map, start_search_start, outer);
|
||||
if let Some(next_start) = maybe_next_start
|
||||
&& (next_start.0 < next_end.0
|
||||
|| next_start.0 == next_end.0 && self.can_be_zero_width(outer))
|
||||
&& !self.ambiguous_outer()
|
||||
{
|
||||
let closing = self.close_at_end(next_start, map, outer)?;
|
||||
end_search_start = closing.next(map)?;
|
||||
start_search_start = if self.can_be_zero_width(outer) {
|
||||
closing.next(map)?
|
||||
} else {
|
||||
closing
|
||||
};
|
||||
} else {
|
||||
return Some(next_end);
|
||||
}
|
||||
}
|
||||
}
|
||||
/// The previous start since `end` (inclusive) on the same nesting level.
|
||||
fn close_at_start(&self, end: Offset, map: &DisplaySnapshot, outer: bool) -> Option<Offset> {
|
||||
let mut start_search_end = if self.can_be_zero_width(outer) {
|
||||
end
|
||||
} else {
|
||||
end.previous(map)?
|
||||
};
|
||||
let mut end_search_end = end.previous(map)?;
|
||||
|
||||
loop {
|
||||
let previous_start = self.previous_start(map, start_search_end, outer)?;
|
||||
let maybe_previous_end = self.previous_end(map, end_search_end, outer);
|
||||
if let Some(previous_end) = maybe_previous_end
|
||||
&& (previous_end.0 > previous_start.0
|
||||
|| previous_end.0 == previous_start.0 && self.can_be_zero_width(outer))
|
||||
&& !self.ambiguous_outer()
|
||||
{
|
||||
let closing = self.close_at_start(previous_end, map, outer)?;
|
||||
start_search_end = closing.previous(map)?;
|
||||
end_search_end = if self.can_be_zero_width(outer) {
|
||||
closing.previous(map)?
|
||||
} else {
|
||||
closing
|
||||
};
|
||||
} else {
|
||||
return Some(previous_start);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Debug, PartialOrd, Ord, Eq)]
|
||||
struct Offset(MultiBufferOffset);
|
||||
impl Offset {
|
||||
fn next(self, map: &DisplaySnapshot) -> Option<Self> {
|
||||
let next = Self(
|
||||
map.buffer_snapshot()
|
||||
.clip_offset(self.0 + 1usize, Bias::Right),
|
||||
);
|
||||
(next.0 > self.0).then(|| next)
|
||||
}
|
||||
fn previous(self, map: &DisplaySnapshot) -> Option<Self> {
|
||||
if self.0 == MultiBufferOffset(0) {
|
||||
return None;
|
||||
}
|
||||
Some(Self(
|
||||
map.buffer_snapshot().clip_offset(self.0 - 1, Bias::Left),
|
||||
))
|
||||
}
|
||||
fn range(
|
||||
start: (DisplayPoint, Bias),
|
||||
end: (DisplayPoint, Bias),
|
||||
map: &DisplaySnapshot,
|
||||
) -> Range<Self> {
|
||||
Self(start.0.to_offset(map, start.1))..Self(end.0.to_offset(map, end.1))
|
||||
}
|
||||
}
|
||||
|
||||
impl<B: BoundedObject> HelixTextObject for B {
|
||||
fn range(
|
||||
&self,
|
||||
map: &DisplaySnapshot,
|
||||
relative_to: Range<DisplayPoint>,
|
||||
around: bool,
|
||||
) -> Option<Range<DisplayPoint>> {
|
||||
let relative_to = Offset::range(
|
||||
(relative_to.start, Bias::Left),
|
||||
(relative_to.end, Bias::Left),
|
||||
map,
|
||||
);
|
||||
|
||||
relative_range(self, around, map, |find_outer| {
|
||||
let search_start = if self.can_be_zero_width(find_outer) {
|
||||
relative_to.end
|
||||
} else {
|
||||
// If the objects can be directly next to each other an object end the
|
||||
// cursor (relative_to) end would not count for close_at_end, so the search
|
||||
// needs to start one character to the left.
|
||||
relative_to.end.previous(map)?
|
||||
};
|
||||
let max_end = self.close_at_end(search_start, map, find_outer)?;
|
||||
let min_start = self.close_at_start(max_end, map, find_outer)?;
|
||||
|
||||
(min_start <= relative_to.start).then(|| min_start..max_end)
|
||||
})
|
||||
}
|
||||
|
||||
fn next_range(
|
||||
&self,
|
||||
map: &DisplaySnapshot,
|
||||
relative_to: Range<DisplayPoint>,
|
||||
around: bool,
|
||||
) -> Option<Range<DisplayPoint>> {
|
||||
let relative_to = Offset::range(
|
||||
(relative_to.start, Bias::Left),
|
||||
(relative_to.end, Bias::Left),
|
||||
map,
|
||||
);
|
||||
|
||||
relative_range(self, around, map, |find_outer| {
|
||||
let min_start = self.next_start(map, relative_to.end, find_outer)?;
|
||||
let max_end = self.close_at_end(min_start, map, find_outer)?;
|
||||
|
||||
Some(min_start..max_end)
|
||||
})
|
||||
}
|
||||
|
||||
fn previous_range(
|
||||
&self,
|
||||
map: &DisplaySnapshot,
|
||||
relative_to: Range<DisplayPoint>,
|
||||
around: bool,
|
||||
) -> Option<Range<DisplayPoint>> {
|
||||
let relative_to = Offset::range(
|
||||
(relative_to.start, Bias::Left),
|
||||
(relative_to.end, Bias::Left),
|
||||
map,
|
||||
);
|
||||
|
||||
relative_range(self, around, map, |find_outer| {
|
||||
let max_end = self.previous_end(map, relative_to.start, find_outer)?;
|
||||
let min_start = self.close_at_start(max_end, map, find_outer)?;
|
||||
|
||||
Some(min_start..max_end)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn relative_range<B: BoundedObject>(
|
||||
object: &B,
|
||||
outer: bool,
|
||||
map: &DisplaySnapshot,
|
||||
find_range: impl Fn(bool) -> Option<Range<Offset>>,
|
||||
) -> Option<Range<DisplayPoint>> {
|
||||
// The cursor could be inside the outer range, but not the inner range.
|
||||
// Whether that should count as found.
|
||||
let find_outer = object.surround_on_both_sides() && !object.ambiguous_outer();
|
||||
let range = find_range(find_outer)?;
|
||||
let min_start = range.start;
|
||||
let max_end = range.end;
|
||||
|
||||
let wanted_range = if outer && !find_outer {
|
||||
// max_end is not yet the outer end
|
||||
object.around(map, min_start..max_end)
|
||||
} else if !outer && find_outer {
|
||||
// max_end is the outer end, but the final result should have the inner end
|
||||
object.inside(map, min_start..max_end)
|
||||
} else {
|
||||
min_start..max_end
|
||||
};
|
||||
|
||||
let start = wanted_range.start.0.to_display_point(map);
|
||||
let end = wanted_range.end.0.to_display_point(map);
|
||||
|
||||
Some(start..end)
|
||||
}
|
||||
|
||||
/// A textobject whose boundaries can easily be found between two chars
|
||||
pub enum ImmediateBoundary {
|
||||
Word { ignore_punctuation: bool },
|
||||
Subword { ignore_punctuation: bool },
|
||||
AngleBrackets,
|
||||
BackQuotes,
|
||||
CurlyBrackets,
|
||||
DoubleQuotes,
|
||||
Parentheses,
|
||||
SingleQuotes,
|
||||
SquareBrackets,
|
||||
VerticalBars,
|
||||
}
|
||||
|
||||
/// A textobject whose start and end can be found from an easy-to-find
|
||||
/// boundary between two chars by following a simple path from there
|
||||
pub enum FuzzyBoundary {
|
||||
Sentence,
|
||||
Paragraph,
|
||||
}
|
||||
|
||||
impl ImmediateBoundary {
|
||||
fn is_inner_start(&self, left: char, right: char, classifier: CharClassifier) -> bool {
|
||||
match self {
|
||||
Self::Word { ignore_punctuation } => {
|
||||
let classifier = classifier.ignore_punctuation(*ignore_punctuation);
|
||||
is_word_start(left, right, &classifier)
|
||||
|| (is_buffer_start(left) && classifier.kind(right) != CharKind::Whitespace)
|
||||
}
|
||||
Self::Subword { ignore_punctuation } => {
|
||||
let classifier = classifier.ignore_punctuation(*ignore_punctuation);
|
||||
movement::is_subword_start(left, right, &classifier)
|
||||
|| (is_buffer_start(left) && classifier.kind(right) != CharKind::Whitespace)
|
||||
}
|
||||
Self::AngleBrackets => left == '<',
|
||||
Self::BackQuotes => left == '`',
|
||||
Self::CurlyBrackets => left == '{',
|
||||
Self::DoubleQuotes => left == '"',
|
||||
Self::Parentheses => left == '(',
|
||||
Self::SingleQuotes => left == '\'',
|
||||
Self::SquareBrackets => left == '[',
|
||||
Self::VerticalBars => left == '|',
|
||||
}
|
||||
}
|
||||
fn is_inner_end(&self, left: char, right: char, classifier: CharClassifier) -> bool {
|
||||
match self {
|
||||
Self::Word { ignore_punctuation } => {
|
||||
let classifier = classifier.ignore_punctuation(*ignore_punctuation);
|
||||
is_word_end(left, right, &classifier)
|
||||
|| (is_buffer_end(right) && classifier.kind(left) != CharKind::Whitespace)
|
||||
}
|
||||
Self::Subword { ignore_punctuation } => {
|
||||
let classifier = classifier.ignore_punctuation(*ignore_punctuation);
|
||||
movement::is_subword_start(left, right, &classifier)
|
||||
|| (is_buffer_end(right) && classifier.kind(left) != CharKind::Whitespace)
|
||||
}
|
||||
Self::AngleBrackets => right == '>',
|
||||
Self::BackQuotes => right == '`',
|
||||
Self::CurlyBrackets => right == '}',
|
||||
Self::DoubleQuotes => right == '"',
|
||||
Self::Parentheses => right == ')',
|
||||
Self::SingleQuotes => right == '\'',
|
||||
Self::SquareBrackets => right == ']',
|
||||
Self::VerticalBars => right == '|',
|
||||
}
|
||||
}
|
||||
fn is_outer_start(&self, left: char, right: char, classifier: CharClassifier) -> bool {
|
||||
match self {
|
||||
word @ Self::Word { .. } => word.is_inner_end(left, right, classifier) || left == '\n',
|
||||
subword @ Self::Subword { .. } => {
|
||||
subword.is_inner_end(left, right, classifier) || left == '\n'
|
||||
}
|
||||
Self::AngleBrackets => right == '<',
|
||||
Self::BackQuotes => right == '`',
|
||||
Self::CurlyBrackets => right == '{',
|
||||
Self::DoubleQuotes => right == '"',
|
||||
Self::Parentheses => right == '(',
|
||||
Self::SingleQuotes => right == '\'',
|
||||
Self::SquareBrackets => right == '[',
|
||||
Self::VerticalBars => right == '|',
|
||||
}
|
||||
}
|
||||
fn is_outer_end(&self, left: char, right: char, classifier: CharClassifier) -> bool {
|
||||
match self {
|
||||
word @ Self::Word { .. } => {
|
||||
word.is_inner_start(left, right, classifier) || right == '\n'
|
||||
}
|
||||
subword @ Self::Subword { .. } => {
|
||||
subword.is_inner_start(left, right, classifier) || right == '\n'
|
||||
}
|
||||
Self::AngleBrackets => left == '>',
|
||||
Self::BackQuotes => left == '`',
|
||||
Self::CurlyBrackets => left == '}',
|
||||
Self::DoubleQuotes => left == '"',
|
||||
Self::Parentheses => left == ')',
|
||||
Self::SingleQuotes => left == '\'',
|
||||
Self::SquareBrackets => left == ']',
|
||||
Self::VerticalBars => left == '|',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BoundedObject for ImmediateBoundary {
|
||||
fn next_start(&self, map: &DisplaySnapshot, from: Offset, outer: bool) -> Option<Offset> {
|
||||
try_find_boundary(map, from, &|left, right| {
|
||||
let classifier = map.buffer_snapshot().char_classifier_at(from.0);
|
||||
if outer {
|
||||
self.is_outer_start(left, right, classifier)
|
||||
} else {
|
||||
self.is_inner_start(left, right, classifier)
|
||||
}
|
||||
})
|
||||
}
|
||||
fn next_end(&self, map: &DisplaySnapshot, from: Offset, outer: bool) -> Option<Offset> {
|
||||
try_find_boundary(map, from, &|left, right| {
|
||||
let classifier = map.buffer_snapshot().char_classifier_at(from.0);
|
||||
if outer {
|
||||
self.is_outer_end(left, right, classifier)
|
||||
} else {
|
||||
self.is_inner_end(left, right, classifier)
|
||||
}
|
||||
})
|
||||
}
|
||||
fn previous_start(&self, map: &DisplaySnapshot, from: Offset, outer: bool) -> Option<Offset> {
|
||||
try_find_preceding_boundary(map, from, &|left, right| {
|
||||
let classifier = map.buffer_snapshot().char_classifier_at(from.0);
|
||||
if outer {
|
||||
self.is_outer_start(left, right, classifier)
|
||||
} else {
|
||||
self.is_inner_start(left, right, classifier)
|
||||
}
|
||||
})
|
||||
}
|
||||
fn previous_end(&self, map: &DisplaySnapshot, from: Offset, outer: bool) -> Option<Offset> {
|
||||
try_find_preceding_boundary(map, from, &|left, right| {
|
||||
let classifier = map.buffer_snapshot().char_classifier_at(from.0);
|
||||
if outer {
|
||||
self.is_outer_end(left, right, classifier)
|
||||
} else {
|
||||
self.is_inner_end(left, right, classifier)
|
||||
}
|
||||
})
|
||||
}
|
||||
fn inner_range_can_be_zero_width(&self) -> bool {
|
||||
match self {
|
||||
Self::Subword { .. } | Self::Word { .. } => false,
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
fn surround_on_both_sides(&self) -> bool {
|
||||
match self {
|
||||
Self::Subword { .. } | Self::Word { .. } => false,
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
fn ambiguous_outer(&self) -> bool {
|
||||
match self {
|
||||
Self::BackQuotes
|
||||
| Self::DoubleQuotes
|
||||
| Self::SingleQuotes
|
||||
| Self::VerticalBars
|
||||
| Self::Subword { .. }
|
||||
| Self::Word { .. } => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FuzzyBoundary {
|
||||
/// When between two chars that form an easy-to-find identifier boundary,
|
||||
/// what's the way to get to the actual start of the object, if any
|
||||
fn is_near_potential_inner_start<'a>(
|
||||
&self,
|
||||
left: char,
|
||||
right: char,
|
||||
classifier: &CharClassifier,
|
||||
) -> Option<Box<dyn Fn(Offset, &'a DisplaySnapshot) -> Option<Offset>>> {
|
||||
if is_buffer_start(left) {
|
||||
return Some(Box::new(|identifier, _| Some(identifier)));
|
||||
}
|
||||
match self {
|
||||
Self::Paragraph => {
|
||||
if left != '\n' || right != '\n' {
|
||||
return None;
|
||||
}
|
||||
Some(Box::new(|identifier, map| {
|
||||
try_find_boundary(map, identifier, &|left, right| {
|
||||
left == '\n' && right != '\n'
|
||||
})
|
||||
}))
|
||||
}
|
||||
Self::Sentence => {
|
||||
if let Some(find_paragraph_start) =
|
||||
Self::Paragraph.is_near_potential_inner_start(left, right, classifier)
|
||||
{
|
||||
return Some(find_paragraph_start);
|
||||
} else if !is_sentence_end(left, right, classifier) {
|
||||
return None;
|
||||
}
|
||||
Some(Box::new(|identifier, map| {
|
||||
let word = ImmediateBoundary::Word {
|
||||
ignore_punctuation: false,
|
||||
};
|
||||
word.next_start(map, identifier, false)
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
/// When between two chars that form an easy-to-find identifier boundary,
|
||||
/// what's the way to get to the actual end of the object, if any
|
||||
fn is_near_potential_inner_end<'a>(
|
||||
&self,
|
||||
left: char,
|
||||
right: char,
|
||||
classifier: &CharClassifier,
|
||||
) -> Option<Box<dyn Fn(Offset, &'a DisplaySnapshot) -> Option<Offset>>> {
|
||||
if is_buffer_end(right) {
|
||||
return Some(Box::new(|identifier, _| Some(identifier)));
|
||||
}
|
||||
match self {
|
||||
Self::Paragraph => {
|
||||
if left != '\n' || right != '\n' {
|
||||
return None;
|
||||
}
|
||||
Some(Box::new(|identifier, map| {
|
||||
try_find_preceding_boundary(map, identifier, &|left, right| {
|
||||
left != '\n' && right == '\n'
|
||||
})
|
||||
}))
|
||||
}
|
||||
Self::Sentence => {
|
||||
if let Some(find_paragraph_end) =
|
||||
Self::Paragraph.is_near_potential_inner_end(left, right, classifier)
|
||||
{
|
||||
return Some(find_paragraph_end);
|
||||
} else if !is_sentence_end(left, right, classifier) {
|
||||
return None;
|
||||
}
|
||||
Some(Box::new(|identifier, _| Some(identifier)))
|
||||
}
|
||||
}
|
||||
}
|
||||
/// When between two chars that form an easy-to-find identifier boundary,
|
||||
/// what's the way to get to the actual end of the object, if any
|
||||
fn is_near_potential_outer_start<'a>(
|
||||
&self,
|
||||
left: char,
|
||||
right: char,
|
||||
classifier: &CharClassifier,
|
||||
) -> Option<Box<dyn Fn(Offset, &'a DisplaySnapshot) -> Option<Offset>>> {
|
||||
match self {
|
||||
paragraph @ Self::Paragraph => {
|
||||
paragraph.is_near_potential_inner_end(left, right, classifier)
|
||||
}
|
||||
sentence @ Self::Sentence => {
|
||||
sentence.is_near_potential_inner_end(left, right, classifier)
|
||||
}
|
||||
}
|
||||
}
|
||||
/// When between two chars that form an easy-to-find identifier boundary,
|
||||
/// what's the way to get to the actual end of the object, if any
|
||||
fn is_near_potential_outer_end<'a>(
|
||||
&self,
|
||||
left: char,
|
||||
right: char,
|
||||
classifier: &CharClassifier,
|
||||
) -> Option<Box<dyn Fn(Offset, &'a DisplaySnapshot) -> Option<Offset>>> {
|
||||
match self {
|
||||
paragraph @ Self::Paragraph => {
|
||||
paragraph.is_near_potential_inner_start(left, right, classifier)
|
||||
}
|
||||
sentence @ Self::Sentence => {
|
||||
sentence.is_near_potential_inner_start(left, right, classifier)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The boundary can be on the other side of `from` than the identifier, so the search needs to go both ways.
|
||||
// Also, the distance (and direction) between identifier and boundary could vary, so a few ones need to be
|
||||
// compared, even if one boundary was already found on the right side of `from`.
|
||||
fn to_boundary(
|
||||
&self,
|
||||
map: &DisplaySnapshot,
|
||||
from: Offset,
|
||||
outer: bool,
|
||||
backward: bool,
|
||||
boundary_kind: Boundary,
|
||||
) -> Option<Offset> {
|
||||
let generate_boundary_data = |left, right, point: Offset| {
|
||||
let classifier = map.buffer_snapshot().char_classifier_at(from.0);
|
||||
let reach_boundary = if outer && boundary_kind == Boundary::Start {
|
||||
self.is_near_potential_outer_start(left, right, &classifier)
|
||||
} else if !outer && boundary_kind == Boundary::Start {
|
||||
self.is_near_potential_inner_start(left, right, &classifier)
|
||||
} else if outer && boundary_kind == Boundary::End {
|
||||
self.is_near_potential_outer_end(left, right, &classifier)
|
||||
} else {
|
||||
self.is_near_potential_inner_end(left, right, &classifier)
|
||||
};
|
||||
|
||||
reach_boundary.map(|reach_start| (point, reach_start))
|
||||
};
|
||||
|
||||
let forwards = try_find_boundary_data(map, from, generate_boundary_data);
|
||||
let backwards = try_find_preceding_boundary_data(map, from, generate_boundary_data);
|
||||
let boundaries = [forwards, backwards]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter_map(|(identifier, reach_boundary)| reach_boundary(identifier, map))
|
||||
.filter(|boundary| match boundary.cmp(&from) {
|
||||
Ordering::Equal => true,
|
||||
Ordering::Less => backward,
|
||||
Ordering::Greater => !backward,
|
||||
});
|
||||
if backward {
|
||||
boundaries.max_by_key(|boundary| *boundary)
|
||||
} else {
|
||||
boundaries.min_by_key(|boundary| *boundary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
enum Boundary {
|
||||
Start,
|
||||
End,
|
||||
}
|
||||
|
||||
impl BoundedObject for FuzzyBoundary {
|
||||
fn next_start(&self, map: &DisplaySnapshot, from: Offset, outer: bool) -> Option<Offset> {
|
||||
self.to_boundary(map, from, outer, false, Boundary::Start)
|
||||
}
|
||||
fn next_end(&self, map: &DisplaySnapshot, from: Offset, outer: bool) -> Option<Offset> {
|
||||
self.to_boundary(map, from, outer, false, Boundary::End)
|
||||
}
|
||||
fn previous_start(&self, map: &DisplaySnapshot, from: Offset, outer: bool) -> Option<Offset> {
|
||||
self.to_boundary(map, from, outer, true, Boundary::Start)
|
||||
}
|
||||
fn previous_end(&self, map: &DisplaySnapshot, from: Offset, outer: bool) -> Option<Offset> {
|
||||
self.to_boundary(map, from, outer, true, Boundary::End)
|
||||
}
|
||||
fn inner_range_can_be_zero_width(&self) -> bool {
|
||||
false
|
||||
}
|
||||
fn surround_on_both_sides(&self) -> bool {
|
||||
false
|
||||
}
|
||||
fn ambiguous_outer(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the first boundary after or at `from` in text direction.
|
||||
/// The start and end of the file are the chars `'\0'`.
|
||||
fn try_find_boundary(
|
||||
map: &DisplaySnapshot,
|
||||
from: Offset,
|
||||
is_boundary: &dyn Fn(char, char) -> bool,
|
||||
) -> Option<Offset> {
|
||||
let boundary = try_find_boundary_data(map, from, |left, right, point| {
|
||||
if is_boundary(left, right) {
|
||||
Some(point)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})?;
|
||||
Some(boundary)
|
||||
}
|
||||
|
||||
/// Returns some information about it (of type `T`) as soon as
|
||||
/// there is a boundary after or at `from` in text direction
|
||||
/// The start and end of the file are the chars `'\0'`.
|
||||
fn try_find_boundary_data<T>(
|
||||
map: &DisplaySnapshot,
|
||||
mut from: Offset,
|
||||
boundary_information: impl Fn(char, char, Offset) -> Option<T>,
|
||||
) -> Option<T> {
|
||||
let mut prev_ch = map
|
||||
.buffer_snapshot()
|
||||
.reversed_chars_at(from.0)
|
||||
.next()
|
||||
.unwrap_or('\0');
|
||||
|
||||
for ch in map.buffer_snapshot().chars_at(from.0).chain(['\0']) {
|
||||
if let Some(boundary_information) = boundary_information(prev_ch, ch, from) {
|
||||
return Some(boundary_information);
|
||||
}
|
||||
from.0 += ch.len_utf8();
|
||||
prev_ch = ch;
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Returns the first boundary after or at `from` in text direction.
|
||||
/// The start and end of the file are the chars `'\0'`.
|
||||
fn try_find_preceding_boundary(
|
||||
map: &DisplaySnapshot,
|
||||
from: Offset,
|
||||
is_boundary: &dyn Fn(char, char) -> bool,
|
||||
) -> Option<Offset> {
|
||||
let boundary = try_find_preceding_boundary_data(map, from, |left, right, point| {
|
||||
if is_boundary(left, right) {
|
||||
Some(point)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})?;
|
||||
Some(boundary)
|
||||
}
|
||||
|
||||
/// Returns some information about it (of type `T`) as soon as
|
||||
/// there is a boundary before or at `from` in opposite text direction
|
||||
/// The start and end of the file are the chars `'\0'`.
|
||||
fn try_find_preceding_boundary_data<T>(
|
||||
map: &DisplaySnapshot,
|
||||
mut from: Offset,
|
||||
is_boundary: impl Fn(char, char, Offset) -> Option<T>,
|
||||
) -> Option<T> {
|
||||
let mut prev_ch = map
|
||||
.buffer_snapshot()
|
||||
.chars_at(from.0)
|
||||
.next()
|
||||
.unwrap_or('\0');
|
||||
|
||||
for ch in map
|
||||
.buffer_snapshot()
|
||||
.reversed_chars_at(from.0)
|
||||
.chain(['\0'])
|
||||
{
|
||||
if let Some(boundary_information) = is_boundary(ch, prev_ch, from) {
|
||||
return Some(boundary_information);
|
||||
}
|
||||
from.0.0 = from.0.0.saturating_sub(ch.len_utf8());
|
||||
prev_ch = ch;
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn is_buffer_start(left: char) -> bool {
|
||||
left == '\0'
|
||||
}
|
||||
|
||||
fn is_buffer_end(right: char) -> bool {
|
||||
right == '\0'
|
||||
}
|
||||
|
||||
fn is_word_start(left: char, right: char, classifier: &CharClassifier) -> bool {
|
||||
classifier.kind(left) != classifier.kind(right)
|
||||
&& classifier.kind(right) != CharKind::Whitespace
|
||||
}
|
||||
|
||||
fn is_word_end(left: char, right: char, classifier: &CharClassifier) -> bool {
|
||||
classifier.kind(left) != classifier.kind(right) && classifier.kind(left) != CharKind::Whitespace
|
||||
}
|
||||
|
||||
fn is_sentence_end(left: char, right: char, classifier: &CharClassifier) -> bool {
|
||||
const ENDS: [char; 1] = ['.'];
|
||||
|
||||
if classifier.kind(right) != CharKind::Whitespace {
|
||||
return false;
|
||||
}
|
||||
ENDS.into_iter().any(|end| left == end)
|
||||
}
|
||||
@@ -0,0 +1,313 @@
|
||||
use std::ops::Range;
|
||||
|
||||
use editor::{DisplayPoint, MultiBufferOffset, display_map::DisplaySnapshot};
|
||||
use gpui::Context;
|
||||
use language::PointUtf16;
|
||||
use multi_buffer::MultiBufferRow;
|
||||
use text::Bias;
|
||||
use ui::Window;
|
||||
|
||||
use crate::Vim;
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
enum Direction {
|
||||
Above,
|
||||
Below,
|
||||
}
|
||||
|
||||
impl Vim {
|
||||
/// Creates a duplicate of every selection below it in the first place that has both its start
|
||||
/// and end
|
||||
pub(super) fn helix_duplicate_selections_below(
|
||||
&mut self,
|
||||
times: Option<usize>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.duplicate_selections(times, window, cx, Direction::Below);
|
||||
}
|
||||
|
||||
/// Creates a duplicate of every selection above it in the first place that has both its start
|
||||
/// and end
|
||||
pub(super) fn helix_duplicate_selections_above(
|
||||
&mut self,
|
||||
times: Option<usize>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.duplicate_selections(times, window, cx, Direction::Above);
|
||||
}
|
||||
|
||||
fn duplicate_selections(
|
||||
&mut self,
|
||||
times: Option<usize>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
direction: Direction,
|
||||
) {
|
||||
let times = times.unwrap_or(1);
|
||||
self.update_editor(cx, |_, editor, cx| {
|
||||
let mut selections = Vec::new();
|
||||
let map = editor.display_snapshot(cx);
|
||||
let mut original_selections = editor.selections.all_display(&map);
|
||||
// The order matters, because it is recorded when the selections are added.
|
||||
if matches!(direction, Direction::Above) {
|
||||
original_selections.reverse();
|
||||
}
|
||||
|
||||
for origin in original_selections {
|
||||
let origin = origin.tail()..origin.head();
|
||||
selections.push(display_point_range_to_offset_range(&origin, &map));
|
||||
let mut last_origin = origin;
|
||||
for _ in 1..=times {
|
||||
if let Some(duplicate) =
|
||||
find_next_valid_duplicate_space(last_origin.clone(), &map, direction)
|
||||
{
|
||||
selections.push(display_point_range_to_offset_range(&duplicate, &map));
|
||||
last_origin = duplicate;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
editor.change_selections(Default::default(), window, cx, |s| {
|
||||
s.select_ranges(selections);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn find_next_valid_duplicate_space(
|
||||
origin: Range<DisplayPoint>,
|
||||
map: &DisplaySnapshot,
|
||||
direction: Direction,
|
||||
) -> Option<Range<DisplayPoint>> {
|
||||
let buffer = map.buffer_snapshot();
|
||||
let start_col_utf16 = buffer
|
||||
.point_to_point_utf16(origin.start.to_point(map))
|
||||
.column;
|
||||
let end_col_utf16 = buffer.point_to_point_utf16(origin.end.to_point(map)).column;
|
||||
|
||||
let mut candidate = origin;
|
||||
loop {
|
||||
match direction {
|
||||
Direction::Below => {
|
||||
if candidate.end.row() >= map.max_point().row() {
|
||||
return None;
|
||||
}
|
||||
*candidate.start.row_mut() += 1;
|
||||
*candidate.end.row_mut() += 1;
|
||||
}
|
||||
Direction::Above => {
|
||||
if candidate.start.row() == DisplayPoint::zero().row() {
|
||||
return None;
|
||||
}
|
||||
*candidate.start.row_mut() = candidate.start.row().0.saturating_sub(1);
|
||||
*candidate.end.row_mut() = candidate.end.row().0.saturating_sub(1);
|
||||
}
|
||||
}
|
||||
|
||||
let start_row = DisplayPoint::new(candidate.start.row(), 0)
|
||||
.to_point(map)
|
||||
.row;
|
||||
let end_row = DisplayPoint::new(candidate.end.row(), 0).to_point(map).row;
|
||||
|
||||
if start_col_utf16 > buffer.line_len_utf16(MultiBufferRow(start_row))
|
||||
|| end_col_utf16 > buffer.line_len_utf16(MultiBufferRow(end_row))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let start_col = buffer
|
||||
.point_utf16_to_point(PointUtf16::new(start_row, start_col_utf16))
|
||||
.column;
|
||||
let end_col = buffer
|
||||
.point_utf16_to_point(PointUtf16::new(end_row, end_col_utf16))
|
||||
.column;
|
||||
|
||||
let candidate_start = DisplayPoint::new(candidate.start.row(), start_col);
|
||||
let candidate_end = DisplayPoint::new(candidate.end.row(), end_col);
|
||||
|
||||
if map.clip_point(candidate_start, Bias::Left) == candidate_start
|
||||
&& map.clip_point(candidate_end, Bias::Right) == candidate_end
|
||||
{
|
||||
return Some(candidate_start..candidate_end);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn display_point_range_to_offset_range(
|
||||
range: &Range<DisplayPoint>,
|
||||
map: &DisplaySnapshot,
|
||||
) -> Range<MultiBufferOffset> {
|
||||
range.start.to_offset(map, Bias::Left)..range.end.to_offset(map, Bias::Right)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use db::indoc;
|
||||
|
||||
use crate::{state::Mode, test::VimTestContext};
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_selection_duplication(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
cx.enable_helix();
|
||||
|
||||
cx.set_state(
|
||||
indoc! {"
|
||||
The quick brown
|
||||
fox «jumpsˇ»
|
||||
over the
|
||||
lazy dog."},
|
||||
Mode::HelixNormal,
|
||||
);
|
||||
|
||||
cx.simulate_keystrokes("C");
|
||||
|
||||
cx.assert_state(
|
||||
indoc! {"
|
||||
The quick brown
|
||||
fox «jumpsˇ»
|
||||
over the
|
||||
lazy« dog.ˇ»"},
|
||||
Mode::HelixNormal,
|
||||
);
|
||||
|
||||
cx.simulate_keystrokes("C");
|
||||
|
||||
cx.assert_state(
|
||||
indoc! {"
|
||||
The quick brown
|
||||
fox «jumpsˇ»
|
||||
over the
|
||||
lazy« dog.ˇ»"},
|
||||
Mode::HelixNormal,
|
||||
);
|
||||
|
||||
cx.simulate_keystrokes("alt-C");
|
||||
|
||||
cx.assert_state(
|
||||
indoc! {"
|
||||
The «quickˇ» brown
|
||||
fox «jumpsˇ»
|
||||
over the
|
||||
lazy« dog.ˇ»"},
|
||||
Mode::HelixNormal,
|
||||
);
|
||||
|
||||
cx.simulate_keystrokes(",");
|
||||
|
||||
cx.assert_state(
|
||||
indoc! {"
|
||||
The «quickˇ» brown
|
||||
fox jumps
|
||||
over the
|
||||
lazy dog."},
|
||||
Mode::HelixNormal,
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_selection_duplication_backwards(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
cx.enable_helix();
|
||||
|
||||
cx.set_state(
|
||||
indoc! {"
|
||||
The quick brown
|
||||
«ˇfox» jumps
|
||||
over the
|
||||
lazy dog."},
|
||||
Mode::HelixNormal,
|
||||
);
|
||||
|
||||
cx.simulate_keystrokes("C C alt-C");
|
||||
|
||||
cx.assert_state(
|
||||
indoc! {"
|
||||
«ˇThe» quick brown
|
||||
«ˇfox» jumps
|
||||
«ˇove»r the
|
||||
«ˇlaz»y dog."},
|
||||
Mode::HelixNormal,
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_selection_duplication_count(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
cx.enable_helix();
|
||||
|
||||
cx.set_state(
|
||||
indoc! {"
|
||||
The «qˇ»uick brown
|
||||
fox jumps
|
||||
over the
|
||||
lazy dog."},
|
||||
Mode::HelixNormal,
|
||||
);
|
||||
|
||||
cx.simulate_keystrokes("9 C");
|
||||
|
||||
cx.assert_state(
|
||||
indoc! {"
|
||||
The «qˇ»uick brown
|
||||
fox «jˇ»umps
|
||||
over« ˇ»the
|
||||
lazy« ˇ»dog."},
|
||||
Mode::HelixNormal,
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_selection_duplication_multiline_multibyte(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
cx.enable_helix();
|
||||
|
||||
// Multiline selection on rows with multibyte chars should preserve
|
||||
// the visual column on both start and end rows.
|
||||
cx.set_state(
|
||||
indoc! {"
|
||||
«H䡻llo
|
||||
Hëllo
|
||||
Hallo"},
|
||||
Mode::HelixNormal,
|
||||
);
|
||||
|
||||
cx.simulate_keystrokes("C");
|
||||
|
||||
cx.assert_state(
|
||||
indoc! {"
|
||||
«H䡻llo
|
||||
«H롻llo
|
||||
Hallo"},
|
||||
Mode::HelixNormal,
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_selection_duplication_multibyte(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
cx.enable_helix();
|
||||
|
||||
// Selection on a line with multibyte chars should duplicate to the
|
||||
// same character column on the next line, not skip it.
|
||||
cx.set_state(
|
||||
indoc! {"
|
||||
H«äˇ»llo
|
||||
Hallo"},
|
||||
Mode::HelixNormal,
|
||||
);
|
||||
|
||||
cx.simulate_keystrokes("C");
|
||||
|
||||
cx.assert_state(
|
||||
indoc! {"
|
||||
H«äˇ»llo
|
||||
H«aˇ»llo"},
|
||||
Mode::HelixNormal,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
use std::{
|
||||
error::Error,
|
||||
fmt::{self, Display},
|
||||
ops::Range,
|
||||
};
|
||||
|
||||
use editor::{DisplayPoint, display_map::DisplaySnapshot, movement};
|
||||
use text::Selection;
|
||||
|
||||
use crate::{
|
||||
helix::boundary::{FuzzyBoundary, ImmediateBoundary},
|
||||
object::Object as VimObject,
|
||||
};
|
||||
|
||||
/// A text object from helix or an extra one
|
||||
pub trait HelixTextObject {
|
||||
fn range(
|
||||
&self,
|
||||
map: &DisplaySnapshot,
|
||||
relative_to: Range<DisplayPoint>,
|
||||
around: bool,
|
||||
) -> Option<Range<DisplayPoint>>;
|
||||
|
||||
fn next_range(
|
||||
&self,
|
||||
map: &DisplaySnapshot,
|
||||
relative_to: Range<DisplayPoint>,
|
||||
around: bool,
|
||||
) -> Option<Range<DisplayPoint>>;
|
||||
|
||||
fn previous_range(
|
||||
&self,
|
||||
map: &DisplaySnapshot,
|
||||
relative_to: Range<DisplayPoint>,
|
||||
around: bool,
|
||||
) -> Option<Range<DisplayPoint>>;
|
||||
}
|
||||
|
||||
impl VimObject {
|
||||
/// Returns the range of the object the cursor is over.
|
||||
/// Follows helix convention.
|
||||
pub fn helix_range(
|
||||
self,
|
||||
map: &DisplaySnapshot,
|
||||
selection: Selection<DisplayPoint>,
|
||||
around: bool,
|
||||
) -> Result<Option<Range<DisplayPoint>>, VimToHelixError> {
|
||||
let cursor = cursor_range(&selection, map);
|
||||
if let Some(helix_object) = self.to_helix_object() {
|
||||
Ok(helix_object.range(map, cursor, around))
|
||||
} else {
|
||||
Err(VimToHelixError)
|
||||
}
|
||||
}
|
||||
/// Returns the range of the next object the cursor is not over.
|
||||
/// Follows helix convention.
|
||||
pub fn helix_next_range(
|
||||
self,
|
||||
map: &DisplaySnapshot,
|
||||
selection: Selection<DisplayPoint>,
|
||||
around: bool,
|
||||
) -> Result<Option<Range<DisplayPoint>>, VimToHelixError> {
|
||||
let cursor = cursor_range(&selection, map);
|
||||
if let Some(helix_object) = self.to_helix_object() {
|
||||
Ok(helix_object.next_range(map, cursor, around))
|
||||
} else {
|
||||
Err(VimToHelixError)
|
||||
}
|
||||
}
|
||||
/// Returns the range of the previous object the cursor is not over.
|
||||
/// Follows helix convention.
|
||||
pub fn helix_previous_range(
|
||||
self,
|
||||
map: &DisplaySnapshot,
|
||||
selection: Selection<DisplayPoint>,
|
||||
around: bool,
|
||||
) -> Result<Option<Range<DisplayPoint>>, VimToHelixError> {
|
||||
let cursor = cursor_range(&selection, map);
|
||||
if let Some(helix_object) = self.to_helix_object() {
|
||||
Ok(helix_object.previous_range(map, cursor, around))
|
||||
} else {
|
||||
Err(VimToHelixError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct VimToHelixError;
|
||||
impl Display for VimToHelixError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"Not all vim text objects have an implemented helix equivalent"
|
||||
)
|
||||
}
|
||||
}
|
||||
impl Error for VimToHelixError {}
|
||||
|
||||
impl VimObject {
|
||||
fn to_helix_object(self) -> Option<Box<dyn HelixTextObject>> {
|
||||
Some(match self {
|
||||
Self::AngleBrackets => Box::new(ImmediateBoundary::AngleBrackets),
|
||||
Self::BackQuotes => Box::new(ImmediateBoundary::BackQuotes),
|
||||
Self::CurlyBrackets => Box::new(ImmediateBoundary::CurlyBrackets),
|
||||
Self::DoubleQuotes => Box::new(ImmediateBoundary::DoubleQuotes),
|
||||
Self::Paragraph => Box::new(FuzzyBoundary::Paragraph),
|
||||
Self::Parentheses => Box::new(ImmediateBoundary::Parentheses),
|
||||
Self::Quotes => Box::new(ImmediateBoundary::SingleQuotes),
|
||||
Self::Sentence => Box::new(FuzzyBoundary::Sentence),
|
||||
Self::SquareBrackets => Box::new(ImmediateBoundary::SquareBrackets),
|
||||
Self::Subword { ignore_punctuation } => {
|
||||
Box::new(ImmediateBoundary::Subword { ignore_punctuation })
|
||||
}
|
||||
Self::VerticalBars => Box::new(ImmediateBoundary::VerticalBars),
|
||||
Self::Word { ignore_punctuation } => {
|
||||
Box::new(ImmediateBoundary::Word { ignore_punctuation })
|
||||
}
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the start of the cursor of a selection, whether that is collapsed or not.
|
||||
pub(crate) fn cursor_range(
|
||||
selection: &Selection<DisplayPoint>,
|
||||
map: &DisplaySnapshot,
|
||||
) -> Range<DisplayPoint> {
|
||||
if selection.is_empty() | selection.reversed {
|
||||
selection.head()..movement::right(map, selection.head())
|
||||
} else {
|
||||
movement::left(map, selection.head())..selection.head()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use db::indoc;
|
||||
|
||||
use crate::{state::Mode, test::VimTestContext};
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_select_word_object(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
let start = indoc! {"
|
||||
The quick brˇowˇnˇ
|
||||
fox «ˇjumps» ov«er
|
||||
the laˇ»zy dogˇ
|
||||
|
||||
"
|
||||
};
|
||||
|
||||
cx.set_state(start, Mode::HelixNormal);
|
||||
|
||||
cx.simulate_keystrokes("m i w");
|
||||
|
||||
cx.assert_state(
|
||||
indoc! {"
|
||||
The quick «brownˇ»
|
||||
fox «jumpsˇ» over
|
||||
the «lazyˇ» dogˇ
|
||||
|
||||
"
|
||||
},
|
||||
Mode::HelixNormal,
|
||||
);
|
||||
|
||||
cx.set_state(start, Mode::HelixNormal);
|
||||
|
||||
cx.simulate_keystrokes("m a w");
|
||||
|
||||
cx.assert_state(
|
||||
indoc! {"
|
||||
The quick« brownˇ»
|
||||
fox «jumps ˇ»over
|
||||
the «lazy ˇ»dogˇ
|
||||
|
||||
"
|
||||
},
|
||||
Mode::HelixNormal,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,529 @@
|
||||
use editor::{ToOffset, movement};
|
||||
use gpui::{Action, Context, Window};
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::{Vim, state::Mode};
|
||||
|
||||
/// Pastes text from the specified register at the cursor position.
|
||||
#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
|
||||
#[action(namespace = vim)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct HelixPaste {
|
||||
#[serde(default)]
|
||||
before: bool,
|
||||
}
|
||||
|
||||
impl Vim {
|
||||
pub fn helix_paste(
|
||||
&mut self,
|
||||
action: &HelixPaste,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.record_current_action(cx);
|
||||
self.store_visual_marks(window, cx);
|
||||
let count = Vim::take_count(cx).unwrap_or(1);
|
||||
// TODO: vim paste calls take_forced_motion here, but I don't know what that does
|
||||
// (none of the other helix_ methods call it)
|
||||
|
||||
self.update_editor(cx, |vim, editor, cx| {
|
||||
if editor.read_only(cx) {
|
||||
return;
|
||||
}
|
||||
|
||||
editor.transact(window, cx, |editor, window, cx| {
|
||||
editor.set_clip_at_line_ends(false, cx);
|
||||
|
||||
let selected_register = vim.selected_register.take();
|
||||
|
||||
let Some(register) = Vim::update_globals(cx, |globals, cx| {
|
||||
globals.read_register(selected_register, Some(editor), cx)
|
||||
})
|
||||
.filter(|reg| !reg.text.is_empty()) else {
|
||||
return;
|
||||
};
|
||||
let text = register.text;
|
||||
let clipboard_selections = register.clipboard_selections;
|
||||
|
||||
let display_map = editor.display_snapshot(cx);
|
||||
let current_selections = editor.selections.all_adjusted_display(&display_map);
|
||||
|
||||
// The clipboard can have multiple selections, and there can
|
||||
// be multiple selections. Helix zips them together, so the first
|
||||
// clipboard entry gets pasted at the first selection, the second
|
||||
// entry gets pasted at the second selection, and so on. If there
|
||||
// are more clipboard selections than selections, the extra ones
|
||||
// don't get pasted anywhere. If there are more selections than
|
||||
// clipboard selections, the last clipboard selection gets
|
||||
// pasted at all remaining selections.
|
||||
|
||||
let mut edits = Vec::new();
|
||||
let mut new_selections = Vec::new();
|
||||
let mut start_offset = 0;
|
||||
|
||||
let mut replacement_texts: Vec<String> = Vec::new();
|
||||
|
||||
for ix in 0..current_selections.len() {
|
||||
let to_insert = if let Some(clip_sel) =
|
||||
clipboard_selections.as_ref().and_then(|s| s.get(ix))
|
||||
{
|
||||
let end_offset = start_offset + clip_sel.len;
|
||||
let text = text[start_offset..end_offset].to_string();
|
||||
start_offset = if clip_sel.is_entire_line {
|
||||
end_offset
|
||||
} else {
|
||||
end_offset + 1
|
||||
};
|
||||
text
|
||||
} else if let Some(last_text) = replacement_texts.last() {
|
||||
// We have more current selections than clipboard selections: repeat the last one.
|
||||
last_text.to_owned()
|
||||
} else {
|
||||
text.to_string()
|
||||
};
|
||||
replacement_texts.push(to_insert);
|
||||
}
|
||||
|
||||
let line_mode = replacement_texts.iter().any(|text| text.ends_with('\n'));
|
||||
|
||||
for (to_insert, sel) in replacement_texts.into_iter().zip(current_selections) {
|
||||
// Helix doesn't care about the head/tail of the selection.
|
||||
// Pasting before means pasting before the whole selection.
|
||||
let display_point = if line_mode {
|
||||
if action.before {
|
||||
movement::line_beginning(&display_map, sel.start, false)
|
||||
} else {
|
||||
if sel.start == sel.end {
|
||||
movement::right(
|
||||
&display_map,
|
||||
movement::line_end(&display_map, sel.end, false),
|
||||
)
|
||||
} else {
|
||||
sel.end
|
||||
}
|
||||
}
|
||||
} else if action.before {
|
||||
sel.start
|
||||
} else if sel.start == sel.end {
|
||||
// In Helix, a single-point cursor is "on top" of a
|
||||
// character, and pasting after means after that character.
|
||||
// At line end this means the next line. But on an empty
|
||||
// line there is no character, so paste at the cursor.
|
||||
let right = movement::right(&display_map, sel.end);
|
||||
if right.row() != sel.end.row() && sel.end.column() == 0 {
|
||||
sel.end
|
||||
} else {
|
||||
right
|
||||
}
|
||||
} else {
|
||||
sel.end
|
||||
};
|
||||
let point = display_point.to_point(&display_map);
|
||||
let anchor = if action.before {
|
||||
display_map.buffer_snapshot().anchor_after(point)
|
||||
} else {
|
||||
display_map.buffer_snapshot().anchor_before(point)
|
||||
};
|
||||
edits.push((point..point, to_insert.repeat(count)));
|
||||
new_selections.push((anchor, to_insert.len() * count));
|
||||
}
|
||||
|
||||
editor.edit(edits, cx);
|
||||
|
||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||
editor.change_selections(Default::default(), window, cx, |s| {
|
||||
s.select_ranges(new_selections.into_iter().map(|(anchor, len)| {
|
||||
let offset = anchor.to_offset(&snapshot);
|
||||
if action.before {
|
||||
offset.saturating_sub_usize(len)..offset
|
||||
} else {
|
||||
offset..(offset + len)
|
||||
}
|
||||
}));
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
self.switch_mode(Mode::HelixNormal, true, window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use indoc::indoc;
|
||||
|
||||
use gpui::ClipboardItem;
|
||||
|
||||
use crate::{state::Mode, test::VimTestContext};
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_system_clipboard_paste(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
cx.enable_helix();
|
||||
cx.set_state(
|
||||
indoc! {"
|
||||
The quiˇck brown
|
||||
fox jumps over
|
||||
the lazy dog."},
|
||||
Mode::HelixNormal,
|
||||
);
|
||||
|
||||
cx.write_to_clipboard(ClipboardItem::new_string("clipboard".to_string()));
|
||||
cx.simulate_keystrokes("p");
|
||||
cx.assert_state(
|
||||
indoc! {"
|
||||
The quic«clipboardˇ»k brown
|
||||
fox jumps over
|
||||
the lazy dog."},
|
||||
Mode::HelixNormal,
|
||||
);
|
||||
|
||||
// Multiple cursors with system clipboard (no metadata) pastes
|
||||
// the same text at each cursor.
|
||||
cx.set_state(
|
||||
indoc! {"
|
||||
ˇThe quick brown
|
||||
fox ˇjumps over
|
||||
the lazy dog."},
|
||||
Mode::HelixNormal,
|
||||
);
|
||||
cx.write_to_clipboard(ClipboardItem::new_string("hi".to_string()));
|
||||
cx.simulate_keystrokes("p");
|
||||
cx.assert_state(
|
||||
indoc! {"
|
||||
T«hiˇ»he quick brown
|
||||
fox j«hiˇ»umps over
|
||||
the lazy dog."},
|
||||
Mode::HelixNormal,
|
||||
);
|
||||
|
||||
// Multiple cursors on empty lines should paste on those same lines.
|
||||
cx.set_state("ˇ\nˇ\nˇ\nend", Mode::HelixNormal);
|
||||
cx.write_to_clipboard(ClipboardItem::new_string("X".to_string()));
|
||||
cx.simulate_keystrokes("p");
|
||||
cx.assert_state("«Xˇ»\n«Xˇ»\n«Xˇ»\nend", Mode::HelixNormal);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_read_only_paste(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
cx.enable_helix();
|
||||
cx.set_state("aˇb", Mode::HelixNormal);
|
||||
cx.write_to_clipboard(ClipboardItem::new_string("clipboard".to_string()));
|
||||
cx.update_editor(|editor, _window, _cx| editor.set_read_only(true));
|
||||
|
||||
cx.simulate_keystrokes("p");
|
||||
|
||||
cx.assert_state("aˇb", Mode::HelixNormal);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_paste(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
cx.enable_helix();
|
||||
cx.set_state(
|
||||
indoc! {"
|
||||
The «quiˇ»ck brown
|
||||
fox jumps over
|
||||
the lazy dog."},
|
||||
Mode::HelixNormal,
|
||||
);
|
||||
|
||||
cx.simulate_keystrokes("y w p");
|
||||
|
||||
cx.assert_state(
|
||||
indoc! {"
|
||||
The quick «quiˇ»brown
|
||||
fox jumps over
|
||||
the lazy dog."},
|
||||
Mode::HelixNormal,
|
||||
);
|
||||
|
||||
// Pasting before the selection:
|
||||
cx.set_state(
|
||||
indoc! {"
|
||||
The quick brown
|
||||
fox «jumpsˇ» over
|
||||
the lazy dog."},
|
||||
Mode::HelixNormal,
|
||||
);
|
||||
cx.simulate_keystrokes("shift-p");
|
||||
cx.assert_state(
|
||||
indoc! {"
|
||||
The quick brown
|
||||
fox «quiˇ»jumps over
|
||||
the lazy dog."},
|
||||
Mode::HelixNormal,
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_point_selection_paste(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
cx.enable_helix();
|
||||
cx.set_state(
|
||||
indoc! {"
|
||||
The quiˇck brown
|
||||
fox jumps over
|
||||
the lazy dog."},
|
||||
Mode::HelixNormal,
|
||||
);
|
||||
|
||||
cx.simulate_keystrokes("y");
|
||||
|
||||
// Pasting before the selection:
|
||||
cx.set_state(
|
||||
indoc! {"
|
||||
The quick brown
|
||||
fox jumpsˇ over
|
||||
the lazy dog."},
|
||||
Mode::HelixNormal,
|
||||
);
|
||||
cx.simulate_keystrokes("shift-p");
|
||||
cx.assert_state(
|
||||
indoc! {"
|
||||
The quick brown
|
||||
fox jumps«cˇ» over
|
||||
the lazy dog."},
|
||||
Mode::HelixNormal,
|
||||
);
|
||||
|
||||
// Pasting after the selection:
|
||||
cx.set_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 «cˇ»over
|
||||
the lazy dog."},
|
||||
Mode::HelixNormal,
|
||||
);
|
||||
|
||||
// Pasting after the selection at the end of a line:
|
||||
cx.set_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
|
||||
«cˇ»the lazy dog."},
|
||||
Mode::HelixNormal,
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_multi_cursor_paste(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
cx.enable_helix();
|
||||
// Select two blocks of text.
|
||||
cx.set_state(
|
||||
indoc! {"
|
||||
The «quiˇ»ck brown
|
||||
fox ju«mpsˇ» over
|
||||
the lazy dog."},
|
||||
Mode::HelixNormal,
|
||||
);
|
||||
cx.simulate_keystrokes("y");
|
||||
|
||||
// Only one cursor: only the first block gets pasted.
|
||||
cx.set_state(
|
||||
indoc! {"
|
||||
ˇThe quick brown
|
||||
fox jumps over
|
||||
the lazy dog."},
|
||||
Mode::HelixNormal,
|
||||
);
|
||||
cx.simulate_keystrokes("shift-p");
|
||||
cx.assert_state(
|
||||
indoc! {"
|
||||
«quiˇ»The quick brown
|
||||
fox jumps over
|
||||
the lazy dog."},
|
||||
Mode::HelixNormal,
|
||||
);
|
||||
|
||||
// Two cursors: both get pasted.
|
||||
cx.set_state(
|
||||
indoc! {"
|
||||
ˇThe ˇquick brown
|
||||
fox jumps over
|
||||
the lazy dog."},
|
||||
Mode::HelixNormal,
|
||||
);
|
||||
cx.simulate_keystrokes("shift-p");
|
||||
cx.assert_state(
|
||||
indoc! {"
|
||||
«quiˇ»The «mpsˇ»quick brown
|
||||
fox jumps over
|
||||
the lazy dog."},
|
||||
Mode::HelixNormal,
|
||||
);
|
||||
|
||||
// Three cursors: the second yanked block is duplicated.
|
||||
cx.set_state(
|
||||
indoc! {"
|
||||
ˇThe ˇquick brown
|
||||
fox jumpsˇ over
|
||||
the lazy dog."},
|
||||
Mode::HelixNormal,
|
||||
);
|
||||
cx.simulate_keystrokes("shift-p");
|
||||
cx.assert_state(
|
||||
indoc! {"
|
||||
«quiˇ»The «mpsˇ»quick brown
|
||||
fox jumps«mpsˇ» over
|
||||
the lazy dog."},
|
||||
Mode::HelixNormal,
|
||||
);
|
||||
|
||||
// Again with three cursors. All three should be pasted twice.
|
||||
cx.set_state(
|
||||
indoc! {"
|
||||
ˇThe ˇquick brown
|
||||
fox jumpsˇ over
|
||||
the lazy dog."},
|
||||
Mode::HelixNormal,
|
||||
);
|
||||
cx.simulate_keystrokes("2 shift-p");
|
||||
cx.assert_state(
|
||||
indoc! {"
|
||||
«quiquiˇ»The «mpsmpsˇ»quick brown
|
||||
fox jumps«mpsmpsˇ» over
|
||||
the lazy dog."},
|
||||
Mode::HelixNormal,
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_line_mode_paste(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
cx.enable_helix();
|
||||
cx.set_state(
|
||||
indoc! {"
|
||||
The quick brow«n
|
||||
ˇ»fox jumps over
|
||||
the lazy dog."},
|
||||
Mode::HelixNormal,
|
||||
);
|
||||
|
||||
cx.simulate_keystrokes("y shift-p");
|
||||
|
||||
cx.assert_state(
|
||||
indoc! {"
|
||||
«n
|
||||
ˇ»The quick brown
|
||||
fox jumps over
|
||||
the lazy dog."},
|
||||
Mode::HelixNormal,
|
||||
);
|
||||
|
||||
// In line mode, if we're in the middle of a line then pasting before pastes on
|
||||
// the line before.
|
||||
cx.set_state(
|
||||
indoc! {"
|
||||
The quick brown
|
||||
fox jumpsˇ over
|
||||
the lazy dog."},
|
||||
Mode::HelixNormal,
|
||||
);
|
||||
cx.simulate_keystrokes("shift-p");
|
||||
cx.assert_state(
|
||||
indoc! {"
|
||||
The quick brown
|
||||
«n
|
||||
ˇ»fox jumps over
|
||||
the lazy dog."},
|
||||
Mode::HelixNormal,
|
||||
);
|
||||
|
||||
// In line mode, if we're in the middle of a line then pasting after pastes on
|
||||
// the line after.
|
||||
cx.set_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
|
||||
«n
|
||||
ˇ»the lazy dog."},
|
||||
Mode::HelixNormal,
|
||||
);
|
||||
|
||||
// If we're currently at the end of a line, "the line after"
|
||||
// means right after the cursor.
|
||||
cx.set_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
|
||||
«n
|
||||
ˇ»the lazy dog."},
|
||||
Mode::HelixNormal,
|
||||
);
|
||||
|
||||
cx.set_state(
|
||||
indoc! {"
|
||||
|
||||
The quick brown
|
||||
fox jumps overˇ
|
||||
the lazy dog."},
|
||||
Mode::HelixNormal,
|
||||
);
|
||||
cx.simulate_keystrokes("x y up up p");
|
||||
cx.assert_state(
|
||||
indoc! {"
|
||||
|
||||
«fox jumps over
|
||||
ˇ»The quick brown
|
||||
fox jumps over
|
||||
the lazy dog."},
|
||||
Mode::HelixNormal,
|
||||
);
|
||||
|
||||
cx.set_state(
|
||||
indoc! {"
|
||||
«The quick brown
|
||||
fox jumps over
|
||||
ˇ»the lazy dog."},
|
||||
Mode::HelixNormal,
|
||||
);
|
||||
cx.simulate_keystrokes("y p p");
|
||||
cx.assert_state(
|
||||
indoc! {"
|
||||
The quick brown
|
||||
fox jumps over
|
||||
The quick brown
|
||||
fox jumps over
|
||||
«The quick brown
|
||||
fox jumps over
|
||||
ˇ»the lazy dog."},
|
||||
Mode::HelixNormal,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
use text::SelectionGoal;
|
||||
use ui::{Context, Window};
|
||||
|
||||
use crate::{Vim, helix::object::cursor_range, object::Object};
|
||||
|
||||
impl Vim {
|
||||
/// Selects the object each cursor is over.
|
||||
/// Follows helix convention.
|
||||
pub fn select_current_object(
|
||||
&mut self,
|
||||
object: Object,
|
||||
around: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.stop_recording(cx);
|
||||
self.update_editor(cx, |_, editor, cx| {
|
||||
editor.change_selections(Default::default(), window, cx, |s| {
|
||||
s.move_with(&mut |map, selection| {
|
||||
let Some(range) = object
|
||||
.helix_range(map, selection.clone(), around)
|
||||
.unwrap_or({
|
||||
let vim_range = object.range(map, selection.clone(), around, None);
|
||||
vim_range.filter(|r| r.start <= cursor_range(selection, map).start)
|
||||
})
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
selection.set_head_tail(range.end, range.start, SelectionGoal::None);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// Selects the next object from each cursor which the cursor is not over.
|
||||
/// Follows helix convention.
|
||||
pub fn select_next_object(
|
||||
&mut self,
|
||||
object: Object,
|
||||
around: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.stop_recording(cx);
|
||||
self.update_editor(cx, |_, editor, cx| {
|
||||
editor.change_selections(Default::default(), window, cx, |s| {
|
||||
s.move_with(&mut |map, selection| {
|
||||
let Ok(Some(range)) = object.helix_next_range(map, selection.clone(), around)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
selection.set_head_tail(range.end, range.start, SelectionGoal::None);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// Selects the previous object from each cursor which the cursor is not over.
|
||||
/// Follows helix convention.
|
||||
pub fn select_previous_object(
|
||||
&mut self,
|
||||
object: Object,
|
||||
around: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.stop_recording(cx);
|
||||
self.update_editor(cx, |_, editor, cx| {
|
||||
editor.change_selections(Default::default(), window, cx, |s| {
|
||||
s.move_with(&mut |map, selection| {
|
||||
let Ok(Some(range)) =
|
||||
object.helix_previous_range(map, selection.clone(), around)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
selection.set_head_tail(range.start, range.end, SelectionGoal::None);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,492 @@
|
||||
use editor::display_map::DisplaySnapshot;
|
||||
use editor::{Bias, DisplayPoint, MultiBufferOffset};
|
||||
use gpui::{Context, Window};
|
||||
use multi_buffer::Anchor;
|
||||
use text::Selection;
|
||||
|
||||
use crate::Vim;
|
||||
use crate::object::surrounding_markers;
|
||||
use crate::surrounds::{SURROUND_PAIRS, bracket_pair_for_str_helix, surround_pair_for_char_helix};
|
||||
|
||||
/// Find the nearest surrounding bracket pair around the cursor.
|
||||
fn find_nearest_surrounding_pair(
|
||||
display_map: &DisplaySnapshot,
|
||||
cursor: DisplayPoint,
|
||||
) -> Option<(char, char)> {
|
||||
let cursor_offset = cursor.to_offset(display_map, Bias::Left);
|
||||
let mut best_pair: Option<(char, char)> = None;
|
||||
let mut min_range_size = usize::MAX;
|
||||
|
||||
for pair in SURROUND_PAIRS {
|
||||
if let Some(range) =
|
||||
surrounding_markers(display_map, cursor, true, true, pair.open, pair.close)
|
||||
{
|
||||
let start_offset = range.start.to_offset(display_map, Bias::Left);
|
||||
let end_offset = range.end.to_offset(display_map, Bias::Right);
|
||||
|
||||
if cursor_offset >= start_offset && cursor_offset <= end_offset {
|
||||
let size = end_offset - start_offset;
|
||||
if size < min_range_size {
|
||||
min_range_size = size;
|
||||
best_pair = Some((pair.open, pair.close));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
best_pair
|
||||
}
|
||||
|
||||
fn surrounding_markers_containing_cursor(
|
||||
display_map: &DisplaySnapshot,
|
||||
cursor: DisplayPoint,
|
||||
open_marker: char,
|
||||
close_marker: char,
|
||||
) -> Option<std::ops::Range<DisplayPoint>> {
|
||||
let range = surrounding_markers(display_map, cursor, true, true, open_marker, close_marker)?;
|
||||
let cursor_offset = cursor.to_offset(display_map, Bias::Left);
|
||||
let start_offset = range.start.to_offset(display_map, Bias::Left);
|
||||
let end_offset = range.end.to_offset(display_map, Bias::Right);
|
||||
|
||||
if cursor_offset >= start_offset && cursor_offset <= end_offset {
|
||||
Some(range)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn selection_cursor(map: &DisplaySnapshot, selection: &Selection<DisplayPoint>) -> DisplayPoint {
|
||||
if selection.reversed || selection.is_empty() {
|
||||
selection.head()
|
||||
} else {
|
||||
editor::movement::left(map, selection.head())
|
||||
}
|
||||
}
|
||||
|
||||
type SurroundEdits = Vec<(std::ops::Range<MultiBufferOffset>, String)>;
|
||||
type SurroundAnchors = Vec<std::ops::Range<Anchor>>;
|
||||
|
||||
fn apply_helix_surround_edits<F>(
|
||||
vim: &mut Vim,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Vim>,
|
||||
mut build: F,
|
||||
) where
|
||||
F: FnMut(&DisplaySnapshot, Vec<Selection<DisplayPoint>>) -> (SurroundEdits, SurroundAnchors),
|
||||
{
|
||||
vim.update_editor(cx, |_, editor, cx| {
|
||||
editor.transact(window, cx, |editor, window, cx| {
|
||||
editor.set_clip_at_line_ends(false, cx);
|
||||
|
||||
let display_map = editor.display_snapshot(cx);
|
||||
let selections = editor.selections.all_display(&display_map);
|
||||
let (mut edits, anchors) = build(&display_map, selections);
|
||||
|
||||
edits.sort_by_key(|edit| edit.0.start);
|
||||
editor.edit(edits, cx);
|
||||
|
||||
editor.change_selections(Default::default(), window, cx, |s| {
|
||||
s.select_anchor_ranges(anchors);
|
||||
});
|
||||
editor.set_clip_at_line_ends(true, cx);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
impl Vim {
|
||||
/// ms - Add surrounding characters around selection.
|
||||
pub fn helix_surround_add(&mut self, text: &str, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.stop_recording(cx);
|
||||
|
||||
let pair = bracket_pair_for_str_helix(text);
|
||||
|
||||
apply_helix_surround_edits(self, window, cx, |display_map, selections| {
|
||||
let mut edits = Vec::new();
|
||||
let mut anchors = Vec::new();
|
||||
|
||||
for selection in selections {
|
||||
let range = selection.range();
|
||||
let start = range.start.to_offset(display_map, Bias::Right);
|
||||
let end = range.end.to_offset(display_map, Bias::Left);
|
||||
|
||||
let end_anchor = display_map.buffer_snapshot().anchor_before(end);
|
||||
edits.push((end..end, pair.end.clone()));
|
||||
edits.push((start..start, pair.start.clone()));
|
||||
anchors.push(end_anchor..end_anchor);
|
||||
}
|
||||
|
||||
(edits, anchors)
|
||||
});
|
||||
}
|
||||
|
||||
/// mr - Replace innermost surrounding pair containing the cursor.
|
||||
pub fn helix_surround_replace(
|
||||
&mut self,
|
||||
old_char: char,
|
||||
new_char: char,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.stop_recording(cx);
|
||||
|
||||
let new_char_str = new_char.to_string();
|
||||
let new_pair = bracket_pair_for_str_helix(&new_char_str);
|
||||
|
||||
apply_helix_surround_edits(self, window, cx, |display_map, selections| {
|
||||
let mut edits: Vec<(std::ops::Range<MultiBufferOffset>, String)> = Vec::new();
|
||||
let mut anchors = Vec::new();
|
||||
|
||||
for selection in selections {
|
||||
let cursor = selection_cursor(display_map, &selection);
|
||||
|
||||
// For 'm', find the nearest surrounding pair
|
||||
let markers = match surround_pair_for_char_helix(old_char) {
|
||||
Some(pair) => Some((pair.open, pair.close)),
|
||||
None => find_nearest_surrounding_pair(display_map, cursor),
|
||||
};
|
||||
|
||||
let Some((open_marker, close_marker)) = markers else {
|
||||
let offset = selection.head().to_offset(display_map, Bias::Left);
|
||||
let anchor = display_map.buffer_snapshot().anchor_before(offset);
|
||||
anchors.push(anchor..anchor);
|
||||
continue;
|
||||
};
|
||||
|
||||
if let Some(range) = surrounding_markers_containing_cursor(
|
||||
display_map,
|
||||
cursor,
|
||||
open_marker,
|
||||
close_marker,
|
||||
) {
|
||||
let open_start = range.start.to_offset(display_map, Bias::Left);
|
||||
let open_end = open_start + open_marker.len_utf8();
|
||||
let close_end = range.end.to_offset(display_map, Bias::Left);
|
||||
let close_start = close_end - close_marker.len_utf8();
|
||||
|
||||
edits.push((close_start..close_end, new_pair.end.clone()));
|
||||
edits.push((open_start..open_end, new_pair.start.clone()));
|
||||
|
||||
let cursor_offset = cursor.to_offset(display_map, Bias::Left);
|
||||
let anchor = display_map.buffer_snapshot().anchor_before(cursor_offset);
|
||||
anchors.push(anchor..anchor);
|
||||
} else {
|
||||
let offset = selection.head().to_offset(display_map, Bias::Left);
|
||||
let anchor = display_map.buffer_snapshot().anchor_before(offset);
|
||||
anchors.push(anchor..anchor);
|
||||
}
|
||||
}
|
||||
|
||||
(edits, anchors)
|
||||
});
|
||||
}
|
||||
|
||||
/// md - Delete innermost surrounding pair containing the cursor.
|
||||
pub fn helix_surround_delete(
|
||||
&mut self,
|
||||
target_char: char,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.stop_recording(cx);
|
||||
|
||||
apply_helix_surround_edits(self, window, cx, |display_map, selections| {
|
||||
let mut edits: Vec<(std::ops::Range<MultiBufferOffset>, String)> = Vec::new();
|
||||
let mut anchors = Vec::new();
|
||||
|
||||
for selection in selections {
|
||||
let cursor = selection_cursor(display_map, &selection);
|
||||
|
||||
// For 'm', find the nearest surrounding pair
|
||||
let markers = match surround_pair_for_char_helix(target_char) {
|
||||
Some(pair) => Some((pair.open, pair.close)),
|
||||
None => find_nearest_surrounding_pair(display_map, cursor),
|
||||
};
|
||||
|
||||
let Some((open_marker, close_marker)) = markers else {
|
||||
let offset = selection.head().to_offset(display_map, Bias::Left);
|
||||
let anchor = display_map.buffer_snapshot().anchor_before(offset);
|
||||
anchors.push(anchor..anchor);
|
||||
continue;
|
||||
};
|
||||
|
||||
if let Some(range) = surrounding_markers_containing_cursor(
|
||||
display_map,
|
||||
cursor,
|
||||
open_marker,
|
||||
close_marker,
|
||||
) {
|
||||
let open_start = range.start.to_offset(display_map, Bias::Left);
|
||||
let open_end = open_start + open_marker.len_utf8();
|
||||
let close_end = range.end.to_offset(display_map, Bias::Left);
|
||||
let close_start = close_end - close_marker.len_utf8();
|
||||
|
||||
edits.push((close_start..close_end, String::new()));
|
||||
edits.push((open_start..open_end, String::new()));
|
||||
|
||||
let cursor_offset = cursor.to_offset(display_map, Bias::Left);
|
||||
let anchor = display_map.buffer_snapshot().anchor_before(cursor_offset);
|
||||
anchors.push(anchor..anchor);
|
||||
} else {
|
||||
let offset = selection.head().to_offset(display_map, Bias::Left);
|
||||
let anchor = display_map.buffer_snapshot().anchor_before(offset);
|
||||
anchors.push(anchor..anchor);
|
||||
}
|
||||
}
|
||||
|
||||
(edits, anchors)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use indoc::indoc;
|
||||
|
||||
use crate::{state::Mode, test::VimTestContext};
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_helix_surround_add(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
cx.enable_helix();
|
||||
|
||||
cx.set_state("hello ˇworld", Mode::HelixNormal);
|
||||
cx.simulate_keystrokes("m s (");
|
||||
cx.assert_state("hello (wˇ)orld", Mode::HelixNormal);
|
||||
|
||||
cx.set_state("hello ˇworld", Mode::HelixNormal);
|
||||
cx.simulate_keystrokes("m s )");
|
||||
cx.assert_state("hello (wˇ)orld", Mode::HelixNormal);
|
||||
|
||||
cx.set_state("hello «worlˇ»d", Mode::HelixNormal);
|
||||
cx.simulate_keystrokes("m s [");
|
||||
cx.assert_state("hello [worlˇ]d", Mode::HelixNormal);
|
||||
|
||||
cx.set_state("hello «worlˇ»d", Mode::HelixNormal);
|
||||
cx.simulate_keystrokes("m s \"");
|
||||
cx.assert_state("hello \"worlˇ\"d", Mode::HelixNormal);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_helix_surround_delete(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
cx.enable_helix();
|
||||
|
||||
cx.set_state("hello (woˇrld) test", Mode::HelixNormal);
|
||||
cx.simulate_keystrokes("m d (");
|
||||
cx.assert_state("hello woˇrld test", Mode::HelixNormal);
|
||||
|
||||
cx.set_state("hello \"woˇrld\" test", Mode::HelixNormal);
|
||||
cx.simulate_keystrokes("m d \"");
|
||||
cx.assert_state("hello woˇrld test", Mode::HelixNormal);
|
||||
|
||||
cx.set_state("hello woˇrld test", Mode::HelixNormal);
|
||||
cx.simulate_keystrokes("m d (");
|
||||
cx.assert_state("hello woˇrld test", Mode::HelixNormal);
|
||||
|
||||
cx.set_state(
|
||||
indoc! {"
|
||||
\"heˇllo\"
|
||||
fn world() {
|
||||
}"},
|
||||
Mode::HelixNormal,
|
||||
);
|
||||
cx.simulate_keystrokes("m d (");
|
||||
cx.assert_state(
|
||||
indoc! {"
|
||||
\"heˇllo\"
|
||||
fn world() {
|
||||
}"},
|
||||
Mode::HelixNormal,
|
||||
);
|
||||
|
||||
cx.set_state("((woˇrld))", Mode::HelixNormal);
|
||||
cx.simulate_keystrokes("m d (");
|
||||
cx.assert_state("(woˇrld)", Mode::HelixNormal);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_helix_surround_replace(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
cx.enable_helix();
|
||||
|
||||
cx.set_state("hello (woˇrld) test", Mode::HelixNormal);
|
||||
cx.simulate_keystrokes("m r ( [");
|
||||
cx.assert_state("hello [woˇrld] test", Mode::HelixNormal);
|
||||
|
||||
cx.set_state("hello (woˇrld) test", Mode::HelixNormal);
|
||||
cx.simulate_keystrokes("m r ( ]");
|
||||
cx.assert_state("hello [woˇrld] test", Mode::HelixNormal);
|
||||
|
||||
cx.set_state("hello \"woˇrld\" test", Mode::HelixNormal);
|
||||
cx.simulate_keystrokes("m r \" {");
|
||||
cx.assert_state("hello {woˇrld} test", Mode::HelixNormal);
|
||||
|
||||
cx.set_state(
|
||||
indoc! {"
|
||||
\"heˇllo\"
|
||||
fn world() {
|
||||
}"},
|
||||
Mode::HelixNormal,
|
||||
);
|
||||
cx.simulate_keystrokes("m r ( [");
|
||||
cx.assert_state(
|
||||
indoc! {"
|
||||
\"heˇllo\"
|
||||
fn world() {
|
||||
}"},
|
||||
Mode::HelixNormal,
|
||||
);
|
||||
|
||||
cx.set_state("((woˇrld))", Mode::HelixNormal);
|
||||
cx.simulate_keystrokes("m r ( [");
|
||||
cx.assert_state("([woˇrld])", Mode::HelixNormal);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_helix_surround_multiline(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
cx.enable_helix();
|
||||
|
||||
cx.set_state(
|
||||
indoc! {"
|
||||
function test() {
|
||||
return ˇvalue;
|
||||
}"},
|
||||
Mode::HelixNormal,
|
||||
);
|
||||
cx.simulate_keystrokes("m d {");
|
||||
cx.assert_state(
|
||||
indoc! {"
|
||||
function test()
|
||||
return ˇvalue;
|
||||
"},
|
||||
Mode::HelixNormal,
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_helix_surround_select_mode(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
cx.enable_helix();
|
||||
|
||||
cx.set_state("hello «worldˇ» test", Mode::HelixSelect);
|
||||
cx.simulate_keystrokes("m s {");
|
||||
cx.assert_state("hello {worldˇ} test", Mode::HelixNormal);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_helix_surround_multi_cursor(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
cx.enable_helix();
|
||||
|
||||
cx.set_state(
|
||||
indoc! {"
|
||||
(heˇllo)
|
||||
(woˇrld)"},
|
||||
Mode::HelixNormal,
|
||||
);
|
||||
cx.simulate_keystrokes("m d (");
|
||||
cx.assert_state(
|
||||
indoc! {"
|
||||
heˇllo
|
||||
woˇrld"},
|
||||
Mode::HelixNormal,
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_helix_surround_escape_cancels(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
cx.enable_helix();
|
||||
|
||||
cx.set_state("hello ˇworld", Mode::HelixNormal);
|
||||
cx.simulate_keystrokes("m escape");
|
||||
cx.assert_state("hello ˇworld", Mode::HelixNormal);
|
||||
|
||||
cx.set_state("hello (woˇrld)", Mode::HelixNormal);
|
||||
cx.simulate_keystrokes("m r ( escape");
|
||||
cx.assert_state("hello (woˇrld)", Mode::HelixNormal);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_helix_surround_no_vim_aliases(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
cx.enable_helix();
|
||||
|
||||
// In Helix mode, 'b', 'B', 'r', 'a' are NOT aliases for brackets.
|
||||
// They are treated as literal characters, so 'mdb' looks for 'b...b' surrounds.
|
||||
|
||||
// 'b' is not an alias - it looks for literal 'b...b', finds none, does nothing
|
||||
cx.set_state("hello (woˇrld) test", Mode::HelixNormal);
|
||||
cx.simulate_keystrokes("m d b");
|
||||
cx.assert_state("hello (woˇrld) test", Mode::HelixNormal);
|
||||
|
||||
// 'B' looks for literal 'B...B', not {}
|
||||
cx.set_state("hello {woˇrld} test", Mode::HelixNormal);
|
||||
cx.simulate_keystrokes("m d B");
|
||||
cx.assert_state("hello {woˇrld} test", Mode::HelixNormal);
|
||||
|
||||
// 'r' looks for literal 'r...r', not []
|
||||
cx.set_state("hello [woˇrld] test", Mode::HelixNormal);
|
||||
cx.simulate_keystrokes("m d r");
|
||||
cx.assert_state("hello [woˇrld] test", Mode::HelixNormal);
|
||||
|
||||
// 'a' looks for literal 'a...a', not <>
|
||||
cx.set_state("hello <woˇrld> test", Mode::HelixNormal);
|
||||
cx.simulate_keystrokes("m d a");
|
||||
cx.assert_state("hello <woˇrld> test", Mode::HelixNormal);
|
||||
|
||||
// Arbitrary chars work as symmetric pairs (Helix feature)
|
||||
cx.set_state("hello *woˇrld* test", Mode::HelixNormal);
|
||||
cx.simulate_keystrokes("m d *");
|
||||
cx.assert_state("hello woˇrld test", Mode::HelixNormal);
|
||||
|
||||
// ms (add) also doesn't use aliases - 'msb' adds literal 'b' surrounds
|
||||
cx.set_state("hello ˇworld", Mode::HelixNormal);
|
||||
cx.simulate_keystrokes("m s b");
|
||||
cx.assert_state("hello bwˇborld", Mode::HelixNormal);
|
||||
|
||||
// mr (replace) also doesn't use aliases
|
||||
cx.set_state("hello (woˇrld) test", Mode::HelixNormal);
|
||||
cx.simulate_keystrokes("m r ( b");
|
||||
cx.assert_state("hello bwoˇrldb test", Mode::HelixNormal);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_helix_surround_match_nearest(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
cx.enable_helix();
|
||||
|
||||
// mdm - delete nearest surrounding pair
|
||||
cx.set_state("hello (woˇrld) test", Mode::HelixNormal);
|
||||
cx.simulate_keystrokes("m d m");
|
||||
cx.assert_state("hello woˇrld test", Mode::HelixNormal);
|
||||
|
||||
cx.set_state("hello [woˇrld] test", Mode::HelixNormal);
|
||||
cx.simulate_keystrokes("m d m");
|
||||
cx.assert_state("hello woˇrld test", Mode::HelixNormal);
|
||||
|
||||
cx.set_state("hello {woˇrld} test", Mode::HelixNormal);
|
||||
cx.simulate_keystrokes("m d m");
|
||||
cx.assert_state("hello woˇrld test", Mode::HelixNormal);
|
||||
|
||||
// Nested - deletes innermost
|
||||
cx.set_state("([woˇrld])", Mode::HelixNormal);
|
||||
cx.simulate_keystrokes("m d m");
|
||||
cx.assert_state("(woˇrld)", Mode::HelixNormal);
|
||||
|
||||
// mrm - replace nearest surrounding pair
|
||||
cx.set_state("hello (woˇrld) test", Mode::HelixNormal);
|
||||
cx.simulate_keystrokes("m r m [");
|
||||
cx.assert_state("hello [woˇrld] test", Mode::HelixNormal);
|
||||
|
||||
cx.set_state("hello {woˇrld} test", Mode::HelixNormal);
|
||||
cx.simulate_keystrokes("m r m (");
|
||||
cx.assert_state("hello (woˇrld) test", Mode::HelixNormal);
|
||||
|
||||
// Nested - replaces innermost
|
||||
cx.set_state("([woˇrld])", Mode::HelixNormal);
|
||||
cx.simulate_keystrokes("m r m {");
|
||||
cx.assert_state("({woˇrld})", Mode::HelixNormal);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
use crate::{Vim, motion::Motion, object::Object, state::Mode};
|
||||
use collections::HashMap;
|
||||
use editor::SelectionEffects;
|
||||
use editor::{Bias, Editor, display_map::ToDisplayPoint};
|
||||
use gpui::actions;
|
||||
use gpui::{Context, Window};
|
||||
use language::SelectionGoal;
|
||||
use settings::Settings;
|
||||
use vim_mode_setting::HelixModeSetting;
|
||||
|
||||
#[derive(PartialEq, Eq)]
|
||||
pub(crate) enum IndentDirection {
|
||||
In,
|
||||
Out,
|
||||
Auto,
|
||||
}
|
||||
|
||||
actions!(
|
||||
vim,
|
||||
[
|
||||
/// Increases indentation of selected lines.
|
||||
Indent,
|
||||
/// Decreases indentation of selected lines.
|
||||
Outdent,
|
||||
/// Automatically adjusts indentation based on syntax.
|
||||
AutoIndent
|
||||
]
|
||||
);
|
||||
|
||||
pub(crate) fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
|
||||
Vim::action(editor, cx, |vim, _: &Indent, window, cx| {
|
||||
vim.record_current_action(cx);
|
||||
let count = Vim::take_count(cx).unwrap_or(1);
|
||||
Vim::take_forced_motion(cx);
|
||||
vim.store_visual_marks(window, cx);
|
||||
vim.update_editor(cx, |vim, editor, cx| {
|
||||
editor.transact(window, cx, |editor, window, cx| {
|
||||
let original_positions = vim.save_selection_starts(editor, cx);
|
||||
for _ in 0..count {
|
||||
editor.indent(&Default::default(), window, cx);
|
||||
}
|
||||
if !HelixModeSetting::get_global(cx).0 {
|
||||
vim.restore_selection_cursors(editor, window, cx, original_positions);
|
||||
}
|
||||
});
|
||||
});
|
||||
if vim.mode.is_visual() {
|
||||
vim.switch_mode(Mode::Normal, true, window, cx)
|
||||
}
|
||||
});
|
||||
|
||||
Vim::action(editor, cx, |vim, _: &Outdent, window, cx| {
|
||||
vim.record_current_action(cx);
|
||||
let count = Vim::take_count(cx).unwrap_or(1);
|
||||
Vim::take_forced_motion(cx);
|
||||
vim.store_visual_marks(window, cx);
|
||||
vim.update_editor(cx, |vim, editor, cx| {
|
||||
editor.transact(window, cx, |editor, window, cx| {
|
||||
let original_positions = vim.save_selection_starts(editor, cx);
|
||||
for _ in 0..count {
|
||||
editor.outdent(&Default::default(), window, cx);
|
||||
}
|
||||
if !HelixModeSetting::get_global(cx).0 {
|
||||
vim.restore_selection_cursors(editor, window, cx, original_positions);
|
||||
}
|
||||
});
|
||||
});
|
||||
if vim.mode.is_visual() {
|
||||
vim.switch_mode(Mode::Normal, true, window, cx)
|
||||
}
|
||||
});
|
||||
|
||||
Vim::action(editor, cx, |vim, _: &AutoIndent, window, cx| {
|
||||
vim.record_current_action(cx);
|
||||
let count = Vim::take_count(cx).unwrap_or(1);
|
||||
Vim::take_forced_motion(cx);
|
||||
vim.store_visual_marks(window, cx);
|
||||
vim.update_editor(cx, |vim, editor, cx| {
|
||||
editor.transact(window, cx, |editor, window, cx| {
|
||||
let original_positions = vim.save_selection_starts(editor, cx);
|
||||
for _ in 0..count {
|
||||
editor.autoindent(&Default::default(), window, cx);
|
||||
}
|
||||
vim.restore_selection_cursors(editor, window, cx, original_positions);
|
||||
});
|
||||
});
|
||||
if vim.mode.is_visual() {
|
||||
vim.switch_mode(Mode::Normal, true, window, cx)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
impl Vim {
|
||||
pub(crate) fn indent_motion(
|
||||
&mut self,
|
||||
motion: Motion,
|
||||
times: Option<usize>,
|
||||
forced_motion: bool,
|
||||
dir: IndentDirection,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.stop_recording(cx);
|
||||
self.update_editor(cx, |_, editor, cx| {
|
||||
let text_layout_details = editor.text_layout_details(window, cx);
|
||||
editor.transact(window, cx, |editor, window, cx| {
|
||||
let mut selection_starts: HashMap<_, _> = Default::default();
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
s.move_with(&mut |map, selection| {
|
||||
let anchor = map.display_point_to_anchor(selection.head(), Bias::Right);
|
||||
selection_starts.insert(selection.id, anchor);
|
||||
motion.expand_selection(
|
||||
map,
|
||||
selection,
|
||||
times,
|
||||
&text_layout_details,
|
||||
forced_motion,
|
||||
);
|
||||
});
|
||||
});
|
||||
match dir {
|
||||
IndentDirection::In => editor.indent(&Default::default(), window, cx),
|
||||
IndentDirection::Out => editor.outdent(&Default::default(), window, cx),
|
||||
IndentDirection::Auto => editor.autoindent(&Default::default(), window, cx),
|
||||
}
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
s.move_with(&mut |map, selection| {
|
||||
let anchor = selection_starts.remove(&selection.id).unwrap();
|
||||
selection.collapse_to(anchor.to_display_point(map), SelectionGoal::None);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn indent_object(
|
||||
&mut self,
|
||||
object: Object,
|
||||
around: bool,
|
||||
dir: IndentDirection,
|
||||
times: Option<usize>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.stop_recording(cx);
|
||||
self.update_editor(cx, |_, editor, cx| {
|
||||
editor.transact(window, cx, |editor, window, cx| {
|
||||
let mut original_positions: HashMap<_, _> = Default::default();
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
s.move_with(&mut |map, selection| {
|
||||
let anchor = map.display_point_to_anchor(selection.head(), Bias::Right);
|
||||
original_positions.insert(selection.id, anchor);
|
||||
object.expand_selection(map, selection, around, times);
|
||||
});
|
||||
});
|
||||
match dir {
|
||||
IndentDirection::In => editor.indent(&Default::default(), window, cx),
|
||||
IndentDirection::Out => editor.outdent(&Default::default(), window, cx),
|
||||
IndentDirection::Auto => editor.autoindent(&Default::default(), window, cx),
|
||||
}
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
s.move_with(&mut |map, selection| {
|
||||
let anchor = original_positions.remove(&selection.id).unwrap();
|
||||
selection.collapse_to(anchor.to_display_point(map), SelectionGoal::None);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::{
|
||||
state::Mode,
|
||||
test::{NeovimBackedTestContext, VimTestContext},
|
||||
};
|
||||
use indoc::indoc;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_indent_gv(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
cx.set_neovim_option("shiftwidth=4").await;
|
||||
|
||||
cx.set_shared_state("ˇhello\nworld\n").await;
|
||||
cx.simulate_shared_keystrokes("v j > g v").await;
|
||||
cx.shared_state()
|
||||
.await
|
||||
.assert_eq("« hello\n ˇ» world\n");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_indent_hx(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
cx.enable_helix();
|
||||
|
||||
cx.set_state("«Hello\nWorldˇ»\n", Mode::HelixNormal);
|
||||
|
||||
cx.simulate_keystrokes(">");
|
||||
cx.assert_state(" «Hello\n Worldˇ»\n", Mode::HelixNormal);
|
||||
|
||||
cx.simulate_keystrokes("<");
|
||||
cx.assert_state("«Hello\nWorldˇ»\n", Mode::HelixNormal);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_autoindent_op(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
|
||||
cx.set_state(
|
||||
indoc!(
|
||||
"
|
||||
fn a() {
|
||||
b();
|
||||
c();
|
||||
|
||||
d();
|
||||
ˇe();
|
||||
f();
|
||||
|
||||
g();
|
||||
}
|
||||
"
|
||||
),
|
||||
Mode::Normal,
|
||||
);
|
||||
|
||||
cx.simulate_keystrokes("= a p");
|
||||
cx.assert_state(
|
||||
indoc!(
|
||||
"
|
||||
fn a() {
|
||||
b();
|
||||
c();
|
||||
|
||||
d();
|
||||
ˇe();
|
||||
f();
|
||||
|
||||
g();
|
||||
}
|
||||
"
|
||||
),
|
||||
Mode::Normal,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
use crate::{Vim, state::Mode};
|
||||
use editor::{Bias, Editor};
|
||||
use gpui::{Action, Context, Window, actions};
|
||||
use language::SelectionGoal;
|
||||
use settings::Settings;
|
||||
use text::Point;
|
||||
use vim_mode_setting::HelixModeSetting;
|
||||
use workspace::searchable::Direction;
|
||||
|
||||
actions!(
|
||||
vim,
|
||||
[
|
||||
/// Switches to normal mode with cursor positioned before the current character.
|
||||
NormalBefore,
|
||||
/// Temporarily switches to normal mode for one command.
|
||||
TemporaryNormal,
|
||||
/// Inserts the next character from the line above into the current line.
|
||||
InsertFromAbove,
|
||||
/// Inserts the next character from the line below into the current line.
|
||||
InsertFromBelow
|
||||
]
|
||||
);
|
||||
|
||||
pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
|
||||
Vim::action(editor, cx, Vim::normal_before);
|
||||
Vim::action(editor, cx, Vim::temporary_normal);
|
||||
Vim::action(editor, cx, |vim, _: &InsertFromAbove, window, cx| {
|
||||
vim.insert_around(Direction::Prev, window, cx)
|
||||
});
|
||||
Vim::action(editor, cx, |vim, _: &InsertFromBelow, window, cx| {
|
||||
vim.insert_around(Direction::Next, window, cx)
|
||||
})
|
||||
}
|
||||
|
||||
impl Vim {
|
||||
pub(crate) fn normal_before(
|
||||
&mut self,
|
||||
action: &NormalBefore,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if self.active_operator().is_some() {
|
||||
self.operator_stack.clear();
|
||||
self.sync_vim_settings(window, cx);
|
||||
return;
|
||||
}
|
||||
let count = Vim::take_count(cx).unwrap_or(1);
|
||||
Vim::take_forced_motion(cx);
|
||||
self.stop_recording_immediately(action.boxed_clone(), cx);
|
||||
if count <= 1 || Vim::globals(cx).dot_replaying {
|
||||
self.create_mark("^".into(), window, cx);
|
||||
|
||||
if HelixModeSetting::get_global(cx).0 {
|
||||
self.update_editor(cx, |_, editor, cx| {
|
||||
editor.dismiss_menus_and_popups(false, window, cx);
|
||||
});
|
||||
self.switch_mode(Mode::HelixNormal, false, window, cx);
|
||||
return;
|
||||
}
|
||||
|
||||
self.update_editor(cx, |_, editor, cx| {
|
||||
editor.dismiss_menus_and_popups(false, window, cx);
|
||||
|
||||
editor.change_selections(Default::default(), window, cx, |s| {
|
||||
s.move_cursors_with(&mut |map, mut cursor, _| {
|
||||
*cursor.column_mut() = cursor.column().saturating_sub(1);
|
||||
(map.clip_point(cursor, Bias::Left), SelectionGoal::None)
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
self.switch_mode(Mode::Normal, false, window, cx);
|
||||
return;
|
||||
}
|
||||
|
||||
self.repeat(true, window, cx)
|
||||
}
|
||||
|
||||
fn temporary_normal(
|
||||
&mut self,
|
||||
_: &TemporaryNormal,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.switch_mode(Mode::Normal, true, window, cx);
|
||||
self.temp_mode = true;
|
||||
}
|
||||
|
||||
fn insert_around(&mut self, direction: Direction, _: &mut Window, cx: &mut Context<Self>) {
|
||||
self.update_editor(cx, |_, editor, cx| {
|
||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||
let mut edits = Vec::new();
|
||||
for selection in editor.selections.all::<Point>(&editor.display_snapshot(cx)) {
|
||||
let point = selection.head();
|
||||
let new_row = match direction {
|
||||
Direction::Next => point.row + 1,
|
||||
Direction::Prev if point.row > 0 => point.row - 1,
|
||||
_ => continue,
|
||||
};
|
||||
let source = snapshot.clip_point(Point::new(new_row, point.column), Bias::Left);
|
||||
if let Some(c) = snapshot.chars_at(source).next()
|
||||
&& c != '\n'
|
||||
{
|
||||
edits.push((point..point, c.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
editor.edit(edits, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::{
|
||||
state::Mode,
|
||||
test::{NeovimBackedTestContext, VimTestContext},
|
||||
};
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_enter_and_exit_insert_mode(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
cx.simulate_keystrokes("i");
|
||||
assert_eq!(cx.mode(), Mode::Insert);
|
||||
cx.simulate_keystrokes("T e s t");
|
||||
cx.assert_editor_state("Testˇ");
|
||||
cx.simulate_keystrokes("escape");
|
||||
assert_eq!(cx.mode(), Mode::Normal);
|
||||
cx.assert_editor_state("Tesˇt");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_insert_with_counts(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_shared_state("ˇhello\n").await;
|
||||
cx.simulate_shared_keystrokes("5 i - escape").await;
|
||||
cx.shared_state().await.assert_eq("----ˇ-hello\n");
|
||||
|
||||
cx.set_shared_state("ˇhello\n").await;
|
||||
cx.simulate_shared_keystrokes("5 a - escape").await;
|
||||
cx.shared_state().await.assert_eq("h----ˇ-ello\n");
|
||||
|
||||
cx.simulate_shared_keystrokes("4 shift-i - escape").await;
|
||||
cx.shared_state().await.assert_eq("---ˇ-h-----ello\n");
|
||||
|
||||
cx.simulate_shared_keystrokes("3 shift-a - escape").await;
|
||||
cx.shared_state().await.assert_eq("----h-----ello--ˇ-\n");
|
||||
|
||||
cx.set_shared_state("ˇhello\n").await;
|
||||
cx.simulate_shared_keystrokes("3 o o i escape").await;
|
||||
cx.shared_state().await.assert_eq("hello\noi\noi\noˇi\n");
|
||||
|
||||
cx.set_shared_state("ˇhello\n").await;
|
||||
cx.simulate_shared_keystrokes("3 shift-o o i escape").await;
|
||||
cx.shared_state().await.assert_eq("oi\noi\noˇi\nhello\n");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_insert_with_repeat(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_shared_state("ˇhello\n").await;
|
||||
cx.simulate_shared_keystrokes("3 i - escape").await;
|
||||
cx.shared_state().await.assert_eq("--ˇ-hello\n");
|
||||
cx.simulate_shared_keystrokes(".").await;
|
||||
cx.shared_state().await.assert_eq("----ˇ--hello\n");
|
||||
cx.simulate_shared_keystrokes("2 .").await;
|
||||
cx.shared_state().await.assert_eq("-----ˇ---hello\n");
|
||||
|
||||
cx.set_shared_state("ˇhello\n").await;
|
||||
cx.simulate_shared_keystrokes("2 o k k escape").await;
|
||||
cx.shared_state().await.assert_eq("hello\nkk\nkˇk\n");
|
||||
cx.simulate_shared_keystrokes(".").await;
|
||||
cx.shared_state()
|
||||
.await
|
||||
.assert_eq("hello\nkk\nkk\nkk\nkˇk\n");
|
||||
cx.simulate_shared_keystrokes("1 .").await;
|
||||
cx.shared_state()
|
||||
.await
|
||||
.assert_eq("hello\nkk\nkk\nkk\nkk\nkˇk\n");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_insert_ctrl_r(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_shared_state("heˇllo\n").await;
|
||||
cx.simulate_shared_keystrokes("y y i ctrl-r \"").await;
|
||||
cx.shared_state().await.assert_eq("hehello\nˇllo\n");
|
||||
|
||||
cx.simulate_shared_keystrokes("ctrl-r x ctrl-r escape")
|
||||
.await;
|
||||
cx.shared_state().await.assert_eq("hehello\nˇllo\n");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_insert_ctrl_y(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_shared_state("hello\nˇ\nworld").await;
|
||||
cx.simulate_shared_keystrokes("i ctrl-y ctrl-e").await;
|
||||
cx.shared_state().await.assert_eq("hello\nhoˇ\nworld");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
use gpui::{
|
||||
App, Context, Element, Entity, FontWeight, Render, Subscription, WeakEntity, Window, div,
|
||||
};
|
||||
use ui::text_for_keystrokes;
|
||||
use workspace::{HideStatusItem, StatusItemView, item::ItemHandle, ui::prelude::*};
|
||||
|
||||
use crate::{Vim, VimEvent, VimGlobals};
|
||||
|
||||
/// The ModeIndicator displays the current mode in the status bar.
|
||||
pub struct ModeIndicator {
|
||||
vim: Option<WeakEntity<Vim>>,
|
||||
pending_keys: Option<String>,
|
||||
vim_subscription: Option<Subscription>,
|
||||
}
|
||||
|
||||
impl ModeIndicator {
|
||||
/// Construct a new mode indicator in this window.
|
||||
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
cx.observe_pending_input(window, |this: &mut Self, window, cx| {
|
||||
this.update_pending_keys(window, cx);
|
||||
cx.notify();
|
||||
})
|
||||
.detach();
|
||||
|
||||
let handle = cx.entity();
|
||||
let window_handle = window.window_handle();
|
||||
cx.observe_new::<Vim>(move |_, window, cx| {
|
||||
let Some(window) = window else {
|
||||
return;
|
||||
};
|
||||
if window.window_handle() != window_handle {
|
||||
return;
|
||||
}
|
||||
let vim = cx.entity();
|
||||
handle.update(cx, |_, cx| {
|
||||
cx.subscribe(&vim, |mode_indicator, vim, event, cx| match event {
|
||||
VimEvent::Focused => {
|
||||
mode_indicator.vim_subscription =
|
||||
Some(cx.observe(&vim, |_, _, cx| cx.notify()));
|
||||
mode_indicator.vim = Some(vim.downgrade());
|
||||
}
|
||||
})
|
||||
.detach()
|
||||
})
|
||||
})
|
||||
.detach();
|
||||
|
||||
Self {
|
||||
vim: None,
|
||||
pending_keys: None,
|
||||
vim_subscription: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn update_pending_keys(&mut self, window: &mut Window, cx: &App) {
|
||||
self.pending_keys = window
|
||||
.pending_input_keystrokes()
|
||||
.map(|keystrokes| text_for_keystrokes(keystrokes, cx));
|
||||
}
|
||||
|
||||
fn vim(&self) -> Option<Entity<Vim>> {
|
||||
self.vim.as_ref().and_then(|vim| vim.upgrade())
|
||||
}
|
||||
|
||||
fn current_operators_description(&self, vim: Entity<Vim>, cx: &mut Context<Self>) -> String {
|
||||
let recording = Vim::globals(cx)
|
||||
.recording_register
|
||||
.map(|reg| format!("recording @{reg} "))
|
||||
.into_iter();
|
||||
|
||||
let vim = vim.read(cx);
|
||||
recording
|
||||
.chain(
|
||||
cx.global::<VimGlobals>()
|
||||
.pre_count
|
||||
.map(|count| format!("{}", count)),
|
||||
)
|
||||
.chain(vim.selected_register.map(|reg| format!("\"{reg}")))
|
||||
.chain(vim.operator_stack.iter().map(|item| item.status()))
|
||||
.chain(
|
||||
cx.global::<VimGlobals>()
|
||||
.post_count
|
||||
.map(|count| format!("{}", count)),
|
||||
)
|
||||
.collect::<Vec<_>>()
|
||||
.join("")
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ModeIndicator {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let vim = self.vim();
|
||||
let Some(vim) = vim else {
|
||||
return div().hidden().into_any_element();
|
||||
};
|
||||
|
||||
let vim_readable = vim.read(cx);
|
||||
let status_label = vim_readable.status_label.clone();
|
||||
let temp_mode = vim_readable.temp_mode;
|
||||
let mode = vim_readable.mode;
|
||||
|
||||
let theme = cx.theme();
|
||||
let colors = theme.colors();
|
||||
let system_transparent = gpui::hsla(0.0, 0.0, 0.0, 0.0);
|
||||
let vim_mode_text = match mode {
|
||||
crate::state::Mode::Normal => colors.vim_normal_foreground,
|
||||
crate::state::Mode::Insert => colors.vim_insert_foreground,
|
||||
crate::state::Mode::Replace => colors.vim_replace_foreground,
|
||||
crate::state::Mode::Visual => colors.vim_visual_foreground,
|
||||
crate::state::Mode::VisualLine => colors.vim_visual_line_foreground,
|
||||
crate::state::Mode::VisualBlock => colors.vim_visual_block_foreground,
|
||||
crate::state::Mode::HelixNormal => colors.vim_helix_normal_foreground,
|
||||
crate::state::Mode::HelixSelect => colors.vim_helix_select_foreground,
|
||||
};
|
||||
let bg_color = match mode {
|
||||
crate::state::Mode::Normal => colors.vim_normal_background,
|
||||
crate::state::Mode::Insert => colors.vim_insert_background,
|
||||
crate::state::Mode::Replace => colors.vim_replace_background,
|
||||
crate::state::Mode::Visual => colors.vim_visual_background,
|
||||
crate::state::Mode::VisualLine => colors.vim_visual_line_background,
|
||||
crate::state::Mode::VisualBlock => colors.vim_visual_block_background,
|
||||
crate::state::Mode::HelixNormal => colors.vim_helix_normal_background,
|
||||
crate::state::Mode::HelixSelect => colors.vim_helix_select_background,
|
||||
};
|
||||
|
||||
let (label, mode): (SharedString, Option<SharedString>) = if let Some(label) = status_label
|
||||
{
|
||||
(label, None)
|
||||
} else {
|
||||
let mode_str = if temp_mode {
|
||||
format!("(insert) {}", mode)
|
||||
} else {
|
||||
mode.to_string()
|
||||
};
|
||||
|
||||
let current_operators_description = self.current_operators_description(vim.clone(), cx);
|
||||
let pending = self
|
||||
.pending_keys
|
||||
.as_ref()
|
||||
.unwrap_or(¤t_operators_description);
|
||||
let mode = if bg_color != system_transparent {
|
||||
mode_str.into()
|
||||
} else {
|
||||
format!("-- {} --", mode_str).into()
|
||||
};
|
||||
(pending.into(), Some(mode))
|
||||
};
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.when(!label.is_empty(), |el| {
|
||||
el.child(
|
||||
Label::new(label)
|
||||
.line_height_style(LineHeightStyle::UiLabel)
|
||||
.weight(FontWeight::MEDIUM),
|
||||
)
|
||||
})
|
||||
.when_some(mode, |el, mode| {
|
||||
el.child(
|
||||
v_flex()
|
||||
.when(bg_color != system_transparent, |el| el.px_2())
|
||||
// match with other icons at the bottom that use default buttons
|
||||
.h(ButtonSize::Default.rems())
|
||||
.justify_center()
|
||||
.rounded_sm()
|
||||
.bg(bg_color)
|
||||
.child(
|
||||
Label::new(mode)
|
||||
.size(LabelSize::Small)
|
||||
.line_height_style(LineHeightStyle::UiLabel)
|
||||
.weight(FontWeight::MEDIUM)
|
||||
.when(
|
||||
bg_color != system_transparent
|
||||
&& vim_mode_text != system_transparent,
|
||||
|el| el.color(Color::Custom(vim_mode_text)),
|
||||
),
|
||||
),
|
||||
)
|
||||
})
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
|
||||
impl StatusItemView for ModeIndicator {
|
||||
fn set_active_pane_item(
|
||||
&mut self,
|
||||
_active_pane_item: Option<&dyn ItemHandle>,
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<Self>,
|
||||
) {
|
||||
}
|
||||
|
||||
fn hide_setting(&self, _: &App) -> Option<HideStatusItem> {
|
||||
// The Vim mode indicator is only visible while Vim mode is on.
|
||||
None
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,737 @@
|
||||
use crate::{
|
||||
Vim,
|
||||
motion::{self, Motion, MotionKind},
|
||||
object::Object,
|
||||
state::Mode,
|
||||
};
|
||||
use editor::{
|
||||
Bias, DisplayPoint,
|
||||
display_map::{DisplaySnapshot, ToDisplayPoint},
|
||||
movement::TextLayoutDetails,
|
||||
};
|
||||
use gpui::{Context, Window};
|
||||
use language::Selection;
|
||||
|
||||
impl Vim {
|
||||
pub fn change_motion(
|
||||
&mut self,
|
||||
motion: Motion,
|
||||
times: Option<usize>,
|
||||
forced_motion: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
// Some motions ignore failure when switching to normal mode
|
||||
let mut motion_kind = if matches!(
|
||||
motion,
|
||||
Motion::Left
|
||||
| Motion::Right
|
||||
| Motion::EndOfLine { .. }
|
||||
| Motion::WrappingLeft
|
||||
| Motion::StartOfLine { .. }
|
||||
) {
|
||||
Some(MotionKind::Exclusive)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
self.update_editor(cx, |vim, editor, cx| {
|
||||
let text_layout_details = editor.text_layout_details(window, cx);
|
||||
editor.transact(window, cx, |editor, window, cx| {
|
||||
// We are swapping to insert mode anyway. Just set the line end clipping behavior now
|
||||
editor.set_clip_at_line_ends(false, cx);
|
||||
editor.change_selections(Default::default(), window, cx, |s| {
|
||||
s.move_with(&mut |map, selection| {
|
||||
let kind = match motion {
|
||||
Motion::NextWordStart { ignore_punctuation }
|
||||
| Motion::NextSubwordStart { ignore_punctuation } => {
|
||||
expand_changed_word_selection(
|
||||
map,
|
||||
selection,
|
||||
times,
|
||||
ignore_punctuation,
|
||||
&text_layout_details,
|
||||
motion == Motion::NextSubwordStart { ignore_punctuation },
|
||||
!matches!(motion, Motion::NextWordStart { .. }),
|
||||
)
|
||||
}
|
||||
_ => {
|
||||
let kind = motion.expand_selection(
|
||||
map,
|
||||
selection,
|
||||
times,
|
||||
&text_layout_details,
|
||||
forced_motion,
|
||||
);
|
||||
if matches!(
|
||||
motion,
|
||||
Motion::CurrentLine | Motion::Down { .. } | Motion::Up { .. }
|
||||
) {
|
||||
let mut start_offset =
|
||||
selection.start.to_offset(map, Bias::Left);
|
||||
let classifier = map
|
||||
.buffer_snapshot()
|
||||
.char_classifier_at(selection.start.to_point(map));
|
||||
for (ch, offset) in map.buffer_chars_at(start_offset) {
|
||||
if ch == '\n' || !classifier.is_whitespace(ch) {
|
||||
break;
|
||||
}
|
||||
start_offset = offset + ch.len_utf8();
|
||||
}
|
||||
selection.start = start_offset.to_display_point(map);
|
||||
}
|
||||
kind
|
||||
}
|
||||
};
|
||||
if let Some(kind) = kind {
|
||||
motion_kind.get_or_insert(kind);
|
||||
}
|
||||
});
|
||||
});
|
||||
if let Some(kind) = motion_kind {
|
||||
vim.copy_selections_content(editor, kind, window, cx);
|
||||
editor.delete_selections_with_linked_edits(window, cx);
|
||||
editor.refresh_edit_prediction(true, false, window, cx);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if motion_kind.is_some() {
|
||||
self.switch_mode(Mode::Insert, false, window, cx)
|
||||
} else {
|
||||
self.switch_mode(Mode::Normal, false, window, cx)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn change_object(
|
||||
&mut self,
|
||||
object: Object,
|
||||
around: bool,
|
||||
times: Option<usize>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let mut objects_found = false;
|
||||
self.update_editor(cx, |vim, editor, cx| {
|
||||
// We are swapping to insert mode anyway. Just set the line end clipping behavior now
|
||||
editor.set_clip_at_line_ends(false, cx);
|
||||
editor.transact(window, cx, |editor, window, cx| {
|
||||
editor.change_selections(Default::default(), window, cx, |s| {
|
||||
s.move_with(&mut |map, selection| {
|
||||
objects_found |= object.expand_selection(map, selection, around, times);
|
||||
});
|
||||
});
|
||||
if objects_found {
|
||||
let kind = match object.target_visual_mode(vim.mode, around) {
|
||||
Mode::VisualLine => MotionKind::Linewise,
|
||||
_ => MotionKind::Exclusive,
|
||||
};
|
||||
vim.copy_selections_content(editor, kind, window, cx);
|
||||
editor.delete_selections_with_linked_edits(window, cx);
|
||||
editor.refresh_edit_prediction(true, false, window, cx);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if objects_found {
|
||||
self.switch_mode(Mode::Insert, false, window, cx);
|
||||
} else {
|
||||
self.switch_mode(Mode::Normal, false, window, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// From the docs https://vimdoc.sourceforge.net/htmldoc/motion.html
|
||||
// Special case: "cw" and "cW" are treated like "ce" and "cE" if the cursor is
|
||||
// on a non-blank. This is because "cw" is interpreted as change-word, and a
|
||||
// word does not include the following white space. {Vi: "cw" when on a blank
|
||||
// followed by other blanks changes only the first blank; this is probably a
|
||||
// bug, because "dw" deletes all the blanks}
|
||||
fn expand_changed_word_selection(
|
||||
map: &DisplaySnapshot,
|
||||
selection: &mut Selection<DisplayPoint>,
|
||||
times: Option<usize>,
|
||||
ignore_punctuation: bool,
|
||||
text_layout_details: &TextLayoutDetails,
|
||||
use_subword: bool,
|
||||
always_advance: bool,
|
||||
) -> Option<MotionKind> {
|
||||
let is_in_word = || {
|
||||
let classifier = map
|
||||
.buffer_snapshot()
|
||||
.char_classifier_at(selection.start.to_point(map));
|
||||
|
||||
map.buffer_chars_at(selection.head().to_offset(map, Bias::Left))
|
||||
.next()
|
||||
.map(|(c, _)| !classifier.is_whitespace(c))
|
||||
.unwrap_or_default()
|
||||
};
|
||||
if (times.is_none() || times.unwrap() == 1) && is_in_word() {
|
||||
let next_char = map
|
||||
.buffer_chars_at(
|
||||
motion::next_char(map, selection.end, false).to_offset(map, Bias::Left),
|
||||
)
|
||||
.next();
|
||||
match next_char {
|
||||
Some((' ', _)) => selection.end = motion::next_char(map, selection.end, false),
|
||||
_ => {
|
||||
if use_subword {
|
||||
selection.end =
|
||||
motion::next_subword_end(map, selection.end, ignore_punctuation, 1, false);
|
||||
} else {
|
||||
selection.end = motion::next_word_end(
|
||||
map,
|
||||
selection.end,
|
||||
ignore_punctuation,
|
||||
1,
|
||||
false,
|
||||
always_advance,
|
||||
);
|
||||
}
|
||||
selection.end = motion::next_char(map, selection.end, false);
|
||||
}
|
||||
}
|
||||
Some(MotionKind::Inclusive)
|
||||
} else {
|
||||
let motion = if use_subword {
|
||||
Motion::NextSubwordStart { ignore_punctuation }
|
||||
} else {
|
||||
Motion::NextWordStart { ignore_punctuation }
|
||||
};
|
||||
motion.expand_selection(map, selection, times, text_layout_details, false)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use indoc::indoc;
|
||||
|
||||
use crate::state::Mode;
|
||||
use crate::test::{NeovimBackedTestContext, VimTestContext};
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_change_h(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
cx.simulate("c h", "Teˇst").await.assert_matches();
|
||||
cx.simulate("c h", "Tˇest").await.assert_matches();
|
||||
cx.simulate("c h", "ˇTest").await.assert_matches();
|
||||
cx.simulate(
|
||||
"c h",
|
||||
indoc! {"
|
||||
Test
|
||||
ˇtest"},
|
||||
)
|
||||
.await
|
||||
.assert_matches();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_change_backspace(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
cx.simulate("c backspace", "Teˇst").await.assert_matches();
|
||||
cx.simulate("c backspace", "Tˇest").await.assert_matches();
|
||||
cx.simulate("c backspace", "ˇTest").await.assert_matches();
|
||||
cx.simulate(
|
||||
"c backspace",
|
||||
indoc! {"
|
||||
Test
|
||||
ˇtest"},
|
||||
)
|
||||
.await
|
||||
.assert_matches();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_change_l(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
cx.simulate("c l", "Teˇst").await.assert_matches();
|
||||
cx.simulate("c l", "Tesˇt").await.assert_matches();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_change_w(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
cx.simulate("c w", "Teˇst").await.assert_matches();
|
||||
cx.simulate("c w", "Tˇest test").await.assert_matches();
|
||||
cx.simulate("c w", "Testˇ test").await.assert_matches();
|
||||
cx.simulate("c w", "Tesˇt test").await.assert_matches();
|
||||
cx.simulate(
|
||||
"c w",
|
||||
indoc! {"
|
||||
Test teˇst
|
||||
test"},
|
||||
)
|
||||
.await
|
||||
.assert_matches();
|
||||
cx.simulate(
|
||||
"c w",
|
||||
indoc! {"
|
||||
Test tesˇt
|
||||
test"},
|
||||
)
|
||||
.await
|
||||
.assert_matches();
|
||||
cx.simulate(
|
||||
"c w",
|
||||
indoc! {"
|
||||
Test test
|
||||
ˇ
|
||||
test"},
|
||||
)
|
||||
.await
|
||||
.assert_matches();
|
||||
|
||||
cx.simulate("c shift-w", "Test teˇst-test test")
|
||||
.await
|
||||
.assert_matches();
|
||||
|
||||
// on last character of word, `cw` doesn't eat subsequent punctuation
|
||||
// see https://github.com/zed-industries/zed/issues/35269
|
||||
cx.simulate("c w", "tesˇt-test").await.assert_matches();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_change_e(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
cx.simulate("c e", "Teˇst Test").await.assert_matches();
|
||||
cx.simulate("c e", "Tˇest test").await.assert_matches();
|
||||
cx.simulate(
|
||||
"c e",
|
||||
indoc! {"
|
||||
Test teˇst
|
||||
test"},
|
||||
)
|
||||
.await
|
||||
.assert_matches();
|
||||
cx.simulate(
|
||||
"c e",
|
||||
indoc! {"
|
||||
Test tesˇt
|
||||
test"},
|
||||
)
|
||||
.await
|
||||
.assert_matches();
|
||||
cx.simulate(
|
||||
"c e",
|
||||
indoc! {"
|
||||
Test test
|
||||
ˇ
|
||||
test"},
|
||||
)
|
||||
.await
|
||||
.assert_matches();
|
||||
|
||||
cx.simulate("c shift-e", "Test teˇst-test test")
|
||||
.await
|
||||
.assert_matches();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_change_b(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
cx.simulate("c b", "Teˇst Test").await.assert_matches();
|
||||
cx.simulate("c b", "Test ˇtest").await.assert_matches();
|
||||
cx.simulate("c b", "Test1 test2 ˇtest3")
|
||||
.await
|
||||
.assert_matches();
|
||||
cx.simulate(
|
||||
"c b",
|
||||
indoc! {"
|
||||
Test test
|
||||
ˇtest"},
|
||||
)
|
||||
.await
|
||||
.assert_matches();
|
||||
cx.simulate(
|
||||
"c b",
|
||||
indoc! {"
|
||||
Test test
|
||||
ˇ
|
||||
test"},
|
||||
)
|
||||
.await
|
||||
.assert_matches();
|
||||
|
||||
cx.simulate("c shift-b", "Test test-test ˇtest")
|
||||
.await
|
||||
.assert_matches();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_change_end_of_line(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
cx.simulate(
|
||||
"c $",
|
||||
indoc! {"
|
||||
The qˇuick
|
||||
brown fox"},
|
||||
)
|
||||
.await
|
||||
.assert_matches();
|
||||
cx.simulate(
|
||||
"c $",
|
||||
indoc! {"
|
||||
The quick
|
||||
ˇ
|
||||
brown fox"},
|
||||
)
|
||||
.await
|
||||
.assert_matches();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_change_0(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.simulate(
|
||||
"c 0",
|
||||
indoc! {"
|
||||
The qˇuick
|
||||
brown fox"},
|
||||
)
|
||||
.await
|
||||
.assert_matches();
|
||||
cx.simulate(
|
||||
"c 0",
|
||||
indoc! {"
|
||||
The quick
|
||||
ˇ
|
||||
brown fox"},
|
||||
)
|
||||
.await
|
||||
.assert_matches();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_change_k(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.simulate(
|
||||
"c k",
|
||||
indoc! {"
|
||||
The quick
|
||||
brown ˇfox
|
||||
jumps over"},
|
||||
)
|
||||
.await
|
||||
.assert_matches();
|
||||
cx.simulate(
|
||||
"c k",
|
||||
indoc! {"
|
||||
The quick
|
||||
brown fox
|
||||
jumps ˇover"},
|
||||
)
|
||||
.await
|
||||
.assert_matches();
|
||||
cx.simulate(
|
||||
"c k",
|
||||
indoc! {"
|
||||
The qˇuick
|
||||
brown fox
|
||||
jumps over"},
|
||||
)
|
||||
.await
|
||||
.assert_matches();
|
||||
cx.simulate(
|
||||
"c k",
|
||||
indoc! {"
|
||||
ˇ
|
||||
brown fox
|
||||
jumps over"},
|
||||
)
|
||||
.await
|
||||
.assert_matches();
|
||||
cx.simulate(
|
||||
"c k",
|
||||
indoc! {"
|
||||
The quick
|
||||
brown fox
|
||||
ˇjumps over"},
|
||||
)
|
||||
.await
|
||||
.assert_matches();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_change_j(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
cx.simulate(
|
||||
"c j",
|
||||
indoc! {"
|
||||
The quick
|
||||
brown ˇfox
|
||||
jumps over"},
|
||||
)
|
||||
.await
|
||||
.assert_matches();
|
||||
cx.simulate(
|
||||
"c j",
|
||||
indoc! {"
|
||||
The quick
|
||||
brown fox
|
||||
jumps ˇover"},
|
||||
)
|
||||
.await
|
||||
.assert_matches();
|
||||
cx.simulate(
|
||||
"c j",
|
||||
indoc! {"
|
||||
The qˇuick
|
||||
brown fox
|
||||
jumps over"},
|
||||
)
|
||||
.await
|
||||
.assert_matches();
|
||||
cx.simulate(
|
||||
"c j",
|
||||
indoc! {"
|
||||
The quick
|
||||
brown fox
|
||||
ˇ"},
|
||||
)
|
||||
.await
|
||||
.assert_matches();
|
||||
cx.simulate(
|
||||
"c j",
|
||||
indoc! {"
|
||||
The quick
|
||||
ˇbrown fox
|
||||
jumps over"},
|
||||
)
|
||||
.await
|
||||
.assert_matches();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_change_end_of_document(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
cx.simulate(
|
||||
"c shift-g",
|
||||
indoc! {"
|
||||
The quick
|
||||
brownˇ fox
|
||||
jumps over
|
||||
the lazy"},
|
||||
)
|
||||
.await
|
||||
.assert_matches();
|
||||
cx.simulate(
|
||||
"c shift-g",
|
||||
indoc! {"
|
||||
The quick
|
||||
brownˇ fox
|
||||
jumps over
|
||||
the lazy"},
|
||||
)
|
||||
.await
|
||||
.assert_matches();
|
||||
cx.simulate(
|
||||
"c shift-g",
|
||||
indoc! {"
|
||||
The quick
|
||||
brown fox
|
||||
jumps over
|
||||
the lˇazy"},
|
||||
)
|
||||
.await
|
||||
.assert_matches();
|
||||
cx.simulate(
|
||||
"c shift-g",
|
||||
indoc! {"
|
||||
The quick
|
||||
brown fox
|
||||
jumps over
|
||||
ˇ"},
|
||||
)
|
||||
.await
|
||||
.assert_matches();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_change_cc(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
cx.simulate(
|
||||
"c c",
|
||||
indoc! {"
|
||||
The quick
|
||||
brownˇ fox
|
||||
jumps over
|
||||
the lazy"},
|
||||
)
|
||||
.await
|
||||
.assert_matches();
|
||||
|
||||
cx.simulate(
|
||||
"c c",
|
||||
indoc! {"
|
||||
ˇThe quick
|
||||
brown fox
|
||||
jumps over
|
||||
the lazy"},
|
||||
)
|
||||
.await
|
||||
.assert_matches();
|
||||
|
||||
cx.simulate(
|
||||
"c c",
|
||||
indoc! {"
|
||||
The quick
|
||||
broˇwn fox
|
||||
jumps over
|
||||
the lazy"},
|
||||
)
|
||||
.await
|
||||
.assert_matches();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_change_gg(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
cx.simulate(
|
||||
"c g g",
|
||||
indoc! {"
|
||||
The quick
|
||||
brownˇ fox
|
||||
jumps over
|
||||
the lazy"},
|
||||
)
|
||||
.await
|
||||
.assert_matches();
|
||||
cx.simulate(
|
||||
"c g g",
|
||||
indoc! {"
|
||||
The quick
|
||||
brown fox
|
||||
jumps over
|
||||
the lˇazy"},
|
||||
)
|
||||
.await
|
||||
.assert_matches();
|
||||
cx.simulate(
|
||||
"c g g",
|
||||
indoc! {"
|
||||
The qˇuick
|
||||
brown fox
|
||||
jumps over
|
||||
the lazy"},
|
||||
)
|
||||
.await
|
||||
.assert_matches();
|
||||
cx.simulate(
|
||||
"c g g",
|
||||
indoc! {"
|
||||
ˇ
|
||||
brown fox
|
||||
jumps over
|
||||
the lazy"},
|
||||
)
|
||||
.await
|
||||
.assert_matches();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_repeated_cj(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
for count in 1..=5 {
|
||||
cx.simulate_at_each_offset(
|
||||
&format!("c {count} j"),
|
||||
indoc! {"
|
||||
ˇThe quˇickˇ browˇn
|
||||
ˇ
|
||||
ˇfox ˇjumpsˇ-ˇoˇver
|
||||
ˇthe lazy dog
|
||||
"},
|
||||
)
|
||||
.await
|
||||
.assert_matches();
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_repeated_cl(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
for count in 1..=5 {
|
||||
cx.simulate_at_each_offset(
|
||||
&format!("c {count} l"),
|
||||
indoc! {"
|
||||
ˇThe quˇickˇ browˇn
|
||||
ˇ
|
||||
ˇfox ˇjumpsˇ-ˇoˇver
|
||||
ˇthe lazy dog
|
||||
"},
|
||||
)
|
||||
.await
|
||||
.assert_matches();
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_repeated_cb(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
for count in 1..=5 {
|
||||
cx.simulate_at_each_offset(
|
||||
&format!("c {count} b"),
|
||||
indoc! {"
|
||||
ˇThe quˇickˇ browˇn
|
||||
ˇ
|
||||
ˇfox ˇjumpsˇ-ˇoˇver
|
||||
ˇthe lazy dog
|
||||
"},
|
||||
)
|
||||
.await
|
||||
.assert_matches()
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_repeated_ce(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
for count in 1..=5 {
|
||||
cx.simulate_at_each_offset(
|
||||
&format!("c {count} e"),
|
||||
indoc! {"
|
||||
ˇThe quˇickˇ browˇn
|
||||
ˇ
|
||||
ˇfox ˇjumpsˇ-ˇoˇver
|
||||
ˇthe lazy dog
|
||||
"},
|
||||
)
|
||||
.await
|
||||
.assert_matches();
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_change_with_selection_spanning_expanded_diff_hunk(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
|
||||
let diff_base = indoc! {"
|
||||
fn main() {
|
||||
println!(\"old\");
|
||||
}
|
||||
"};
|
||||
|
||||
cx.set_state(
|
||||
indoc! {"
|
||||
fn main() {
|
||||
ˇprintln!(\"new\");
|
||||
}
|
||||
"},
|
||||
Mode::Normal,
|
||||
);
|
||||
cx.set_head_text(diff_base);
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.expand_all_diff_hunks(&editor::actions::ExpandAllDiffHunks, window, cx);
|
||||
});
|
||||
|
||||
// Enter visual mode and move up so the selection spans from the
|
||||
// insertion (current line) into the deletion (diff base line).
|
||||
// Then press `c` which in visual mode dispatches `vim::Substitute`,
|
||||
// performing the change operation across the insertion/deletion boundary.
|
||||
cx.simulate_keystrokes("v k c");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,469 @@
|
||||
use collections::HashMap;
|
||||
use editor::{SelectionEffects, display_map::ToDisplayPoint};
|
||||
use gpui::{Context, Window};
|
||||
use language::{Bias, Point, SelectionGoal};
|
||||
use multi_buffer::MultiBufferRow;
|
||||
|
||||
use crate::{
|
||||
Vim,
|
||||
motion::Motion,
|
||||
normal::{ChangeCase, ConvertToLowerCase, ConvertToRot13, ConvertToRot47, ConvertToUpperCase},
|
||||
object::Object,
|
||||
state::Mode,
|
||||
};
|
||||
|
||||
pub enum ConvertTarget {
|
||||
LowerCase,
|
||||
UpperCase,
|
||||
OppositeCase,
|
||||
Rot13,
|
||||
Rot47,
|
||||
}
|
||||
|
||||
impl Vim {
|
||||
pub fn convert_motion(
|
||||
&mut self,
|
||||
motion: Motion,
|
||||
times: Option<usize>,
|
||||
forced_motion: bool,
|
||||
mode: ConvertTarget,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.stop_recording(cx);
|
||||
self.update_editor(cx, |_, editor, cx| {
|
||||
editor.set_clip_at_line_ends(false, cx);
|
||||
let text_layout_details = editor.text_layout_details(window, cx);
|
||||
editor.transact(window, cx, |editor, window, cx| {
|
||||
let mut selection_starts: HashMap<_, _> = Default::default();
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
s.move_with(&mut |map, selection| {
|
||||
let anchor = map.display_point_to_anchor(selection.head(), Bias::Left);
|
||||
selection_starts.insert(selection.id, anchor);
|
||||
motion.expand_selection(
|
||||
map,
|
||||
selection,
|
||||
times,
|
||||
&text_layout_details,
|
||||
forced_motion,
|
||||
);
|
||||
});
|
||||
});
|
||||
match mode {
|
||||
ConvertTarget::LowerCase => {
|
||||
editor.convert_to_lower_case(&Default::default(), window, cx)
|
||||
}
|
||||
ConvertTarget::UpperCase => {
|
||||
editor.convert_to_upper_case(&Default::default(), window, cx)
|
||||
}
|
||||
ConvertTarget::OppositeCase => {
|
||||
editor.convert_to_opposite_case(&Default::default(), window, cx)
|
||||
}
|
||||
ConvertTarget::Rot13 => {
|
||||
editor.convert_to_rot13(&Default::default(), window, cx)
|
||||
}
|
||||
ConvertTarget::Rot47 => {
|
||||
editor.convert_to_rot47(&Default::default(), window, cx)
|
||||
}
|
||||
}
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
s.move_with(&mut |map, selection| {
|
||||
let anchor = selection_starts.remove(&selection.id).unwrap();
|
||||
selection.collapse_to(anchor.to_display_point(map), SelectionGoal::None);
|
||||
});
|
||||
});
|
||||
});
|
||||
editor.set_clip_at_line_ends(true, cx);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn convert_object(
|
||||
&mut self,
|
||||
object: Object,
|
||||
around: bool,
|
||||
mode: ConvertTarget,
|
||||
times: Option<usize>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.stop_recording(cx);
|
||||
self.update_editor(cx, |_, editor, cx| {
|
||||
editor.transact(window, cx, |editor, window, cx| {
|
||||
editor.set_clip_at_line_ends(false, cx);
|
||||
let mut original_positions: HashMap<_, _> = Default::default();
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
s.move_with(&mut |map, selection| {
|
||||
object.expand_selection(map, selection, around, times);
|
||||
original_positions.insert(
|
||||
selection.id,
|
||||
map.display_point_to_anchor(selection.start, Bias::Left),
|
||||
);
|
||||
});
|
||||
});
|
||||
match mode {
|
||||
ConvertTarget::LowerCase => {
|
||||
editor.convert_to_lower_case(&Default::default(), window, cx)
|
||||
}
|
||||
ConvertTarget::UpperCase => {
|
||||
editor.convert_to_upper_case(&Default::default(), window, cx)
|
||||
}
|
||||
ConvertTarget::OppositeCase => {
|
||||
editor.convert_to_opposite_case(&Default::default(), window, cx)
|
||||
}
|
||||
ConvertTarget::Rot13 => {
|
||||
editor.convert_to_rot13(&Default::default(), window, cx)
|
||||
}
|
||||
ConvertTarget::Rot47 => {
|
||||
editor.convert_to_rot47(&Default::default(), window, cx)
|
||||
}
|
||||
}
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
s.move_with(&mut |map, selection| {
|
||||
let anchor = original_positions.remove(&selection.id).unwrap();
|
||||
selection.collapse_to(anchor.to_display_point(map), SelectionGoal::None);
|
||||
});
|
||||
});
|
||||
editor.set_clip_at_line_ends(true, cx);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
pub fn change_case(&mut self, _: &ChangeCase, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.manipulate_text(window, cx, |c| {
|
||||
if c.is_lowercase() {
|
||||
c.to_uppercase().collect::<Vec<char>>()
|
||||
} else {
|
||||
c.to_lowercase().collect::<Vec<char>>()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn convert_to_upper_case(
|
||||
&mut self,
|
||||
_: &ConvertToUpperCase,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.manipulate_text(window, cx, |c| c.to_uppercase().collect::<Vec<char>>())
|
||||
}
|
||||
|
||||
pub fn convert_to_lower_case(
|
||||
&mut self,
|
||||
_: &ConvertToLowerCase,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.manipulate_text(window, cx, |c| c.to_lowercase().collect::<Vec<char>>())
|
||||
}
|
||||
|
||||
pub fn convert_to_rot13(
|
||||
&mut self,
|
||||
_: &ConvertToRot13,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.manipulate_text(window, cx, |c| {
|
||||
vec![match c {
|
||||
'A'..='M' | 'a'..='m' => ((c as u8) + 13) as char,
|
||||
'N'..='Z' | 'n'..='z' => ((c as u8) - 13) as char,
|
||||
_ => c,
|
||||
}]
|
||||
})
|
||||
}
|
||||
|
||||
pub fn convert_to_rot47(
|
||||
&mut self,
|
||||
_: &ConvertToRot47,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.manipulate_text(window, cx, |c| {
|
||||
let code_point = c as u32;
|
||||
if code_point >= 33 && code_point <= 126 {
|
||||
return vec![char::from_u32(33 + ((code_point + 14) % 94)).unwrap()];
|
||||
}
|
||||
vec![c]
|
||||
})
|
||||
}
|
||||
|
||||
fn manipulate_text<F>(&mut self, window: &mut Window, cx: &mut Context<Self>, transform: F)
|
||||
where
|
||||
F: Fn(char) -> Vec<char> + Copy,
|
||||
{
|
||||
self.record_current_action(cx);
|
||||
self.store_visual_marks(window, cx);
|
||||
let count = Vim::take_count(cx).unwrap_or(1) as u32;
|
||||
Vim::take_forced_motion(cx);
|
||||
|
||||
self.update_editor(cx, |vim, editor, cx| {
|
||||
let mut ranges = Vec::new();
|
||||
let mut cursor_positions = Vec::new();
|
||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||
for selection in editor.selections.all_adjusted(&editor.display_snapshot(cx)) {
|
||||
match vim.mode {
|
||||
Mode::Visual | Mode::VisualLine => {
|
||||
ranges.push(selection.start..selection.end);
|
||||
cursor_positions.push(selection.start..selection.start);
|
||||
}
|
||||
Mode::VisualBlock => {
|
||||
ranges.push(selection.start..selection.end);
|
||||
if cursor_positions.is_empty() {
|
||||
cursor_positions.push(selection.start..selection.start);
|
||||
}
|
||||
}
|
||||
|
||||
Mode::HelixNormal | Mode::HelixSelect => {
|
||||
if selection.is_empty() {
|
||||
// Handle empty selection by operating on single character
|
||||
let start = selection.start;
|
||||
let end = snapshot.clip_point(start + Point::new(0, 1), Bias::Right);
|
||||
ranges.push(start..end);
|
||||
cursor_positions.push(selection.start..selection.start);
|
||||
} else {
|
||||
ranges.push(selection.start..selection.end);
|
||||
cursor_positions.push(selection.start..selection.end);
|
||||
}
|
||||
}
|
||||
Mode::Insert | Mode::Normal | Mode::Replace => {
|
||||
let start = selection.start;
|
||||
let mut end = start;
|
||||
for _ in 0..count {
|
||||
end = snapshot.clip_point(end + Point::new(0, 1), Bias::Right);
|
||||
}
|
||||
ranges.push(start..end);
|
||||
|
||||
if end.column == snapshot.line_len(MultiBufferRow(end.row))
|
||||
&& end.column > 0
|
||||
{
|
||||
end = snapshot.clip_point(end - Point::new(0, 1), Bias::Left);
|
||||
}
|
||||
cursor_positions.push(end..end)
|
||||
}
|
||||
}
|
||||
}
|
||||
editor.transact(window, cx, |editor, window, cx| {
|
||||
for range in ranges.into_iter().rev() {
|
||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||
let text = snapshot
|
||||
.text_for_range(range.start..range.end)
|
||||
.flat_map(|s| s.chars())
|
||||
.flat_map(transform)
|
||||
.collect::<String>();
|
||||
editor.edit([(range, text)], cx)
|
||||
}
|
||||
editor.change_selections(Default::default(), window, cx, |s| {
|
||||
s.select_ranges(cursor_positions)
|
||||
})
|
||||
});
|
||||
});
|
||||
if self.mode != Mode::HelixNormal {
|
||||
self.switch_mode(Mode::Normal, true, window, cx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::test::VimTestContext;
|
||||
|
||||
use crate::{state::Mode, test::NeovimBackedTestContext};
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_change_case(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
cx.set_shared_state("ˇabC\n").await;
|
||||
cx.simulate_shared_keystrokes("~").await;
|
||||
cx.shared_state().await.assert_eq("AˇbC\n");
|
||||
cx.simulate_shared_keystrokes("2 ~").await;
|
||||
cx.shared_state().await.assert_eq("ABˇc\n");
|
||||
|
||||
// works in visual mode
|
||||
cx.set_shared_state("a😀C«dÉ1*fˇ»\n").await;
|
||||
cx.simulate_shared_keystrokes("~").await;
|
||||
cx.shared_state().await.assert_eq("a😀CˇDé1*F\n");
|
||||
|
||||
// works with multibyte characters
|
||||
cx.simulate_shared_keystrokes("~").await;
|
||||
cx.set_shared_state("aˇC😀é1*F\n").await;
|
||||
cx.simulate_shared_keystrokes("4 ~").await;
|
||||
cx.shared_state().await.assert_eq("ac😀É1ˇ*F\n");
|
||||
|
||||
// works with line selections
|
||||
cx.set_shared_state("abˇC\n").await;
|
||||
cx.simulate_shared_keystrokes("shift-v ~").await;
|
||||
cx.shared_state().await.assert_eq("ˇABc\n");
|
||||
|
||||
// works in visual block mode
|
||||
cx.set_shared_state("ˇaa\nbb\ncc").await;
|
||||
cx.simulate_shared_keystrokes("ctrl-v j ~").await;
|
||||
cx.shared_state().await.assert_eq("ˇAa\nBb\ncc");
|
||||
|
||||
// works with multiple cursors (zed only)
|
||||
cx.set_state("aˇßcdˇe\n", Mode::Normal);
|
||||
cx.simulate_keystrokes("~");
|
||||
cx.assert_state("aSSˇcdˇE\n", Mode::Normal);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_convert_to_upper_case(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
// works in visual mode
|
||||
cx.set_shared_state("a😀C«dÉ1*fˇ»\n").await;
|
||||
cx.simulate_shared_keystrokes("shift-u").await;
|
||||
cx.shared_state().await.assert_eq("a😀CˇDÉ1*F\n");
|
||||
|
||||
// works with line selections
|
||||
cx.set_shared_state("abˇC\n").await;
|
||||
cx.simulate_shared_keystrokes("shift-v shift-u").await;
|
||||
cx.shared_state().await.assert_eq("ˇABC\n");
|
||||
|
||||
// works in visual block mode
|
||||
cx.set_shared_state("ˇaa\nbb\ncc").await;
|
||||
cx.simulate_shared_keystrokes("ctrl-v j shift-u").await;
|
||||
cx.shared_state().await.assert_eq("ˇAa\nBb\ncc");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_convert_to_lower_case(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
// works in visual mode
|
||||
cx.set_shared_state("A😀c«DÉ1*fˇ»\n").await;
|
||||
cx.simulate_shared_keystrokes("u").await;
|
||||
cx.shared_state().await.assert_eq("A😀cˇdé1*f\n");
|
||||
|
||||
// works with line selections
|
||||
cx.set_shared_state("ABˇc\n").await;
|
||||
cx.simulate_shared_keystrokes("shift-v u").await;
|
||||
cx.shared_state().await.assert_eq("ˇabc\n");
|
||||
|
||||
// works in visual block mode
|
||||
cx.set_shared_state("ˇAa\nBb\nCc").await;
|
||||
cx.simulate_shared_keystrokes("ctrl-v j u").await;
|
||||
cx.shared_state().await.assert_eq("ˇaa\nbb\nCc");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_change_case_motion(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_shared_state("ˇabc def").await;
|
||||
cx.simulate_shared_keystrokes("g shift-u w").await;
|
||||
cx.shared_state().await.assert_eq("ˇABC def");
|
||||
|
||||
cx.simulate_shared_keystrokes("g u w").await;
|
||||
cx.shared_state().await.assert_eq("ˇabc def");
|
||||
|
||||
cx.simulate_shared_keystrokes("g ~ w").await;
|
||||
cx.shared_state().await.assert_eq("ˇABC def");
|
||||
|
||||
cx.simulate_shared_keystrokes(".").await;
|
||||
cx.shared_state().await.assert_eq("ˇabc def");
|
||||
|
||||
cx.set_shared_state("abˇc def").await;
|
||||
cx.simulate_shared_keystrokes("g ~ i w").await;
|
||||
cx.shared_state().await.assert_eq("ˇABC def");
|
||||
|
||||
cx.simulate_shared_keystrokes(".").await;
|
||||
cx.shared_state().await.assert_eq("ˇabc def");
|
||||
|
||||
cx.simulate_shared_keystrokes("g shift-u $").await;
|
||||
cx.shared_state().await.assert_eq("ˇABC DEF");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_change_case_motion_object(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_shared_state("abc dˇef\n").await;
|
||||
cx.simulate_shared_keystrokes("g shift-u i w").await;
|
||||
cx.shared_state().await.assert_eq("abc ˇDEF\n");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_convert_to_rot13(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
// works in visual mode
|
||||
cx.set_shared_state("a😀C«dÉ1*fˇ»\n").await;
|
||||
cx.simulate_shared_keystrokes("g ?").await;
|
||||
cx.shared_state().await.assert_eq("a😀CˇqÉ1*s\n");
|
||||
|
||||
// works with line selections
|
||||
cx.set_shared_state("abˇC\n").await;
|
||||
cx.simulate_shared_keystrokes("shift-v g ?").await;
|
||||
cx.shared_state().await.assert_eq("ˇnoP\n");
|
||||
|
||||
// works in visual block mode
|
||||
cx.set_shared_state("ˇaa\nbb\ncc").await;
|
||||
cx.simulate_shared_keystrokes("ctrl-v j g ?").await;
|
||||
cx.shared_state().await.assert_eq("ˇna\nob\ncc");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_change_rot13_motion(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_shared_state("ˇabc def").await;
|
||||
cx.simulate_shared_keystrokes("g ? w").await;
|
||||
cx.shared_state().await.assert_eq("ˇnop def");
|
||||
|
||||
cx.simulate_shared_keystrokes("g ? w").await;
|
||||
cx.shared_state().await.assert_eq("ˇabc def");
|
||||
|
||||
cx.simulate_shared_keystrokes(".").await;
|
||||
cx.shared_state().await.assert_eq("ˇnop def");
|
||||
|
||||
cx.set_shared_state("abˇc def").await;
|
||||
cx.simulate_shared_keystrokes("g ? i w").await;
|
||||
cx.shared_state().await.assert_eq("ˇnop def");
|
||||
|
||||
cx.simulate_shared_keystrokes(".").await;
|
||||
cx.shared_state().await.assert_eq("ˇabc def");
|
||||
|
||||
cx.simulate_shared_keystrokes("g ? $").await;
|
||||
cx.shared_state().await.assert_eq("ˇnop qrs");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_change_rot13_object(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_shared_state("ˇabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes("g ? i w").await;
|
||||
cx.shared_state()
|
||||
.await
|
||||
.assert_eq("ˇnopqrstuvwxyzabcdefghijklmNOPQRSTUVWXYZABCDEFGHIJKLM");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_change_case_helix_mode(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
|
||||
// Explicit selection
|
||||
cx.set_state("«hello worldˇ»", Mode::HelixNormal);
|
||||
cx.simulate_keystrokes("~");
|
||||
cx.assert_state("«HELLO WORLDˇ»", Mode::HelixNormal);
|
||||
|
||||
// Cursor-only (empty) selection - switch case
|
||||
cx.set_state("The ˇquick brown", Mode::HelixNormal);
|
||||
cx.simulate_keystrokes("~");
|
||||
cx.assert_state("The ˇQuick brown", Mode::HelixNormal);
|
||||
cx.simulate_keystrokes("~");
|
||||
cx.assert_state("The ˇquick brown", Mode::HelixNormal);
|
||||
|
||||
// Cursor-only (empty) selection - switch to uppercase and lowercase explicitly
|
||||
cx.set_state("The ˇquick brown", Mode::HelixNormal);
|
||||
cx.simulate_keystrokes("alt-`");
|
||||
cx.assert_state("The ˇQuick brown", Mode::HelixNormal);
|
||||
cx.simulate_keystrokes("`");
|
||||
cx.assert_state("The ˇquick brown", Mode::HelixNormal);
|
||||
|
||||
// With `e` motion (which extends selection to end of word in Helix)
|
||||
cx.set_state("The ˇquick brown fox", Mode::HelixNormal);
|
||||
cx.simulate_keystrokes("e");
|
||||
cx.simulate_keystrokes("~");
|
||||
cx.assert_state("The «QUICKˇ» brown fox", Mode::HelixNormal);
|
||||
|
||||
// Cursor-only
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,763 @@
|
||||
use crate::{
|
||||
Vim,
|
||||
motion::{Motion, MotionKind},
|
||||
object::Object,
|
||||
state::Mode,
|
||||
};
|
||||
use collections::{HashMap, HashSet};
|
||||
use editor::{
|
||||
Bias, DisplayPoint,
|
||||
display_map::{DisplaySnapshot, ToDisplayPoint},
|
||||
};
|
||||
use gpui::{Context, Window};
|
||||
use language::{Point, Selection};
|
||||
use multi_buffer::MultiBufferRow;
|
||||
|
||||
impl Vim {
|
||||
pub fn delete_motion(
|
||||
&mut self,
|
||||
motion: Motion,
|
||||
times: Option<usize>,
|
||||
forced_motion: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.stop_recording(cx);
|
||||
self.update_editor(cx, |vim, editor, cx| {
|
||||
let text_layout_details = editor.text_layout_details(window, cx);
|
||||
editor.transact(window, cx, |editor, window, cx| {
|
||||
editor.set_clip_at_line_ends(false, cx);
|
||||
let mut original_columns: HashMap<_, _> = Default::default();
|
||||
let mut motion_kind = None;
|
||||
let mut ranges_to_copy = Vec::new();
|
||||
editor.change_selections(Default::default(), window, cx, |s| {
|
||||
s.move_with(&mut |map, selection| {
|
||||
let original_head = selection.head();
|
||||
original_columns.insert(selection.id, original_head.column());
|
||||
let kind = motion.expand_selection(
|
||||
map,
|
||||
selection,
|
||||
times,
|
||||
&text_layout_details,
|
||||
forced_motion,
|
||||
);
|
||||
ranges_to_copy
|
||||
.push(selection.start.to_point(map)..selection.end.to_point(map));
|
||||
|
||||
// When deleting line-wise, we always want to delete a newline.
|
||||
// If there is one after the current line, it goes; otherwise we
|
||||
// pick the one before.
|
||||
if kind == Some(MotionKind::Linewise) {
|
||||
let start = selection.start.to_point(map);
|
||||
let end = selection.end.to_point(map);
|
||||
if end.row < map.buffer_snapshot().max_point().row {
|
||||
selection.end = Point::new(end.row + 1, 0).to_display_point(map)
|
||||
} else if start.row > 0 {
|
||||
selection.start = Point::new(
|
||||
start.row - 1,
|
||||
map.buffer_snapshot()
|
||||
.line_len(MultiBufferRow(start.row - 1)),
|
||||
)
|
||||
.to_display_point(map)
|
||||
}
|
||||
}
|
||||
if let Some(kind) = kind {
|
||||
motion_kind.get_or_insert(kind);
|
||||
}
|
||||
});
|
||||
});
|
||||
let Some(kind) = motion_kind else { return };
|
||||
vim.copy_ranges(editor, kind, false, ranges_to_copy, window, cx);
|
||||
editor.delete_selections_with_linked_edits(window, cx);
|
||||
|
||||
// Fixup cursor position after the deletion
|
||||
editor.set_clip_at_line_ends(true, cx);
|
||||
editor.change_selections(Default::default(), window, cx, |s| {
|
||||
s.move_with(&mut |map, selection| {
|
||||
let mut cursor = selection.head();
|
||||
if kind.linewise()
|
||||
&& let Some(column) = original_columns.get(&selection.id)
|
||||
{
|
||||
*cursor.column_mut() = *column
|
||||
}
|
||||
cursor = map.clip_point(cursor, Bias::Left);
|
||||
selection.collapse_to(cursor, selection.goal)
|
||||
});
|
||||
});
|
||||
editor.refresh_edit_prediction(true, false, window, cx);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
pub fn delete_object(
|
||||
&mut self,
|
||||
object: Object,
|
||||
around: bool,
|
||||
times: Option<usize>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.stop_recording(cx);
|
||||
self.update_editor(cx, |vim, editor, cx| {
|
||||
editor.transact(window, cx, |editor, window, cx| {
|
||||
editor.set_clip_at_line_ends(false, cx);
|
||||
// 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(&mut |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 =
|
||||
|map: &DisplaySnapshot, selection: &mut Selection<DisplayPoint>| {
|
||||
let start = selection.start.to_offset(map, Bias::Left);
|
||||
if selection.start.row().0 > 0 {
|
||||
should_move_to_start.insert(selection.id);
|
||||
selection.start =
|
||||
(start - '\n'.len_utf8()).to_display_point(map);
|
||||
}
|
||||
};
|
||||
let range = selection.start.to_offset(map, Bias::Left)
|
||||
..selection.end.to_offset(map, Bias::Right);
|
||||
let contains_only_newlines = map
|
||||
.buffer_chars_at(range.start)
|
||||
.take_while(|(_, p)| p < &range.end)
|
||||
.all(|(char, _)| char == '\n')
|
||||
&& !offset_range.is_empty();
|
||||
let end_at_newline = map
|
||||
.buffer_chars_at(range.end)
|
||||
.next()
|
||||
.map(|(c, _)| c == '\n')
|
||||
.unwrap_or(false);
|
||||
|
||||
// If expanded range contains only newlines and
|
||||
// the object is around or sentence, expand to include a newline
|
||||
// at the end or start
|
||||
if (around || object == Object::Sentence) && contains_only_newlines {
|
||||
if end_at_newline {
|
||||
move_selection_end_to_next_line(map, selection);
|
||||
} else {
|
||||
move_selection_start_to_previous_line(map, selection);
|
||||
}
|
||||
}
|
||||
|
||||
// Does post-processing for the trailing newline and EOF
|
||||
// when not cancelled.
|
||||
let cancelled = around && selection.start == selection.end;
|
||||
if object == Object::Paragraph && !cancelled {
|
||||
// EOF check should be done before including a trailing newline.
|
||||
if ends_at_eof(map, selection) {
|
||||
move_selection_start_to_previous_line(map, selection);
|
||||
}
|
||||
|
||||
if end_at_newline {
|
||||
move_selection_end_to_next_line(map, selection);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
vim.copy_selections_content(editor, MotionKind::Exclusive, window, cx);
|
||||
editor.delete_selections_with_linked_edits(window, cx);
|
||||
|
||||
// Fixup cursor position after the deletion
|
||||
editor.set_clip_at_line_ends(true, cx);
|
||||
editor.change_selections(Default::default(), window, cx, |s| {
|
||||
s.move_with(&mut |map, selection| {
|
||||
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)
|
||||
});
|
||||
});
|
||||
editor.refresh_edit_prediction(true, false, window, cx);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn move_selection_end_to_next_line(map: &DisplaySnapshot, selection: &mut Selection<DisplayPoint>) {
|
||||
let end = selection.end.to_offset(map, Bias::Left);
|
||||
selection.end = (end + '\n'.len_utf8()).to_display_point(map);
|
||||
}
|
||||
|
||||
fn ends_at_eof(map: &DisplaySnapshot, selection: &mut Selection<DisplayPoint>) -> bool {
|
||||
selection.end.to_point(map) == map.buffer_snapshot().max_point()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use indoc::indoc;
|
||||
|
||||
use crate::{
|
||||
state::Mode,
|
||||
test::{NeovimBackedTestContext, VimTestContext},
|
||||
};
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_delete_h(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
cx.simulate("d h", "Teˇst").await.assert_matches();
|
||||
cx.simulate("d h", "Tˇest").await.assert_matches();
|
||||
cx.simulate("d h", "ˇTest").await.assert_matches();
|
||||
cx.simulate(
|
||||
"d h",
|
||||
indoc! {"
|
||||
Test
|
||||
ˇtest"},
|
||||
)
|
||||
.await
|
||||
.assert_matches();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_delete_l(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
cx.simulate("d l", "ˇTest").await.assert_matches();
|
||||
cx.simulate("d l", "Teˇst").await.assert_matches();
|
||||
cx.simulate("d l", "Tesˇt").await.assert_matches();
|
||||
cx.simulate(
|
||||
"d l",
|
||||
indoc! {"
|
||||
Tesˇt
|
||||
test"},
|
||||
)
|
||||
.await
|
||||
.assert_matches();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_delete_w(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
cx.simulate(
|
||||
"d w",
|
||||
indoc! {"
|
||||
Test tesˇt
|
||||
test"},
|
||||
)
|
||||
.await
|
||||
.assert_matches();
|
||||
|
||||
cx.simulate("d w", "Teˇst").await.assert_matches();
|
||||
cx.simulate("d w", "Tˇest test").await.assert_matches();
|
||||
cx.simulate(
|
||||
"d w",
|
||||
indoc! {"
|
||||
Test teˇst
|
||||
test"},
|
||||
)
|
||||
.await
|
||||
.assert_matches();
|
||||
cx.simulate(
|
||||
"d w",
|
||||
indoc! {"
|
||||
Test tesˇt
|
||||
test"},
|
||||
)
|
||||
.await
|
||||
.assert_matches();
|
||||
|
||||
cx.simulate(
|
||||
"d w",
|
||||
indoc! {"
|
||||
Test test
|
||||
ˇ
|
||||
test"},
|
||||
)
|
||||
.await
|
||||
.assert_matches();
|
||||
|
||||
cx.simulate("d shift-w", "Test teˇst-test test")
|
||||
.await
|
||||
.assert_matches();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_delete_next_word_end(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
cx.simulate("d e", "Teˇst Test\n").await.assert_matches();
|
||||
cx.simulate("d e", "Tˇest test\n").await.assert_matches();
|
||||
cx.simulate(
|
||||
"d e",
|
||||
indoc! {"
|
||||
Test teˇst
|
||||
test"},
|
||||
)
|
||||
.await
|
||||
.assert_matches();
|
||||
cx.simulate(
|
||||
"d e",
|
||||
indoc! {"
|
||||
Test tesˇt
|
||||
test"},
|
||||
)
|
||||
.await
|
||||
.assert_matches();
|
||||
|
||||
cx.simulate("d e", "Test teˇst-test test")
|
||||
.await
|
||||
.assert_matches();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_delete_b(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
cx.simulate("d b", "Teˇst Test").await.assert_matches();
|
||||
cx.simulate("d b", "Test ˇtest").await.assert_matches();
|
||||
cx.simulate("d b", "Test1 test2 ˇtest3")
|
||||
.await
|
||||
.assert_matches();
|
||||
cx.simulate(
|
||||
"d b",
|
||||
indoc! {"
|
||||
Test test
|
||||
ˇtest"},
|
||||
)
|
||||
.await
|
||||
.assert_matches();
|
||||
cx.simulate(
|
||||
"d b",
|
||||
indoc! {"
|
||||
Test test
|
||||
ˇ
|
||||
test"},
|
||||
)
|
||||
.await
|
||||
.assert_matches();
|
||||
|
||||
cx.simulate("d shift-b", "Test test-test ˇtest")
|
||||
.await
|
||||
.assert_matches();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_delete_end_of_line(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
cx.simulate(
|
||||
"d $",
|
||||
indoc! {"
|
||||
The qˇuick
|
||||
brown fox"},
|
||||
)
|
||||
.await
|
||||
.assert_matches();
|
||||
cx.simulate(
|
||||
"d $",
|
||||
indoc! {"
|
||||
The quick
|
||||
ˇ
|
||||
brown fox"},
|
||||
)
|
||||
.await
|
||||
.assert_matches();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_delete_end_of_paragraph(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
cx.simulate(
|
||||
"d }",
|
||||
indoc! {"
|
||||
ˇhello world.
|
||||
|
||||
hello world."},
|
||||
)
|
||||
.await
|
||||
.assert_matches();
|
||||
|
||||
cx.simulate(
|
||||
"d }",
|
||||
indoc! {"
|
||||
ˇhello world.
|
||||
hello world."},
|
||||
)
|
||||
.await
|
||||
.assert_matches();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_delete_0(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
cx.simulate(
|
||||
"d 0",
|
||||
indoc! {"
|
||||
The qˇuick
|
||||
brown fox"},
|
||||
)
|
||||
.await
|
||||
.assert_matches();
|
||||
cx.simulate(
|
||||
"d 0",
|
||||
indoc! {"
|
||||
The quick
|
||||
ˇ
|
||||
brown fox"},
|
||||
)
|
||||
.await
|
||||
.assert_matches();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_delete_k(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
cx.simulate(
|
||||
"d k",
|
||||
indoc! {"
|
||||
The quick
|
||||
brown ˇfox
|
||||
jumps over"},
|
||||
)
|
||||
.await
|
||||
.assert_matches();
|
||||
cx.simulate(
|
||||
"d k",
|
||||
indoc! {"
|
||||
The quick
|
||||
brown fox
|
||||
jumps ˇover"},
|
||||
)
|
||||
.await
|
||||
.assert_matches();
|
||||
cx.simulate(
|
||||
"d k",
|
||||
indoc! {"
|
||||
The qˇuick
|
||||
brown fox
|
||||
jumps over"},
|
||||
)
|
||||
.await
|
||||
.assert_matches();
|
||||
cx.simulate(
|
||||
"d k",
|
||||
indoc! {"
|
||||
ˇbrown fox
|
||||
jumps over"},
|
||||
)
|
||||
.await
|
||||
.assert_matches();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_delete_j(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
cx.simulate(
|
||||
"d j",
|
||||
indoc! {"
|
||||
The quick
|
||||
brown ˇfox
|
||||
jumps over"},
|
||||
)
|
||||
.await
|
||||
.assert_matches();
|
||||
cx.simulate(
|
||||
"d j",
|
||||
indoc! {"
|
||||
The quick
|
||||
brown fox
|
||||
jumps ˇover"},
|
||||
)
|
||||
.await
|
||||
.assert_matches();
|
||||
cx.simulate(
|
||||
"d j",
|
||||
indoc! {"
|
||||
The qˇuick
|
||||
brown fox
|
||||
jumps over"},
|
||||
)
|
||||
.await
|
||||
.assert_matches();
|
||||
cx.simulate(
|
||||
"d j",
|
||||
indoc! {"
|
||||
The quick
|
||||
brown fox
|
||||
ˇ"},
|
||||
)
|
||||
.await
|
||||
.assert_matches();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_delete_end_of_document(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
cx.simulate(
|
||||
"d shift-g",
|
||||
indoc! {"
|
||||
The quick
|
||||
brownˇ fox
|
||||
jumps over
|
||||
the lazy"},
|
||||
)
|
||||
.await
|
||||
.assert_matches();
|
||||
cx.simulate(
|
||||
"d shift-g",
|
||||
indoc! {"
|
||||
The quick
|
||||
brownˇ fox
|
||||
jumps over
|
||||
the lazy"},
|
||||
)
|
||||
.await
|
||||
.assert_matches();
|
||||
cx.simulate(
|
||||
"d shift-g",
|
||||
indoc! {"
|
||||
The quick
|
||||
brown fox
|
||||
jumps over
|
||||
the lˇazy"},
|
||||
)
|
||||
.await
|
||||
.assert_matches();
|
||||
cx.simulate(
|
||||
"d shift-g",
|
||||
indoc! {"
|
||||
The quick
|
||||
brown fox
|
||||
jumps over
|
||||
ˇ"},
|
||||
)
|
||||
.await
|
||||
.assert_matches();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_delete_to_line(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
cx.simulate(
|
||||
"d 3 shift-g",
|
||||
indoc! {"
|
||||
The quick
|
||||
brownˇ fox
|
||||
jumps over
|
||||
the lazy"},
|
||||
)
|
||||
.await
|
||||
.assert_matches();
|
||||
cx.simulate(
|
||||
"d 3 shift-g",
|
||||
indoc! {"
|
||||
The quick
|
||||
brown fox
|
||||
jumps over
|
||||
the lˇazy"},
|
||||
)
|
||||
.await
|
||||
.assert_matches();
|
||||
cx.simulate(
|
||||
"d 2 shift-g",
|
||||
indoc! {"
|
||||
The quick
|
||||
brown fox
|
||||
jumps over
|
||||
ˇ"},
|
||||
)
|
||||
.await
|
||||
.assert_matches();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_delete_gg(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
cx.simulate(
|
||||
"d g g",
|
||||
indoc! {"
|
||||
The quick
|
||||
brownˇ fox
|
||||
jumps over
|
||||
the lazy"},
|
||||
)
|
||||
.await
|
||||
.assert_matches();
|
||||
cx.simulate(
|
||||
"d g g",
|
||||
indoc! {"
|
||||
The quick
|
||||
brown fox
|
||||
jumps over
|
||||
the lˇazy"},
|
||||
)
|
||||
.await
|
||||
.assert_matches();
|
||||
cx.simulate(
|
||||
"d g g",
|
||||
indoc! {"
|
||||
The qˇuick
|
||||
brown fox
|
||||
jumps over
|
||||
the lazy"},
|
||||
)
|
||||
.await
|
||||
.assert_matches();
|
||||
cx.simulate(
|
||||
"d g g",
|
||||
indoc! {"
|
||||
ˇ
|
||||
brown fox
|
||||
jumps over
|
||||
the lazy"},
|
||||
)
|
||||
.await
|
||||
.assert_matches();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_cancel_delete_operator(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
cx.set_state(
|
||||
indoc! {"
|
||||
The quick brown
|
||||
fox juˇmps over
|
||||
the lazy dog"},
|
||||
Mode::Normal,
|
||||
);
|
||||
|
||||
// Canceling operator twice reverts to normal mode with no active operator
|
||||
cx.simulate_keystrokes("d escape k");
|
||||
assert_eq!(cx.active_operator(), None);
|
||||
assert_eq!(cx.mode(), Mode::Normal);
|
||||
cx.assert_editor_state(indoc! {"
|
||||
The quˇick brown
|
||||
fox jumps over
|
||||
the lazy dog"});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_unbound_command_cancels_pending_operator(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
cx.set_state(
|
||||
indoc! {"
|
||||
The quick brown
|
||||
fox juˇmps over
|
||||
the lazy dog"},
|
||||
Mode::Normal,
|
||||
);
|
||||
|
||||
// Canceling operator twice reverts to normal mode with no active operator
|
||||
cx.simulate_keystrokes("d y");
|
||||
assert_eq!(cx.active_operator(), None);
|
||||
assert_eq!(cx.mode(), Mode::Normal);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_delete_with_counts(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
cx.set_shared_state(indoc! {"
|
||||
The ˇquick brown
|
||||
fox jumps over
|
||||
the lazy dog"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes("d 2 d").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {"
|
||||
the ˇlazy dog"});
|
||||
|
||||
cx.set_shared_state(indoc! {"
|
||||
The ˇquick brown
|
||||
fox jumps over
|
||||
the lazy dog"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes("2 d d").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {"
|
||||
the ˇlazy dog"});
|
||||
|
||||
cx.set_shared_state(indoc! {"
|
||||
The ˇquick brown
|
||||
fox jumps over
|
||||
the moon,
|
||||
a star, and
|
||||
the lazy dog"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes("2 d 2 d").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {"
|
||||
the ˇlazy dog"});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_delete_to_adjacent_character(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
cx.simulate("d t x", "ˇax").await.assert_matches();
|
||||
cx.simulate("d t x", "aˇx").await.assert_matches();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_delete_sentence(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
// cx.simulate(
|
||||
// "d )",
|
||||
// indoc! {"
|
||||
// Fiˇrst. Second. Third.
|
||||
// Fourth.
|
||||
// "},
|
||||
// )
|
||||
// .await
|
||||
// .assert_matches();
|
||||
|
||||
// cx.simulate(
|
||||
// "d )",
|
||||
// indoc! {"
|
||||
// First. Secˇond. Third.
|
||||
// Fourth.
|
||||
// "},
|
||||
// )
|
||||
// .await
|
||||
// .assert_matches();
|
||||
|
||||
// // Two deletes
|
||||
// cx.simulate(
|
||||
// "d ) d )",
|
||||
// indoc! {"
|
||||
// First. Second. Thirˇd.
|
||||
// Fourth.
|
||||
// "},
|
||||
// )
|
||||
// .await
|
||||
// .assert_matches();
|
||||
|
||||
// Should delete whole line if done on first column
|
||||
cx.simulate(
|
||||
"d )",
|
||||
indoc! {"
|
||||
ˇFirst.
|
||||
Fourth.
|
||||
"},
|
||||
)
|
||||
.await
|
||||
.assert_matches();
|
||||
|
||||
// Backwards it should also delete the whole first line
|
||||
cx.simulate(
|
||||
"d (",
|
||||
indoc! {"
|
||||
First.
|
||||
ˇSecond.
|
||||
Fourth.
|
||||
"},
|
||||
)
|
||||
.await
|
||||
.assert_matches();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,913 @@
|
||||
use editor::{Editor, MultiBufferSnapshot, ToOffset, ToPoint};
|
||||
use gpui::{Action, Context, Window};
|
||||
use language::{Bias, Point};
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use std::ops::Range;
|
||||
|
||||
use crate::{Vim, state::Mode};
|
||||
|
||||
const BOOLEAN_PAIRS: &[(&str, &str)] = &[("true", "false"), ("yes", "no"), ("on", "off")];
|
||||
|
||||
/// Increments the number under the cursor or toggles boolean values.
|
||||
#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
|
||||
#[action(namespace = vim)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct Increment {
|
||||
#[serde(default)]
|
||||
step: bool,
|
||||
}
|
||||
|
||||
/// Decrements the number under the cursor or toggles boolean values.
|
||||
#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
|
||||
#[action(namespace = vim)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct Decrement {
|
||||
#[serde(default)]
|
||||
step: bool,
|
||||
}
|
||||
|
||||
pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
|
||||
Vim::action(editor, cx, |vim, action: &Increment, window, cx| {
|
||||
vim.record_current_action(cx);
|
||||
let count = Vim::take_count(cx).unwrap_or(1);
|
||||
Vim::take_forced_motion(cx);
|
||||
let step = if action.step { count as i32 } else { 0 };
|
||||
vim.increment(count as i64, step, window, cx)
|
||||
});
|
||||
Vim::action(editor, cx, |vim, action: &Decrement, window, cx| {
|
||||
vim.record_current_action(cx);
|
||||
let count = Vim::take_count(cx).unwrap_or(1);
|
||||
Vim::take_forced_motion(cx);
|
||||
let step = if action.step { -1 * (count as i32) } else { 0 };
|
||||
vim.increment(-(count as i64), step, window, cx)
|
||||
});
|
||||
}
|
||||
|
||||
impl Vim {
|
||||
fn increment(
|
||||
&mut self,
|
||||
mut delta: i64,
|
||||
step: i32,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.store_visual_marks(window, cx);
|
||||
self.update_editor(cx, |vim, editor, cx| {
|
||||
let mut edits = Vec::new();
|
||||
let mut new_anchors = Vec::new();
|
||||
|
||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||
for selection in editor.selections.all_adjusted(&editor.display_snapshot(cx)) {
|
||||
if !selection.is_empty()
|
||||
&& (vim.mode != Mode::VisualBlock || new_anchors.is_empty())
|
||||
{
|
||||
new_anchors.push((true, snapshot.anchor_before(selection.start)))
|
||||
}
|
||||
for row in selection.start.row..=selection.end.row {
|
||||
let start = if row == selection.start.row {
|
||||
selection.start
|
||||
} else {
|
||||
Point::new(row, 0)
|
||||
};
|
||||
let end = if row == selection.end.row {
|
||||
selection.end
|
||||
} else {
|
||||
Point::new(row, snapshot.line_len(multi_buffer::MultiBufferRow(row)))
|
||||
};
|
||||
|
||||
let find_result = if !selection.is_empty() {
|
||||
find_target(&snapshot, start, end, true)
|
||||
} else {
|
||||
find_target(&snapshot, start, end, false)
|
||||
};
|
||||
|
||||
if let Some((range, target, radix)) = find_result {
|
||||
let replace = match radix {
|
||||
10 => increment_decimal_string(&target, delta),
|
||||
16 => increment_hex_string(&target, delta),
|
||||
2 => increment_binary_string(&target, delta),
|
||||
0 => increment_toggle_string(&target),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
delta += step as i64;
|
||||
edits.push((range.clone(), replace));
|
||||
if selection.is_empty() {
|
||||
new_anchors.push((false, snapshot.anchor_after(range.end)))
|
||||
}
|
||||
} else if selection.is_empty() {
|
||||
new_anchors.push((true, snapshot.anchor_after(start)))
|
||||
}
|
||||
}
|
||||
}
|
||||
editor.transact(window, cx, |editor, window, cx| {
|
||||
editor.edit(edits, cx);
|
||||
|
||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||
editor.change_selections(Default::default(), window, cx, |s| {
|
||||
let mut new_ranges = Vec::new();
|
||||
for (visual, anchor) in new_anchors.iter() {
|
||||
let mut point = anchor.to_point(&snapshot);
|
||||
if !*visual && point.column > 0 {
|
||||
point.column -= 1;
|
||||
point = snapshot.clip_point(point, Bias::Left)
|
||||
}
|
||||
new_ranges.push(point..point);
|
||||
}
|
||||
s.select_ranges(new_ranges)
|
||||
})
|
||||
});
|
||||
});
|
||||
self.switch_mode(Mode::Normal, true, window, cx)
|
||||
}
|
||||
}
|
||||
|
||||
fn increment_decimal_string(num: &str, delta: i64) -> String {
|
||||
let (negative, delta, num_str) = match num.strip_prefix('-') {
|
||||
Some(n) => (true, -delta, n),
|
||||
None => (false, delta, num),
|
||||
};
|
||||
let num_length = num_str.len();
|
||||
let leading_zero = num_str.starts_with('0');
|
||||
|
||||
let (result, new_negative) = match u64::from_str_radix(num_str, 10) {
|
||||
Ok(value) => {
|
||||
let wrapped = value.wrapping_add_signed(delta);
|
||||
if delta < 0 && wrapped > value {
|
||||
((u64::MAX - wrapped).wrapping_add(1), !negative)
|
||||
} else if delta > 0 && wrapped < value {
|
||||
(u64::MAX - wrapped, !negative)
|
||||
} else {
|
||||
(wrapped, negative)
|
||||
}
|
||||
}
|
||||
Err(_) => (u64::MAX, negative),
|
||||
};
|
||||
|
||||
let formatted = format!("{}", result);
|
||||
let new_significant_digits = formatted.len();
|
||||
let padding = if leading_zero {
|
||||
num_length.saturating_sub(new_significant_digits)
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
if new_negative && result != 0 {
|
||||
format!("-{}{}", "0".repeat(padding), formatted)
|
||||
} else {
|
||||
format!("{}{}", "0".repeat(padding), formatted)
|
||||
}
|
||||
}
|
||||
|
||||
fn increment_hex_string(num: &str, delta: i64) -> String {
|
||||
let result = if let Ok(val) = u64::from_str_radix(num, 16) {
|
||||
val.wrapping_add_signed(delta)
|
||||
} else {
|
||||
u64::MAX
|
||||
};
|
||||
if should_use_lowercase(num) {
|
||||
format!("{:0width$x}", result, width = num.len())
|
||||
} else {
|
||||
format!("{:0width$X}", result, width = num.len())
|
||||
}
|
||||
}
|
||||
|
||||
fn should_use_lowercase(num: &str) -> bool {
|
||||
let mut use_uppercase = false;
|
||||
for ch in num.chars() {
|
||||
if ch.is_ascii_lowercase() {
|
||||
return true;
|
||||
}
|
||||
if ch.is_ascii_uppercase() {
|
||||
use_uppercase = true;
|
||||
}
|
||||
}
|
||||
!use_uppercase
|
||||
}
|
||||
|
||||
fn increment_binary_string(num: &str, delta: i64) -> String {
|
||||
let result = if let Ok(val) = u64::from_str_radix(num, 2) {
|
||||
val.wrapping_add_signed(delta)
|
||||
} else {
|
||||
u64::MAX
|
||||
};
|
||||
format!("{:0width$b}", result, width = num.len())
|
||||
}
|
||||
|
||||
fn find_target(
|
||||
snapshot: &MultiBufferSnapshot,
|
||||
start: Point,
|
||||
end: Point,
|
||||
need_range: bool,
|
||||
) -> Option<(Range<Point>, String, u32)> {
|
||||
let start_offset = start.to_offset(snapshot);
|
||||
let end_offset = end.to_offset(snapshot);
|
||||
|
||||
let mut first_char_is_num = snapshot
|
||||
.chars_at(start_offset)
|
||||
.next()
|
||||
.map_or(false, |ch| ch.is_ascii_hexdigit());
|
||||
let mut pre_char = String::new();
|
||||
|
||||
let next_offset = start_offset
|
||||
+ snapshot
|
||||
.chars_at(start_offset)
|
||||
.next()
|
||||
.map_or(0, |ch| ch.len_utf8());
|
||||
// Backward scan to find the start of the number, but stop at start_offset.
|
||||
// We track `offset` as the start position of the current character. Initialize
|
||||
// to `next_offset` and decrement at the start of each iteration so that `offset`
|
||||
// always lands on a valid character boundary (not in the middle of a multibyte char).
|
||||
let mut offset = next_offset;
|
||||
for ch in snapshot.reversed_chars_at(next_offset) {
|
||||
offset -= ch.len_utf8();
|
||||
|
||||
// Search boundaries
|
||||
if offset.0 == 0 || ch.is_whitespace() || (need_range && offset <= start_offset) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Avoid the influence of hexadecimal letters
|
||||
if first_char_is_num
|
||||
&& !ch.is_ascii_hexdigit()
|
||||
&& (ch != 'b' && ch != 'B')
|
||||
&& (ch != 'x' && ch != 'X')
|
||||
&& ch != '-'
|
||||
{
|
||||
// Used to determine if the initial character is a number.
|
||||
if is_numeric_string(&pre_char) {
|
||||
break;
|
||||
} else {
|
||||
first_char_is_num = false;
|
||||
}
|
||||
}
|
||||
|
||||
pre_char.insert(0, ch);
|
||||
}
|
||||
|
||||
// The backward scan breaks on whitespace, including newlines. Without this
|
||||
// skip, the forward scan would start on the newline and immediately break
|
||||
// (since it also breaks on newlines), finding nothing on the current line.
|
||||
if let Some(ch) = snapshot.chars_at(offset).next() {
|
||||
if ch == '\n' {
|
||||
offset += ch.len_utf8();
|
||||
}
|
||||
}
|
||||
|
||||
let mut begin = None;
|
||||
let mut end = None;
|
||||
let mut target = String::new();
|
||||
let mut radix = 10;
|
||||
let mut is_num = false;
|
||||
|
||||
let mut chars = snapshot.chars_at(offset).peekable();
|
||||
|
||||
while let Some(ch) = chars.next() {
|
||||
if need_range && offset >= end_offset {
|
||||
break; // stop at end of selection
|
||||
}
|
||||
|
||||
if target == "0"
|
||||
&& (ch == 'b' || ch == 'B')
|
||||
&& chars.peek().is_some()
|
||||
&& chars.peek().unwrap().is_digit(2)
|
||||
{
|
||||
radix = 2;
|
||||
begin = None;
|
||||
target = String::new();
|
||||
} else if target == "0"
|
||||
&& (ch == 'x' || ch == 'X')
|
||||
&& chars.peek().is_some()
|
||||
&& chars.peek().unwrap().is_ascii_hexdigit()
|
||||
{
|
||||
radix = 16;
|
||||
begin = None;
|
||||
target = String::new();
|
||||
} else if ch == '.' {
|
||||
// When the cursor is on a number followed by a dot and a non-digit
|
||||
// (`ˇ1. item`), terminate the match so the number is incrementable.
|
||||
// Without this, the dot unconditionally resets the scan and the
|
||||
// number is skipped. We only do this when the cursor is on the
|
||||
// number, when it's past (`111.ˇ.2`), we still reset so the forward
|
||||
// scan can find the number after the dots.
|
||||
let next_is_non_digit = chars.peek().map_or(true, |char| !char.is_digit(radix));
|
||||
let on_number =
|
||||
is_num && begin.is_some_and(|begin| begin >= start_offset || start_offset < offset);
|
||||
|
||||
if on_number && next_is_non_digit {
|
||||
end = Some(offset);
|
||||
break;
|
||||
}
|
||||
|
||||
is_num = false;
|
||||
begin = None;
|
||||
target = String::new();
|
||||
} else if ch.is_digit(radix)
|
||||
|| ((begin.is_none() || !is_num)
|
||||
&& ch == '-'
|
||||
&& chars.peek().is_some()
|
||||
&& chars.peek().unwrap().is_digit(radix))
|
||||
{
|
||||
if !is_num {
|
||||
is_num = true;
|
||||
begin = Some(offset);
|
||||
target = String::new();
|
||||
} else if begin.is_none() {
|
||||
begin = Some(offset);
|
||||
}
|
||||
target.push(ch);
|
||||
} else if ch.is_ascii_alphabetic() && !is_num {
|
||||
if begin.is_none() {
|
||||
begin = Some(offset);
|
||||
}
|
||||
target.push(ch);
|
||||
} else if begin.is_some() && (is_num || !is_num && is_toggle_word(&target)) {
|
||||
// End of matching
|
||||
end = Some(offset);
|
||||
break;
|
||||
} else if ch == '\n' {
|
||||
break;
|
||||
} else {
|
||||
// To match the next word
|
||||
is_num = false;
|
||||
begin = None;
|
||||
target = String::new();
|
||||
}
|
||||
|
||||
offset += ch.len_utf8();
|
||||
}
|
||||
|
||||
if let Some(begin) = begin
|
||||
&& (is_num || !is_num && is_toggle_word(&target))
|
||||
{
|
||||
if !is_num {
|
||||
radix = 0;
|
||||
}
|
||||
|
||||
let end = end.unwrap_or(offset);
|
||||
Some((
|
||||
begin.to_point(snapshot)..end.to_point(snapshot),
|
||||
target,
|
||||
radix,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn is_numeric_string(s: &str) -> bool {
|
||||
if s.is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let (_, rest) = if let Some(r) = s.strip_prefix('-') {
|
||||
(true, r)
|
||||
} else {
|
||||
(false, s)
|
||||
};
|
||||
|
||||
if rest.is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if let Some(digits) = rest.strip_prefix("0b").or_else(|| rest.strip_prefix("0B")) {
|
||||
digits.is_empty() || digits.chars().all(|c| c == '0' || c == '1')
|
||||
} else if let Some(digits) = rest.strip_prefix("0x").or_else(|| rest.strip_prefix("0X")) {
|
||||
digits.is_empty() || digits.chars().all(|c| c.is_ascii_hexdigit())
|
||||
} else {
|
||||
!rest.is_empty() && rest.chars().all(|c| c.is_ascii_digit())
|
||||
}
|
||||
}
|
||||
|
||||
fn is_toggle_word(word: &str) -> bool {
|
||||
let lower = word.to_lowercase();
|
||||
BOOLEAN_PAIRS
|
||||
.iter()
|
||||
.any(|(a, b)| lower == *a || lower == *b)
|
||||
}
|
||||
|
||||
fn increment_toggle_string(boolean: &str) -> String {
|
||||
let lower = boolean.to_lowercase();
|
||||
|
||||
let target = BOOLEAN_PAIRS
|
||||
.iter()
|
||||
.find_map(|(a, b)| {
|
||||
if lower == *a {
|
||||
Some(b)
|
||||
} else if lower == *b {
|
||||
Some(a)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.unwrap_or(&boolean);
|
||||
|
||||
if boolean.chars().all(|c| c.is_uppercase()) {
|
||||
// Upper case
|
||||
target.to_uppercase()
|
||||
} else if boolean.chars().next().unwrap_or(' ').is_uppercase() {
|
||||
// Title case
|
||||
let mut chars = target.chars();
|
||||
match chars.next() {
|
||||
None => String::new(),
|
||||
Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
|
||||
}
|
||||
} else {
|
||||
target.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use indoc::indoc;
|
||||
|
||||
use crate::{
|
||||
state::Mode,
|
||||
test::{NeovimBackedTestContext, VimTestContext},
|
||||
};
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_increment(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_shared_state(indoc! {"
|
||||
1ˇ2
|
||||
"})
|
||||
.await;
|
||||
|
||||
cx.simulate_shared_keystrokes("ctrl-a").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {"
|
||||
1ˇ3
|
||||
"});
|
||||
cx.simulate_shared_keystrokes("ctrl-x").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {"
|
||||
1ˇ2
|
||||
"});
|
||||
|
||||
cx.simulate_shared_keystrokes("9 9 ctrl-a").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {"
|
||||
11ˇ1
|
||||
"});
|
||||
cx.simulate_shared_keystrokes("1 1 1 ctrl-x").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {"
|
||||
ˇ0
|
||||
"});
|
||||
cx.simulate_shared_keystrokes(".").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {"
|
||||
-11ˇ1
|
||||
"});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_increment_with_dot(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_shared_state(indoc! {"
|
||||
1ˇ.2
|
||||
"})
|
||||
.await;
|
||||
|
||||
cx.simulate_shared_keystrokes("ctrl-a").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {"
|
||||
1.ˇ3
|
||||
"});
|
||||
cx.simulate_shared_keystrokes("ctrl-x").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {"
|
||||
1.ˇ2
|
||||
"});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_increment_with_leading_zeros(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_shared_state(indoc! {"
|
||||
000ˇ9
|
||||
"})
|
||||
.await;
|
||||
|
||||
cx.simulate_shared_keystrokes("ctrl-a").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {"
|
||||
001ˇ0
|
||||
"});
|
||||
cx.simulate_shared_keystrokes("2 ctrl-x").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {"
|
||||
000ˇ8
|
||||
"});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_increment_with_leading_zeros_and_zero(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_shared_state(indoc! {"
|
||||
01ˇ1
|
||||
"})
|
||||
.await;
|
||||
|
||||
cx.simulate_shared_keystrokes("ctrl-a").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {"
|
||||
01ˇ2
|
||||
"});
|
||||
cx.simulate_shared_keystrokes("1 2 ctrl-x").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {"
|
||||
00ˇ0
|
||||
"});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_increment_with_changing_leading_zeros(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_shared_state(indoc! {"
|
||||
099ˇ9
|
||||
"})
|
||||
.await;
|
||||
|
||||
cx.simulate_shared_keystrokes("ctrl-a").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {"
|
||||
100ˇ0
|
||||
"});
|
||||
cx.simulate_shared_keystrokes("2 ctrl-x").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {"
|
||||
99ˇ8
|
||||
"});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_increment_with_two_dots(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_shared_state(indoc! {"
|
||||
111.ˇ.2
|
||||
"})
|
||||
.await;
|
||||
|
||||
cx.simulate_shared_keystrokes("ctrl-a").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {"
|
||||
111..ˇ3
|
||||
"});
|
||||
cx.simulate_shared_keystrokes("ctrl-x").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {"
|
||||
111..ˇ2
|
||||
"});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_increment_sign_change(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
cx.set_shared_state(indoc! {"
|
||||
ˇ0
|
||||
"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes("ctrl-x").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {"
|
||||
-ˇ1
|
||||
"});
|
||||
cx.simulate_shared_keystrokes("2 ctrl-a").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {"
|
||||
ˇ1
|
||||
"});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_increment_sign_change_with_leading_zeros(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
cx.set_shared_state(indoc! {"
|
||||
00ˇ1
|
||||
"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes("ctrl-x").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {"
|
||||
00ˇ0
|
||||
"});
|
||||
cx.simulate_shared_keystrokes("ctrl-x").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {"
|
||||
-00ˇ1
|
||||
"});
|
||||
cx.simulate_shared_keystrokes("2 ctrl-a").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {"
|
||||
00ˇ1
|
||||
"});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_increment_bin_wrapping_and_padding(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
cx.set_shared_state(indoc! {"
|
||||
0b111111111111111111111111111111111111111111111111111111111111111111111ˇ1
|
||||
"})
|
||||
.await;
|
||||
|
||||
cx.simulate_shared_keystrokes("ctrl-a").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {"
|
||||
0b000000111111111111111111111111111111111111111111111111111111111111111ˇ1
|
||||
"});
|
||||
cx.simulate_shared_keystrokes("ctrl-a").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {"
|
||||
0b000000000000000000000000000000000000000000000000000000000000000000000ˇ0
|
||||
"});
|
||||
|
||||
cx.simulate_shared_keystrokes("ctrl-a").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {"
|
||||
0b000000000000000000000000000000000000000000000000000000000000000000000ˇ1
|
||||
"});
|
||||
cx.simulate_shared_keystrokes("2 ctrl-x").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {"
|
||||
0b000000111111111111111111111111111111111111111111111111111111111111111ˇ1
|
||||
"});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_increment_hex_wrapping_and_padding(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
cx.set_shared_state(indoc! {"
|
||||
0xfffffffffffffffffffˇf
|
||||
"})
|
||||
.await;
|
||||
|
||||
cx.simulate_shared_keystrokes("ctrl-a").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {"
|
||||
0x0000fffffffffffffffˇf
|
||||
"});
|
||||
cx.simulate_shared_keystrokes("ctrl-a").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {"
|
||||
0x0000000000000000000ˇ0
|
||||
"});
|
||||
cx.simulate_shared_keystrokes("ctrl-a").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {"
|
||||
0x0000000000000000000ˇ1
|
||||
"});
|
||||
cx.simulate_shared_keystrokes("2 ctrl-x").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {"
|
||||
0x0000fffffffffffffffˇf
|
||||
"});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_increment_wrapping(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
cx.set_shared_state(indoc! {"
|
||||
1844674407370955161ˇ9
|
||||
"})
|
||||
.await;
|
||||
|
||||
cx.simulate_shared_keystrokes("ctrl-a").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {"
|
||||
1844674407370955161ˇ5
|
||||
"});
|
||||
cx.simulate_shared_keystrokes("ctrl-a").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {"
|
||||
-1844674407370955161ˇ5
|
||||
"});
|
||||
cx.simulate_shared_keystrokes("ctrl-a").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {"
|
||||
-1844674407370955161ˇ4
|
||||
"});
|
||||
cx.simulate_shared_keystrokes("3 ctrl-x").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {"
|
||||
1844674407370955161ˇ4
|
||||
"});
|
||||
cx.simulate_shared_keystrokes("2 ctrl-a").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {"
|
||||
-1844674407370955161ˇ5
|
||||
"});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_increment_inline(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
cx.set_shared_state(indoc! {"
|
||||
inline0x3ˇ9u32
|
||||
"})
|
||||
.await;
|
||||
|
||||
cx.simulate_shared_keystrokes("ctrl-a").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {"
|
||||
inline0x3ˇau32
|
||||
"});
|
||||
cx.simulate_shared_keystrokes("ctrl-a").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {"
|
||||
inline0x3ˇbu32
|
||||
"});
|
||||
cx.simulate_shared_keystrokes("l l l ctrl-a").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {"
|
||||
inline0x3bu3ˇ3
|
||||
"});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_increment_hex_casing(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
cx.set_shared_state(indoc! {"
|
||||
0xFˇa
|
||||
"})
|
||||
.await;
|
||||
|
||||
cx.simulate_shared_keystrokes("ctrl-a").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {"
|
||||
0xfˇb
|
||||
"});
|
||||
cx.simulate_shared_keystrokes("ctrl-a").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {"
|
||||
0xfˇc
|
||||
"});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_increment_radix(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.simulate("ctrl-a", "ˇ total: 0xff")
|
||||
.await
|
||||
.assert_matches();
|
||||
cx.simulate("ctrl-x", "ˇ total: 0xff")
|
||||
.await
|
||||
.assert_matches();
|
||||
cx.simulate("ctrl-x", "ˇ total: 0xFF")
|
||||
.await
|
||||
.assert_matches();
|
||||
cx.simulate("ctrl-a", "(ˇ0b10f)").await.assert_matches();
|
||||
cx.simulate("ctrl-a", "ˇ-1").await.assert_matches();
|
||||
cx.simulate("ctrl-a", "-ˇ1").await.assert_matches();
|
||||
cx.simulate("ctrl-a", "banˇana").await.assert_matches();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_increment_steps(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_shared_state(indoc! {"
|
||||
ˇ1
|
||||
1
|
||||
1 2
|
||||
1
|
||||
1"})
|
||||
.await;
|
||||
|
||||
cx.simulate_shared_keystrokes("j v shift-g g ctrl-a").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {"
|
||||
1
|
||||
ˇ2
|
||||
3 2
|
||||
4
|
||||
5"});
|
||||
|
||||
cx.simulate_shared_keystrokes("shift-g ctrl-v g g").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {"
|
||||
«1ˇ»
|
||||
«2ˇ»
|
||||
«3ˇ» 2
|
||||
«4ˇ»
|
||||
«5ˇ»"});
|
||||
|
||||
cx.simulate_shared_keystrokes("g ctrl-x").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {"
|
||||
ˇ0
|
||||
0
|
||||
0 2
|
||||
0
|
||||
0"});
|
||||
cx.simulate_shared_keystrokes("v shift-g g ctrl-a").await;
|
||||
cx.simulate_shared_keystrokes("v shift-g 5 g ctrl-a").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {"
|
||||
ˇ6
|
||||
12
|
||||
18 2
|
||||
24
|
||||
30"});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_increment_toggle(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
|
||||
cx.set_state("let enabled = trˇue;", Mode::Normal);
|
||||
cx.simulate_keystrokes("ctrl-a");
|
||||
cx.assert_state("let enabled = falsˇe;", Mode::Normal);
|
||||
|
||||
cx.simulate_keystrokes("0 ctrl-a");
|
||||
cx.assert_state("let enabled = truˇe;", Mode::Normal);
|
||||
|
||||
cx.set_state(
|
||||
indoc! {"
|
||||
ˇlet enabled = TRUE;
|
||||
let enabled = TRUE;
|
||||
let enabled = TRUE;
|
||||
"},
|
||||
Mode::Normal,
|
||||
);
|
||||
cx.simulate_keystrokes("shift-v j j ctrl-x");
|
||||
cx.assert_state(
|
||||
indoc! {"
|
||||
ˇlet enabled = FALSE;
|
||||
let enabled = FALSE;
|
||||
let enabled = FALSE;
|
||||
"},
|
||||
Mode::Normal,
|
||||
);
|
||||
|
||||
cx.set_state(
|
||||
indoc! {"
|
||||
let enabled = ˇYes;
|
||||
let enabled = Yes;
|
||||
let enabled = Yes;
|
||||
"},
|
||||
Mode::Normal,
|
||||
);
|
||||
cx.simulate_keystrokes("ctrl-v j j e ctrl-x");
|
||||
cx.assert_state(
|
||||
indoc! {"
|
||||
let enabled = ˇNo;
|
||||
let enabled = No;
|
||||
let enabled = No;
|
||||
"},
|
||||
Mode::Normal,
|
||||
);
|
||||
|
||||
cx.set_state("ˇlet enabled = True;", Mode::Normal);
|
||||
cx.simulate_keystrokes("ctrl-a");
|
||||
cx.assert_state("let enabled = Falsˇe;", Mode::Normal);
|
||||
|
||||
cx.simulate_keystrokes("ctrl-a");
|
||||
cx.assert_state("let enabled = Truˇe;", Mode::Normal);
|
||||
|
||||
cx.set_state("let enabled = Onˇ;", Mode::Normal);
|
||||
cx.simulate_keystrokes("v b ctrl-a");
|
||||
cx.assert_state("let enabled = ˇOff;", Mode::Normal);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_increment_order(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
|
||||
cx.set_state("aaˇa false 1 2 3", Mode::Normal);
|
||||
cx.simulate_keystrokes("ctrl-a");
|
||||
cx.assert_state("aaa truˇe 1 2 3", Mode::Normal);
|
||||
|
||||
cx.set_state("aaˇa 1 false 2 3", Mode::Normal);
|
||||
cx.simulate_keystrokes("ctrl-a");
|
||||
cx.assert_state("aaa ˇ2 false 2 3", Mode::Normal);
|
||||
|
||||
cx.set_state("trueˇ 1 2 3", Mode::Normal);
|
||||
cx.simulate_keystrokes("ctrl-a");
|
||||
cx.assert_state("true ˇ2 2 3", Mode::Normal);
|
||||
|
||||
cx.set_state("falseˇ", Mode::Normal);
|
||||
cx.simulate_keystrokes("ctrl-a");
|
||||
cx.assert_state("truˇe", Mode::Normal);
|
||||
|
||||
cx.set_state("⚡️ˇ⚡️", Mode::Normal);
|
||||
cx.simulate_keystrokes("ctrl-a");
|
||||
cx.assert_state("⚡️ˇ⚡️", Mode::Normal);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_increment_visual_partial_number(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_shared_state("ˇ123").await;
|
||||
cx.simulate_shared_keystrokes("v l ctrl-a").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {"ˇ133"});
|
||||
cx.simulate_shared_keystrokes("l v l ctrl-a").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {"1ˇ34"});
|
||||
cx.simulate_shared_keystrokes("shift-v y p p ctrl-v k k l ctrl-a")
|
||||
.await;
|
||||
cx.shared_state().await.assert_eq(indoc! {"ˇ144\n144\n144"});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_increment_markdown_list_markers_multiline(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_shared_state("# Title\nˇ1. item\n2. item\n3. item")
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes("ctrl-a").await;
|
||||
cx.shared_state()
|
||||
.await
|
||||
.assert_eq("# Title\nˇ2. item\n2. item\n3. item");
|
||||
cx.simulate_shared_keystrokes("j").await;
|
||||
cx.shared_state()
|
||||
.await
|
||||
.assert_eq("# Title\n2. item\nˇ2. item\n3. item");
|
||||
cx.simulate_shared_keystrokes("ctrl-a").await;
|
||||
cx.shared_state()
|
||||
.await
|
||||
.assert_eq("# Title\n2. item\nˇ3. item\n3. item");
|
||||
cx.simulate_shared_keystrokes("ctrl-x").await;
|
||||
cx.shared_state()
|
||||
.await
|
||||
.assert_eq("# Title\n2. item\nˇ2. item\n3. item");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_increment_with_multibyte_characters(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
|
||||
// Test cursor after a multibyte character - this would panic before the fix
|
||||
// because the backward scan would land in the middle of the Korean character
|
||||
cx.set_state("지ˇ1", Mode::Normal);
|
||||
cx.simulate_keystrokes("ctrl-a");
|
||||
cx.assert_state("지ˇ2", Mode::Normal);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,465 @@
|
||||
use std::{ops::Range, path::Path, sync::Arc};
|
||||
|
||||
use editor::{
|
||||
Anchor, Bias, DisplayPoint, Editor, MultiBuffer,
|
||||
display_map::{DisplaySnapshot, ToDisplayPoint},
|
||||
movement,
|
||||
};
|
||||
use gpui::{Context, Entity, EntityId, TaskExt, UpdateGlobal, Window};
|
||||
use language::SelectionGoal;
|
||||
use text::Point;
|
||||
use ui::App;
|
||||
use workspace::OpenOptions;
|
||||
|
||||
use crate::{
|
||||
Vim,
|
||||
motion::{self, Motion},
|
||||
state::{Mark, Mode, VimGlobals},
|
||||
};
|
||||
|
||||
impl Vim {
|
||||
pub fn create_mark(&mut self, text: Arc<str>, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.update_editor(cx, |vim, editor, cx| {
|
||||
let anchors = editor
|
||||
.selections
|
||||
.disjoint_anchors_arc()
|
||||
.iter()
|
||||
.map(|s| s.head())
|
||||
.collect::<Vec<_>>();
|
||||
vim.set_mark(text.to_string(), anchors, editor.buffer(), window, cx);
|
||||
});
|
||||
self.clear_operator(window, cx);
|
||||
}
|
||||
|
||||
// When handling an action, you must create visual marks if you will switch to normal
|
||||
// mode without the default selection behavior.
|
||||
pub(crate) fn store_visual_marks(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.mode.is_visual() {
|
||||
self.create_visual_marks(self.mode, window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn create_visual_marks(
|
||||
&mut self,
|
||||
mode: Mode,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let mut starts = vec![];
|
||||
let mut ends = vec![];
|
||||
let mut reversed = vec![];
|
||||
|
||||
self.update_editor(cx, |vim, editor, cx| {
|
||||
let display_map = editor.display_snapshot(cx);
|
||||
let selections = editor.selections.all_display(&display_map);
|
||||
for selection in selections {
|
||||
let end = movement::saturating_left(&display_map, selection.end);
|
||||
ends.push(
|
||||
display_map
|
||||
.buffer_snapshot()
|
||||
.anchor_before(end.to_offset(&display_map, Bias::Left)),
|
||||
);
|
||||
starts.push(
|
||||
display_map
|
||||
.buffer_snapshot()
|
||||
.anchor_before(selection.start.to_offset(&display_map, Bias::Left)),
|
||||
);
|
||||
reversed.push(selection.reversed)
|
||||
}
|
||||
vim.set_mark("<".to_string(), starts, editor.buffer(), window, cx);
|
||||
vim.set_mark(">".to_string(), ends, editor.buffer(), window, cx);
|
||||
});
|
||||
|
||||
self.stored_visual_mode.replace((mode, reversed));
|
||||
}
|
||||
|
||||
fn open_buffer_mark(
|
||||
&mut self,
|
||||
line: bool,
|
||||
entity_id: EntityId,
|
||||
anchors: Vec<Anchor>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(workspace) = self.workspace(window, cx) else {
|
||||
return;
|
||||
};
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
let item = workspace.items(cx).find(|item| {
|
||||
item.act_as::<Editor>(cx)
|
||||
.is_some_and(|editor| editor.read(cx).buffer().entity_id() == entity_id)
|
||||
});
|
||||
let Some(item) = item.cloned() else {
|
||||
return;
|
||||
};
|
||||
if let Some(pane) = workspace.pane_for(item.as_ref()) {
|
||||
pane.update(cx, |pane, cx| {
|
||||
if let Some(index) = pane.index_for_item(item.as_ref()) {
|
||||
pane.activate_item(index, true, true, window, cx);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
item.act_as::<Editor>(cx).unwrap().update(cx, |editor, cx| {
|
||||
let map = editor.snapshot(window, cx);
|
||||
let mut ranges: Vec<Range<Anchor>> = Vec::new();
|
||||
for mut anchor in anchors {
|
||||
if line {
|
||||
let mut point = anchor.to_display_point(&map.display_snapshot);
|
||||
point = motion::first_non_whitespace(&map.display_snapshot, false, point);
|
||||
anchor = map
|
||||
.display_snapshot
|
||||
.buffer_snapshot()
|
||||
.anchor_before(point.to_point(&map.display_snapshot));
|
||||
}
|
||||
|
||||
if ranges.last() != Some(&(anchor..anchor)) {
|
||||
ranges.push(anchor..anchor);
|
||||
}
|
||||
}
|
||||
|
||||
editor.change_selections(Default::default(), window, cx, |s| {
|
||||
s.select_anchor_ranges(ranges)
|
||||
});
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
fn open_path_mark(
|
||||
&mut self,
|
||||
line: bool,
|
||||
path: Arc<Path>,
|
||||
points: Vec<Point>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(workspace) = self.workspace(window, cx) else {
|
||||
return;
|
||||
};
|
||||
let task = workspace.update(cx, |workspace, cx| {
|
||||
workspace.open_abs_path(
|
||||
path.to_path_buf(),
|
||||
OpenOptions {
|
||||
visible: Some(workspace::OpenVisible::All),
|
||||
focus: Some(true),
|
||||
..Default::default()
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let editor = task.await?;
|
||||
this.update_in(cx, |_, window, cx| {
|
||||
if let Some(editor) = editor.act_as::<Editor>(cx) {
|
||||
editor.update(cx, |editor, cx| {
|
||||
let map = editor.snapshot(window, cx);
|
||||
let points: Vec<_> = points
|
||||
.into_iter()
|
||||
.map(|p| {
|
||||
if line {
|
||||
let point = p.to_display_point(&map.display_snapshot);
|
||||
motion::first_non_whitespace(
|
||||
&map.display_snapshot,
|
||||
false,
|
||||
point,
|
||||
)
|
||||
.to_point(&map.display_snapshot)
|
||||
} else {
|
||||
p
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
editor.change_selections(Default::default(), window, cx, |s| {
|
||||
s.select_ranges(points.into_iter().map(|p| p..p))
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
pub fn jump(
|
||||
&mut self,
|
||||
text: Arc<str>,
|
||||
line: bool,
|
||||
should_pop_operator: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if should_pop_operator {
|
||||
self.pop_operator(window, cx);
|
||||
}
|
||||
let mark = self
|
||||
.update_editor(cx, |vim, editor, cx| {
|
||||
vim.get_mark(&text, editor, window, cx)
|
||||
})
|
||||
.flatten();
|
||||
let anchors = match mark {
|
||||
None => None,
|
||||
Some(Mark::Local(anchors)) => Some(anchors),
|
||||
Some(Mark::Buffer(entity_id, anchors)) => {
|
||||
self.open_buffer_mark(line, entity_id, anchors, window, cx);
|
||||
return;
|
||||
}
|
||||
Some(Mark::Path(path, points)) => {
|
||||
self.open_path_mark(line, path, points, window, cx);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let Some(mut anchors) = anchors else { return };
|
||||
|
||||
self.update_editor(cx, |_, editor, cx| {
|
||||
editor.create_nav_history_entry(cx);
|
||||
});
|
||||
let is_active_operator = self.active_operator().is_some();
|
||||
if is_active_operator {
|
||||
if let Some(anchor) = anchors.last() {
|
||||
self.motion(
|
||||
Motion::Jump {
|
||||
anchor: *anchor,
|
||||
line,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// Save the last anchor so as to jump to it later.
|
||||
let anchor: Option<Anchor> = anchors.last_mut().map(|anchor| *anchor);
|
||||
let should_jump = self.mode == Mode::Visual
|
||||
|| self.mode == Mode::VisualLine
|
||||
|| self.mode == Mode::VisualBlock;
|
||||
|
||||
self.update_editor(cx, |_, editor, cx| {
|
||||
let map = editor.snapshot(window, cx);
|
||||
let mut ranges: Vec<Range<Anchor>> = Vec::new();
|
||||
for mut anchor in anchors {
|
||||
if line {
|
||||
let mut point = anchor.to_display_point(&map.display_snapshot);
|
||||
point = motion::first_non_whitespace(&map.display_snapshot, false, point);
|
||||
anchor = map
|
||||
.display_snapshot
|
||||
.buffer_snapshot()
|
||||
.anchor_before(point.to_point(&map.display_snapshot));
|
||||
}
|
||||
|
||||
if ranges.last() != Some(&(anchor..anchor)) {
|
||||
ranges.push(anchor..anchor);
|
||||
}
|
||||
}
|
||||
|
||||
if !should_jump && !ranges.is_empty() {
|
||||
editor.change_selections(Default::default(), window, cx, |s| {
|
||||
s.select_anchor_ranges(ranges)
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if should_jump && let Some(anchor) = anchor {
|
||||
self.motion(Motion::Jump { anchor, line }, window, cx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_mark(
|
||||
&mut self,
|
||||
mut name: String,
|
||||
anchors: Vec<Anchor>,
|
||||
buffer_entity: &Entity<MultiBuffer>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let Some(workspace) = self.workspace(window, cx) else {
|
||||
return;
|
||||
};
|
||||
if name == "`" {
|
||||
name = "'".to_string();
|
||||
}
|
||||
if matches!(&name[..], "-" | " ") {
|
||||
// Not allowed marks
|
||||
return;
|
||||
}
|
||||
let entity_id = workspace.entity_id();
|
||||
Vim::update_globals(cx, |vim_globals, cx| {
|
||||
let Some(marks_state) = vim_globals.marks.get(&entity_id) else {
|
||||
return;
|
||||
};
|
||||
marks_state.update(cx, |ms, cx| {
|
||||
ms.set_mark(name.clone(), buffer_entity, anchors, cx);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
pub fn get_mark(
|
||||
&self,
|
||||
mut name: &str,
|
||||
editor: &mut Editor,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Option<Mark> {
|
||||
if name == "`" {
|
||||
name = "'";
|
||||
}
|
||||
if matches!(name, "{" | "}" | "(" | ")") {
|
||||
let display_map = editor.display_snapshot(cx);
|
||||
let selections = editor.selections.all_display(&display_map);
|
||||
let anchors = selections
|
||||
.into_iter()
|
||||
.map(|selection| {
|
||||
let point = match name {
|
||||
"{" => movement::start_of_paragraph(&display_map, selection.head(), 1),
|
||||
"}" => movement::end_of_paragraph(&display_map, selection.head(), 1),
|
||||
"(" => motion::sentence_backwards(&display_map, selection.head(), 1),
|
||||
")" => motion::sentence_forwards(&display_map, selection.head(), 1),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
display_map
|
||||
.buffer_snapshot()
|
||||
.anchor_before(point.to_offset(&display_map, Bias::Left))
|
||||
})
|
||||
.collect::<Vec<Anchor>>();
|
||||
return Some(Mark::Local(anchors));
|
||||
}
|
||||
VimGlobals::update_global(cx, |globals, cx| {
|
||||
let workspace_id = self.workspace(window, cx)?.entity_id();
|
||||
globals
|
||||
.marks
|
||||
.get_mut(&workspace_id)?
|
||||
.update(cx, |ms, cx| ms.get_mark(name, editor.buffer(), cx))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn delete_mark(
|
||||
&self,
|
||||
name: String,
|
||||
editor: &mut Editor,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let Some(workspace) = self.workspace(window, cx) else {
|
||||
return;
|
||||
};
|
||||
if name == "`" || name == "'" {
|
||||
return;
|
||||
}
|
||||
let entity_id = workspace.entity_id();
|
||||
Vim::update_globals(cx, |vim_globals, cx| {
|
||||
let Some(marks_state) = vim_globals.marks.get(&entity_id) else {
|
||||
return;
|
||||
};
|
||||
marks_state.update(cx, |ms, cx| {
|
||||
ms.delete_mark(name.clone(), editor.buffer(), cx);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn jump_motion(
|
||||
map: &DisplaySnapshot,
|
||||
anchor: Anchor,
|
||||
line: bool,
|
||||
) -> (DisplayPoint, SelectionGoal) {
|
||||
let mut point = anchor.to_display_point(map);
|
||||
if line {
|
||||
point = motion::first_non_whitespace(map, false, point)
|
||||
}
|
||||
|
||||
(point, SelectionGoal::None)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::test::{NeovimBackedTestContext, VimTestContext};
|
||||
use editor::Editor;
|
||||
use gpui::TestAppContext;
|
||||
use std::path::Path;
|
||||
use util::path;
|
||||
use workspace::{CloseActiveItem, OpenOptions};
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_quote_mark(cx: &mut TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_shared_state("ˇHello, world!").await;
|
||||
cx.simulate_shared_keystrokes("w m o").await;
|
||||
cx.shared_state().await.assert_eq("Helloˇ, world!");
|
||||
cx.simulate_shared_keystrokes("$ ` o").await;
|
||||
cx.shared_state().await.assert_eq("Helloˇ, world!");
|
||||
cx.simulate_shared_keystrokes("` `").await;
|
||||
cx.shared_state().await.assert_eq("Hello, worldˇ!");
|
||||
cx.simulate_shared_keystrokes("` `").await;
|
||||
cx.shared_state().await.assert_eq("Helloˇ, world!");
|
||||
cx.simulate_shared_keystrokes("$ m '").await;
|
||||
cx.shared_state().await.assert_eq("Hello, worldˇ!");
|
||||
cx.simulate_shared_keystrokes("^ ` `").await;
|
||||
cx.shared_state().await.assert_eq("Hello, worldˇ!");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_global_mark_overwrite(cx: &mut TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
|
||||
let path = Path::new(path!("/first.rs"));
|
||||
let fs = cx.workspace(|workspace, _, cx| workspace.project().read(cx).fs().clone());
|
||||
fs.as_fake().insert_file(path, "one".into()).await;
|
||||
let path = Path::new(path!("/second.rs"));
|
||||
fs.as_fake().insert_file(path, "two".into()).await;
|
||||
|
||||
let _ = cx
|
||||
.workspace(|workspace, window, cx| {
|
||||
workspace.open_abs_path(
|
||||
path!("/first.rs").into(),
|
||||
OpenOptions::default(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await;
|
||||
|
||||
cx.simulate_keystrokes("m A");
|
||||
|
||||
let _ = cx
|
||||
.workspace(|workspace, window, cx| {
|
||||
workspace.open_abs_path(
|
||||
path!("/second.rs").into(),
|
||||
OpenOptions::default(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await;
|
||||
|
||||
cx.simulate_keystrokes("m A");
|
||||
|
||||
let _ = cx
|
||||
.workspace(|workspace, window, cx| {
|
||||
workspace.active_pane().update(cx, |pane, cx| {
|
||||
pane.close_active_item(&CloseActiveItem::default(), window, cx)
|
||||
})
|
||||
})
|
||||
.await;
|
||||
|
||||
cx.simulate_keystrokes("m B");
|
||||
|
||||
cx.simulate_keystrokes("' A");
|
||||
|
||||
cx.workspace(|workspace, _, cx| {
|
||||
let active_editor = workspace.active_item_as::<Editor>(cx).unwrap();
|
||||
|
||||
let buffer = active_editor
|
||||
.read(cx)
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.as_singleton()
|
||||
.unwrap();
|
||||
|
||||
let file = buffer.read(cx).file().unwrap();
|
||||
let file_path = file.as_local().unwrap().abs_path(cx);
|
||||
|
||||
assert_eq!(file_path.to_str().unwrap(), path!("/second.rs"));
|
||||
})
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,583 @@
|
||||
use crate::{Vim, state::Mode};
|
||||
use editor::{
|
||||
DisplayPoint, Editor, EditorSettings, SelectionEffects, display_map::DisplayRow,
|
||||
scroll::ScrollAmount,
|
||||
};
|
||||
use gpui::{Context, Window, actions};
|
||||
use language::Bias;
|
||||
use settings::Settings;
|
||||
use text::SelectionGoal;
|
||||
|
||||
actions!(
|
||||
vim,
|
||||
[
|
||||
/// Scrolls up by one line.
|
||||
LineUp,
|
||||
/// Scrolls down by one line.
|
||||
LineDown,
|
||||
/// Scrolls right by one column.
|
||||
ColumnRight,
|
||||
/// Scrolls left by one column.
|
||||
ColumnLeft,
|
||||
/// Scrolls up by half a page.
|
||||
ScrollUp,
|
||||
/// Scrolls down by half a page.
|
||||
ScrollDown,
|
||||
/// Scrolls up by one page.
|
||||
PageUp,
|
||||
/// Scrolls down by one page.
|
||||
PageDown,
|
||||
/// Scrolls right by half a page's width.
|
||||
HalfPageRight,
|
||||
/// Scrolls left by half a page's width.
|
||||
HalfPageLeft,
|
||||
]
|
||||
);
|
||||
|
||||
pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
|
||||
Vim::action(editor, cx, |vim, _: &LineDown, window, cx| {
|
||||
vim.scroll(false, window, cx, |c| ScrollAmount::Line(c.unwrap_or(1.)))
|
||||
});
|
||||
Vim::action(editor, cx, |vim, _: &LineUp, window, cx| {
|
||||
vim.scroll(false, window, cx, |c| ScrollAmount::Line(-c.unwrap_or(1.)))
|
||||
});
|
||||
Vim::action(editor, cx, |vim, _: &ColumnRight, window, cx| {
|
||||
vim.scroll(false, window, cx, |c| ScrollAmount::Column(c.unwrap_or(1.)))
|
||||
});
|
||||
Vim::action(editor, cx, |vim, _: &ColumnLeft, window, cx| {
|
||||
vim.scroll(false, window, cx, |c| {
|
||||
ScrollAmount::Column(-c.unwrap_or(1.))
|
||||
})
|
||||
});
|
||||
Vim::action(editor, cx, |vim, _: &PageDown, window, cx| {
|
||||
vim.scroll(false, window, cx, |c| ScrollAmount::Page(c.unwrap_or(1.)))
|
||||
});
|
||||
Vim::action(editor, cx, |vim, _: &PageUp, window, cx| {
|
||||
vim.scroll(false, window, cx, |c| ScrollAmount::Page(-c.unwrap_or(1.)))
|
||||
});
|
||||
Vim::action(editor, cx, |vim, _: &HalfPageRight, window, cx| {
|
||||
vim.scroll(false, window, cx, |c| {
|
||||
ScrollAmount::PageWidth(c.unwrap_or(0.5))
|
||||
})
|
||||
});
|
||||
Vim::action(editor, cx, |vim, _: &HalfPageLeft, window, cx| {
|
||||
vim.scroll(false, window, cx, |c| {
|
||||
ScrollAmount::PageWidth(-c.unwrap_or(0.5))
|
||||
})
|
||||
});
|
||||
Vim::action(editor, cx, |vim, _: &ScrollDown, window, cx| {
|
||||
vim.scroll(true, window, cx, |c| {
|
||||
if let Some(c) = c {
|
||||
ScrollAmount::Line(c)
|
||||
} else {
|
||||
ScrollAmount::Page(0.5)
|
||||
}
|
||||
})
|
||||
});
|
||||
Vim::action(editor, cx, |vim, _: &ScrollUp, window, cx| {
|
||||
vim.scroll(true, window, cx, |c| {
|
||||
if let Some(c) = c {
|
||||
ScrollAmount::Line(-c)
|
||||
} else {
|
||||
ScrollAmount::Page(-0.5)
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
impl Vim {
|
||||
fn scroll(
|
||||
&mut self,
|
||||
preserve_cursor_position: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
by: fn(c: Option<f32>) -> ScrollAmount,
|
||||
) {
|
||||
let amount = by(Vim::take_count(cx).map(|c| c as f32));
|
||||
Vim::take_forced_motion(cx);
|
||||
self.exit_temporary_normal(window, cx);
|
||||
self.scroll_editor(preserve_cursor_position, amount, window, cx);
|
||||
}
|
||||
|
||||
fn scroll_editor(
|
||||
&mut self,
|
||||
preserve_cursor_position: bool,
|
||||
amount: ScrollAmount,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Vim>,
|
||||
) {
|
||||
self.update_editor(cx, |vim, editor, cx| {
|
||||
let should_move_cursor = editor.newest_selection_on_screen(cx).is_eq();
|
||||
let display_snapshot = editor.display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||
let old_top = editor.scroll_top_display_point(&display_snapshot, cx);
|
||||
|
||||
if editor.scroll_hover(amount, window, cx) {
|
||||
return;
|
||||
}
|
||||
|
||||
let full_page_up = amount.is_full_page() && amount.direction().is_upwards();
|
||||
let amount = match (amount.is_full_page(), editor.visible_line_count()) {
|
||||
(true, Some(visible_line_count)) => {
|
||||
if amount.direction().is_upwards() {
|
||||
ScrollAmount::Line((amount.lines(visible_line_count) + 1.0) as f32)
|
||||
} else {
|
||||
ScrollAmount::Line((amount.lines(visible_line_count) - 1.0) as f32)
|
||||
}
|
||||
}
|
||||
_ => amount,
|
||||
};
|
||||
|
||||
editor.scroll_screen(&amount, window, cx);
|
||||
if !should_move_cursor {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(visible_line_count) = editor.visible_line_count() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(visible_column_count) = editor.visible_column_count() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let display_snapshot = editor.display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||
let top = editor.scroll_top_display_point(&display_snapshot, cx);
|
||||
let vertical_scroll_margin = EditorSettings::get_global(cx).vertical_scroll_margin;
|
||||
|
||||
let mut move_cursor = |map: &editor::display_map::DisplaySnapshot,
|
||||
mut head: DisplayPoint,
|
||||
goal: SelectionGoal| {
|
||||
// TODO: Improve the logic and function calls below to be dependent on
|
||||
// the `amount`. If the amount is vertical, we don't care about
|
||||
// columns, while if it's horizontal, we don't care about rows,
|
||||
// so we don't need to calculate both and deal with logic for
|
||||
// both.
|
||||
let max_point = map.max_point();
|
||||
let starting_column = head.column();
|
||||
|
||||
let vertical_scroll_margin =
|
||||
(vertical_scroll_margin as u32).min(visible_line_count as u32 / 2);
|
||||
|
||||
if preserve_cursor_position {
|
||||
let new_row =
|
||||
if old_top.row() == top.row() {
|
||||
DisplayRow(
|
||||
head.row()
|
||||
.0
|
||||
.saturating_add_signed(amount.lines(visible_line_count) as i32),
|
||||
)
|
||||
} else {
|
||||
DisplayRow(top.row().0.saturating_add_signed(
|
||||
head.row().0 as i32 - old_top.row().0 as i32,
|
||||
))
|
||||
};
|
||||
head = map.clip_point(DisplayPoint::new(new_row, head.column()), Bias::Left)
|
||||
}
|
||||
|
||||
let min_row = if top.row().0 == 0 {
|
||||
DisplayRow(0)
|
||||
} else {
|
||||
DisplayRow(top.row().0 + vertical_scroll_margin)
|
||||
};
|
||||
|
||||
let max_visible_row = top.row().0.saturating_add(
|
||||
(visible_line_count as u32).saturating_sub(1 + vertical_scroll_margin),
|
||||
);
|
||||
// scroll off the end.
|
||||
let max_row = if top.row().0 + visible_line_count as u32 >= max_point.row().0 {
|
||||
max_point.row()
|
||||
} else {
|
||||
DisplayRow(
|
||||
(top.row().0 + visible_line_count as u32)
|
||||
.saturating_sub(1 + vertical_scroll_margin),
|
||||
)
|
||||
};
|
||||
|
||||
let new_row = if full_page_up {
|
||||
// Special-casing ctrl-b/page-up, which is special-cased by Vim, it seems
|
||||
// to always put the cursor on the last line of the page, even if the cursor
|
||||
// was before that.
|
||||
DisplayRow(max_visible_row)
|
||||
} else if head.row() < min_row {
|
||||
min_row
|
||||
} else if head.row() > max_row {
|
||||
max_row
|
||||
} else {
|
||||
head.row()
|
||||
};
|
||||
|
||||
// The minimum column position that the cursor position can be
|
||||
// at is either the scroll manager's anchor column, which is the
|
||||
// left-most column in the visible area, or the scroll manager's
|
||||
// old anchor column, in case the cursor position is being
|
||||
// preserved. This is necessary for motions like `ctrl-d` in
|
||||
// case there's not enough content to scroll half page down, in
|
||||
// which case the scroll manager's anchor column will be the
|
||||
// maximum column for the current line, so the minimum column
|
||||
// would end up being the same as the maximum column.
|
||||
let min_column = match preserve_cursor_position {
|
||||
true => old_top.column(),
|
||||
false => top.column(),
|
||||
};
|
||||
|
||||
// As for the maximum column position, that should be either the
|
||||
// right-most column in the visible area, which we can easily
|
||||
// calculate by adding the visible column count to the minimum
|
||||
// column position, or the right-most column in the current
|
||||
// line, seeing as the cursor might be in a short line, in which
|
||||
// case we don't want to go past its last column.
|
||||
let max_row_column = if new_row <= map.max_point().row() {
|
||||
map.line_len(new_row)
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let max_column = match min_column + visible_column_count as u32 {
|
||||
max_column if max_column >= max_row_column => max_row_column,
|
||||
max_column => max_column,
|
||||
};
|
||||
|
||||
// Ensure that the cursor's column stays within the visible
|
||||
// area, otherwise clip it at either the left or right edge of
|
||||
// the visible area.
|
||||
let new_column = match (min_column, max_column) {
|
||||
(min_column, _) if starting_column < min_column => min_column,
|
||||
(_, max_column) if starting_column > max_column => max_column,
|
||||
_ => starting_column,
|
||||
};
|
||||
|
||||
let new_head = map.clip_point(DisplayPoint::new(new_row, new_column), Bias::Left);
|
||||
let goal = match amount {
|
||||
ScrollAmount::Column(_) | ScrollAmount::PageWidth(_) => SelectionGoal::None,
|
||||
_ => goal,
|
||||
};
|
||||
|
||||
Some((new_head, goal))
|
||||
};
|
||||
|
||||
if vim.mode == Mode::VisualBlock {
|
||||
vim.visual_block_motion(true, editor, window, cx, &mut move_cursor);
|
||||
} else {
|
||||
editor.change_selections(
|
||||
SelectionEffects::no_scroll().nav_history(false),
|
||||
window,
|
||||
cx,
|
||||
|s| {
|
||||
s.move_with(&mut |map, selection| {
|
||||
if let Some((new_head, goal)) =
|
||||
move_cursor(map, selection.head(), selection.goal)
|
||||
{
|
||||
if selection.is_empty() || !vim.mode.is_visual() {
|
||||
selection.collapse_to(new_head, goal)
|
||||
} else {
|
||||
selection.set_head(new_head, goal)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::{
|
||||
state::Mode,
|
||||
test::{NeovimBackedTestContext, VimTestContext},
|
||||
};
|
||||
use editor::ScrollBeyondLastLine;
|
||||
use gpui::{AppContext as _, point, px, size};
|
||||
use indoc::indoc;
|
||||
use language::Point;
|
||||
use settings::SettingsStore;
|
||||
|
||||
pub fn sample_text(rows: usize, cols: usize, start_char: char) -> String {
|
||||
let mut text = String::new();
|
||||
for row in 0..rows {
|
||||
let c: char = (start_char as u32 + row as u32) as u8 as char;
|
||||
let mut line = c.to_string().repeat(cols);
|
||||
if row < rows - 1 {
|
||||
line.push('\n');
|
||||
}
|
||||
text += &line;
|
||||
}
|
||||
text
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_scroll(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
|
||||
let (line_height, visible_line_count) = cx.update_editor(|editor, window, cx| {
|
||||
(
|
||||
editor
|
||||
.style(cx)
|
||||
.text
|
||||
.line_height_in_pixels(window.rem_size()),
|
||||
editor.visible_line_count().unwrap(),
|
||||
)
|
||||
});
|
||||
|
||||
let window = cx.window;
|
||||
let margin = cx
|
||||
.update_window(window, |_, window, _cx| {
|
||||
window.viewport_size().height - line_height * visible_line_count as f32
|
||||
})
|
||||
.unwrap();
|
||||
cx.simulate_window_resize(
|
||||
cx.window,
|
||||
size(px(1000.), margin + 8. * line_height - px(1.0)),
|
||||
);
|
||||
|
||||
cx.set_state(
|
||||
indoc!(
|
||||
"ˇone
|
||||
two
|
||||
three
|
||||
four
|
||||
five
|
||||
six
|
||||
seven
|
||||
eight
|
||||
nine
|
||||
ten
|
||||
eleven
|
||||
twelve
|
||||
"
|
||||
),
|
||||
Mode::Normal,
|
||||
);
|
||||
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
assert_eq!(editor.snapshot(window, cx).scroll_position(), point(0., 0.))
|
||||
});
|
||||
cx.simulate_keystrokes("ctrl-e");
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
assert_eq!(editor.snapshot(window, cx).scroll_position(), point(0., 1.))
|
||||
});
|
||||
cx.simulate_keystrokes("2 ctrl-e");
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
assert_eq!(editor.snapshot(window, cx).scroll_position(), point(0., 3.))
|
||||
});
|
||||
cx.simulate_keystrokes("ctrl-y");
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
assert_eq!(editor.snapshot(window, cx).scroll_position(), point(0., 2.))
|
||||
});
|
||||
|
||||
// does not select in normal mode
|
||||
cx.simulate_keystrokes("g g");
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
assert_eq!(editor.snapshot(window, cx).scroll_position(), point(0., 0.))
|
||||
});
|
||||
cx.simulate_keystrokes("ctrl-d");
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
assert_eq!(
|
||||
editor.snapshot(window, cx).scroll_position(),
|
||||
point(0., 3.0)
|
||||
);
|
||||
assert_eq!(
|
||||
editor
|
||||
.selections
|
||||
.newest(&editor.display_snapshot(cx))
|
||||
.range(),
|
||||
Point::new(6, 0)..Point::new(6, 0)
|
||||
)
|
||||
});
|
||||
|
||||
// does select in visual mode
|
||||
cx.simulate_keystrokes("g g");
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
assert_eq!(editor.snapshot(window, cx).scroll_position(), point(0., 0.))
|
||||
});
|
||||
cx.simulate_keystrokes("v ctrl-d");
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
assert_eq!(
|
||||
editor.snapshot(window, cx).scroll_position(),
|
||||
point(0., 3.0)
|
||||
);
|
||||
assert_eq!(
|
||||
editor
|
||||
.selections
|
||||
.newest(&editor.display_snapshot(cx))
|
||||
.range(),
|
||||
Point::new(0, 0)..Point::new(6, 1)
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_ctrl_d_u(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_scroll_height(10).await;
|
||||
|
||||
let content = "ˇ".to_owned() + &sample_text(26, 2, 'a');
|
||||
cx.set_shared_state(&content).await;
|
||||
|
||||
// skip over the scrolloff at the top
|
||||
// test ctrl-d
|
||||
cx.simulate_shared_keystrokes("4 j ctrl-d").await;
|
||||
cx.shared_state().await.assert_matches();
|
||||
cx.simulate_shared_keystrokes("ctrl-d").await;
|
||||
cx.shared_state().await.assert_matches();
|
||||
cx.simulate_shared_keystrokes("g g ctrl-d").await;
|
||||
cx.shared_state().await.assert_matches();
|
||||
|
||||
// test ctrl-u
|
||||
cx.simulate_shared_keystrokes("ctrl-u").await;
|
||||
cx.shared_state().await.assert_matches();
|
||||
cx.simulate_shared_keystrokes("ctrl-d ctrl-d 4 j ctrl-u ctrl-u")
|
||||
.await;
|
||||
cx.shared_state().await.assert_matches();
|
||||
|
||||
// test returning to top
|
||||
cx.simulate_shared_keystrokes("g g ctrl-d ctrl-u ctrl-u")
|
||||
.await;
|
||||
cx.shared_state().await.assert_matches();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_ctrl_f_b(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
let visible_lines = 10;
|
||||
cx.set_scroll_height(visible_lines).await;
|
||||
|
||||
// First test without vertical scroll margin
|
||||
cx.neovim.set_option(&format!("scrolloff={}", 0)).await;
|
||||
cx.update_global(|store: &mut SettingsStore, cx| {
|
||||
store.update_user_settings(cx, |s| s.editor.vertical_scroll_margin = Some(0.0));
|
||||
});
|
||||
|
||||
let content = "ˇ".to_owned() + &sample_text(26, 2, 'a');
|
||||
cx.set_shared_state(&content).await;
|
||||
|
||||
// scroll down: ctrl-f
|
||||
cx.simulate_shared_keystrokes("ctrl-f").await;
|
||||
cx.shared_state().await.assert_matches();
|
||||
|
||||
cx.simulate_shared_keystrokes("ctrl-f").await;
|
||||
cx.shared_state().await.assert_matches();
|
||||
|
||||
// scroll up: ctrl-b
|
||||
cx.simulate_shared_keystrokes("ctrl-b").await;
|
||||
cx.shared_state().await.assert_matches();
|
||||
|
||||
cx.simulate_shared_keystrokes("ctrl-b").await;
|
||||
cx.shared_state().await.assert_matches();
|
||||
|
||||
// Now go back to start of file, and test with vertical scroll margin
|
||||
cx.simulate_shared_keystrokes("g g").await;
|
||||
cx.shared_state().await.assert_matches();
|
||||
|
||||
cx.neovim.set_option(&format!("scrolloff={}", 3)).await;
|
||||
cx.update_global(|store: &mut SettingsStore, cx| {
|
||||
store.update_user_settings(cx, |s| s.editor.vertical_scroll_margin = Some(3.0));
|
||||
});
|
||||
|
||||
// scroll down: ctrl-f
|
||||
cx.simulate_shared_keystrokes("ctrl-f").await;
|
||||
cx.shared_state().await.assert_matches();
|
||||
|
||||
cx.simulate_shared_keystrokes("ctrl-f").await;
|
||||
cx.shared_state().await.assert_matches();
|
||||
|
||||
// scroll up: ctrl-b
|
||||
cx.simulate_shared_keystrokes("ctrl-b").await;
|
||||
cx.shared_state().await.assert_matches();
|
||||
|
||||
cx.simulate_shared_keystrokes("ctrl-b").await;
|
||||
cx.shared_state().await.assert_matches();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_scroll_beyond_last_line(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_scroll_height(10).await;
|
||||
|
||||
let content = "ˇ".to_owned() + &sample_text(26, 2, 'a');
|
||||
cx.set_shared_state(&content).await;
|
||||
|
||||
cx.update_global(|store: &mut SettingsStore, cx| {
|
||||
store.update_user_settings(cx, |s| {
|
||||
s.editor.scroll_beyond_last_line = Some(ScrollBeyondLastLine::Off);
|
||||
});
|
||||
});
|
||||
|
||||
// ctrl-d can reach the end and the cursor stays in the first column
|
||||
cx.simulate_shared_keystrokes("shift-g k").await;
|
||||
cx.shared_state().await.assert_matches();
|
||||
cx.simulate_shared_keystrokes("ctrl-d").await;
|
||||
cx.shared_state().await.assert_matches();
|
||||
|
||||
// ctrl-u from the last line
|
||||
cx.simulate_shared_keystrokes("shift-g").await;
|
||||
cx.shared_state().await.assert_matches();
|
||||
cx.simulate_shared_keystrokes("ctrl-u").await;
|
||||
cx.shared_state().await.assert_matches();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_ctrl_y_e(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_scroll_height(10).await;
|
||||
|
||||
let content = "ˇ".to_owned() + &sample_text(26, 2, 'a');
|
||||
cx.set_shared_state(&content).await;
|
||||
|
||||
for _ in 0..8 {
|
||||
cx.simulate_shared_keystrokes("ctrl-e").await;
|
||||
cx.shared_state().await.assert_matches();
|
||||
}
|
||||
|
||||
for _ in 0..8 {
|
||||
cx.simulate_shared_keystrokes("ctrl-y").await;
|
||||
cx.shared_state().await.assert_matches();
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_scroll_jumps(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_scroll_height(20).await;
|
||||
|
||||
let content = "ˇ".to_owned() + &sample_text(52, 2, 'a');
|
||||
cx.set_shared_state(&content).await;
|
||||
|
||||
cx.simulate_shared_keystrokes("shift-g g g").await;
|
||||
cx.simulate_shared_keystrokes("ctrl-d ctrl-d ctrl-o").await;
|
||||
cx.shared_state().await.assert_matches();
|
||||
cx.simulate_shared_keystrokes("ctrl-o").await;
|
||||
cx.shared_state().await.assert_matches();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_horizontal_scroll(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_scroll_height(20).await;
|
||||
cx.set_shared_wrap(12).await;
|
||||
cx.set_neovim_option("nowrap").await;
|
||||
|
||||
let content = "ˇ01234567890123456789";
|
||||
cx.set_shared_state(content).await;
|
||||
|
||||
cx.simulate_shared_keystrokes("z shift-l").await;
|
||||
cx.shared_state().await.assert_eq("012345ˇ67890123456789");
|
||||
|
||||
// At this point, `z h` should not move the cursor as it should still be
|
||||
// visible within the 12 column width.
|
||||
cx.simulate_shared_keystrokes("z h").await;
|
||||
cx.shared_state().await.assert_eq("012345ˇ67890123456789");
|
||||
|
||||
let content = "ˇ01234567890123456789";
|
||||
cx.set_shared_state(content).await;
|
||||
|
||||
cx.simulate_shared_keystrokes("z l").await;
|
||||
cx.shared_state().await.assert_eq("0ˇ1234567890123456789");
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,315 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{
|
||||
Mode, Vim,
|
||||
motion::{Motion, MotionKind},
|
||||
};
|
||||
use editor::{Editor, SelectionEffects, movement};
|
||||
use gpui::{Context, Window, actions};
|
||||
use language::Point;
|
||||
|
||||
actions!(
|
||||
vim,
|
||||
[
|
||||
/// Substitutes characters in the current selection.
|
||||
Substitute,
|
||||
/// Substitutes the entire line.
|
||||
SubstituteLine
|
||||
]
|
||||
);
|
||||
|
||||
pub(crate) fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
|
||||
Vim::action(editor, cx, |vim, _: &Substitute, window, cx| {
|
||||
vim.start_recording(cx);
|
||||
let count = Vim::take_count(cx);
|
||||
Vim::take_forced_motion(cx);
|
||||
vim.substitute(count, vim.mode == Mode::VisualLine, window, cx);
|
||||
});
|
||||
|
||||
Vim::action(editor, cx, |vim, _: &SubstituteLine, window, cx| {
|
||||
vim.start_recording(cx);
|
||||
if matches!(vim.mode, Mode::VisualBlock | Mode::Visual) {
|
||||
vim.switch_mode(Mode::VisualLine, false, window, cx)
|
||||
}
|
||||
let count = Vim::take_count(cx);
|
||||
Vim::take_forced_motion(cx);
|
||||
vim.substitute(count, true, window, cx)
|
||||
});
|
||||
}
|
||||
|
||||
impl Vim {
|
||||
pub fn substitute(
|
||||
&mut self,
|
||||
count: Option<usize>,
|
||||
line_mode: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.store_visual_marks(window, cx);
|
||||
self.update_editor(cx, |vim, editor, cx| {
|
||||
editor.set_clip_at_line_ends(false, cx);
|
||||
editor.transact(window, cx, |editor, window, cx| {
|
||||
let text_layout_details = editor.text_layout_details(window, cx);
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
s.move_with(&mut |map, selection| {
|
||||
if selection.start == selection.end {
|
||||
Motion::Right.expand_selection(
|
||||
map,
|
||||
selection,
|
||||
count,
|
||||
&text_layout_details,
|
||||
false,
|
||||
);
|
||||
}
|
||||
if line_mode {
|
||||
// in Visual mode when the selection contains the newline at the end
|
||||
// of the line, we should exclude it.
|
||||
if !selection.is_empty() && selection.end.column() == 0 {
|
||||
selection.end = movement::left(map, selection.end);
|
||||
}
|
||||
Motion::CurrentLine.expand_selection(
|
||||
map,
|
||||
selection,
|
||||
None,
|
||||
&text_layout_details,
|
||||
false,
|
||||
);
|
||||
if let Some((point, _)) = (Motion::FirstNonWhitespace {
|
||||
display_lines: false,
|
||||
})
|
||||
.move_point(
|
||||
map,
|
||||
selection.start,
|
||||
selection.goal,
|
||||
None,
|
||||
&text_layout_details,
|
||||
) {
|
||||
selection.start = point;
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
let kind = if line_mode {
|
||||
MotionKind::Linewise
|
||||
} else {
|
||||
MotionKind::Exclusive
|
||||
};
|
||||
vim.copy_selections_content(editor, kind, window, cx);
|
||||
let linked_edits = editor.linked_edits_for_selections(Arc::from(""), cx);
|
||||
let selections = editor
|
||||
.selections
|
||||
.all::<Point>(&editor.display_snapshot(cx))
|
||||
.into_iter();
|
||||
let edits = selections.map(|selection| (selection.start..selection.end, ""));
|
||||
editor.edit(edits, cx);
|
||||
linked_edits.apply(cx);
|
||||
});
|
||||
});
|
||||
self.switch_mode(Mode::Insert, true, window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::{
|
||||
state::Mode,
|
||||
test::{NeovimBackedTestContext, VimTestContext},
|
||||
};
|
||||
use indoc::indoc;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_substitute(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
|
||||
// supports a single cursor
|
||||
cx.set_state(indoc! {"ˇabc\n"}, Mode::Normal);
|
||||
cx.simulate_keystrokes("s x");
|
||||
cx.assert_editor_state("xˇbc\n");
|
||||
|
||||
// supports a selection
|
||||
cx.set_state(indoc! {"a«bcˇ»\n"}, Mode::Visual);
|
||||
cx.assert_editor_state("a«bcˇ»\n");
|
||||
cx.simulate_keystrokes("s x");
|
||||
cx.assert_editor_state("axˇ\n");
|
||||
|
||||
// supports counts
|
||||
cx.set_state(indoc! {"ˇabc\n"}, Mode::Normal);
|
||||
cx.simulate_keystrokes("2 s x");
|
||||
cx.assert_editor_state("xˇc\n");
|
||||
|
||||
// supports multiple cursors
|
||||
cx.set_state(indoc! {"a«bcˇ»deˇffg\n"}, Mode::Normal);
|
||||
cx.simulate_keystrokes("2 s x");
|
||||
cx.assert_editor_state("axˇdexˇg\n");
|
||||
|
||||
// does not read beyond end of line
|
||||
cx.set_state(indoc! {"ˇabc\n"}, Mode::Normal);
|
||||
cx.simulate_keystrokes("5 s x");
|
||||
cx.assert_editor_state("xˇ\n");
|
||||
|
||||
// it handles multibyte characters
|
||||
cx.set_state(indoc! {"ˇcàfé\n"}, Mode::Normal);
|
||||
cx.simulate_keystrokes("4 s");
|
||||
cx.assert_editor_state("ˇ\n");
|
||||
|
||||
// should transactionally undo selection changes
|
||||
cx.simulate_keystrokes("escape u");
|
||||
cx.assert_editor_state("ˇcàfé\n");
|
||||
|
||||
// it handles visual line mode
|
||||
cx.set_state(
|
||||
indoc! {"
|
||||
alpha
|
||||
beˇta
|
||||
gamma"},
|
||||
Mode::Normal,
|
||||
);
|
||||
cx.simulate_keystrokes("shift-v s");
|
||||
cx.assert_editor_state(indoc! {"
|
||||
alpha
|
||||
ˇ
|
||||
gamma"});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_visual_change(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_shared_state("The quick ˇbrown").await;
|
||||
cx.simulate_shared_keystrokes("v w c").await;
|
||||
cx.shared_state().await.assert_eq("The quick ˇ");
|
||||
|
||||
cx.set_shared_state(indoc! {"
|
||||
The ˇquick brown
|
||||
fox jumps over
|
||||
the lazy dog"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes("v w j c").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {"
|
||||
The ˇver
|
||||
the lazy dog"});
|
||||
|
||||
cx.simulate_at_each_offset(
|
||||
"v w j c",
|
||||
indoc! {"
|
||||
The ˇquick brown
|
||||
fox jumps ˇover
|
||||
the ˇlazy dog"},
|
||||
)
|
||||
.await
|
||||
.assert_matches();
|
||||
cx.simulate_at_each_offset(
|
||||
"v w k c",
|
||||
indoc! {"
|
||||
The ˇquick brown
|
||||
fox jumps ˇover
|
||||
the ˇlazy dog"},
|
||||
)
|
||||
.await
|
||||
.assert_matches();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_visual_line_change(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
cx.simulate(
|
||||
"shift-v c",
|
||||
indoc! {"
|
||||
The quˇick brown
|
||||
fox jumps over
|
||||
the lazy dog"},
|
||||
)
|
||||
.await
|
||||
.assert_matches();
|
||||
// Test pasting code copied on change
|
||||
cx.simulate_shared_keystrokes("escape j p").await;
|
||||
cx.shared_state().await.assert_matches();
|
||||
|
||||
cx.simulate_at_each_offset(
|
||||
"shift-v c",
|
||||
indoc! {"
|
||||
The quick brown
|
||||
fox juˇmps over
|
||||
the laˇzy dog"},
|
||||
)
|
||||
.await
|
||||
.assert_matches();
|
||||
cx.simulate(
|
||||
"shift-v j c",
|
||||
indoc! {"
|
||||
The quˇick brown
|
||||
fox jumps over
|
||||
the lazy dog"},
|
||||
)
|
||||
.await
|
||||
.assert_matches();
|
||||
// Test pasting code copied on delete
|
||||
cx.simulate_shared_keystrokes("escape j p").await;
|
||||
cx.shared_state().await.assert_matches();
|
||||
|
||||
cx.simulate_at_each_offset(
|
||||
"shift-v j c",
|
||||
indoc! {"
|
||||
The quick brown
|
||||
fox juˇmps over
|
||||
the laˇzy dog"},
|
||||
)
|
||||
.await
|
||||
.assert_matches();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_substitute_line(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
let initial_state = indoc! {"
|
||||
The quick brown
|
||||
fox juˇmps over
|
||||
the lazy dog
|
||||
"};
|
||||
|
||||
// normal mode
|
||||
cx.set_shared_state(initial_state).await;
|
||||
cx.simulate_shared_keystrokes("shift-s o").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {"
|
||||
The quick brown
|
||||
oˇ
|
||||
the lazy dog
|
||||
"});
|
||||
|
||||
// visual mode
|
||||
cx.set_shared_state(initial_state).await;
|
||||
cx.simulate_shared_keystrokes("v k shift-s o").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {"
|
||||
oˇ
|
||||
the lazy dog
|
||||
"});
|
||||
|
||||
// visual block mode
|
||||
cx.set_shared_state(initial_state).await;
|
||||
cx.simulate_shared_keystrokes("ctrl-v j shift-s o").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {"
|
||||
The quick brown
|
||||
oˇ
|
||||
"});
|
||||
|
||||
// visual mode including newline
|
||||
cx.set_shared_state(initial_state).await;
|
||||
cx.simulate_shared_keystrokes("v $ shift-s o").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {"
|
||||
The quick brown
|
||||
oˇ
|
||||
the lazy dog
|
||||
"});
|
||||
|
||||
// indentation
|
||||
cx.set_neovim_option("shiftwidth=4").await;
|
||||
cx.set_shared_state(initial_state).await;
|
||||
cx.simulate_shared_keystrokes("> > shift-s o").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {"
|
||||
The quick brown
|
||||
oˇ
|
||||
the lazy dog
|
||||
"});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
use crate::{Vim, motion::Motion, object::Object};
|
||||
use collections::HashMap;
|
||||
use editor::{Bias, SelectionEffects, display_map::ToDisplayPoint};
|
||||
use gpui::{Context, Window};
|
||||
use language::SelectionGoal;
|
||||
|
||||
impl Vim {
|
||||
pub fn toggle_comments_motion(
|
||||
&mut self,
|
||||
motion: Motion,
|
||||
times: Option<usize>,
|
||||
forced_motion: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.stop_recording(cx);
|
||||
self.update_editor(cx, |_, editor, cx| {
|
||||
let text_layout_details = editor.text_layout_details(window, cx);
|
||||
editor.transact(window, cx, |editor, window, cx| {
|
||||
let mut selection_starts: HashMap<_, _> = Default::default();
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
s.move_with(&mut |map, selection| {
|
||||
let anchor = map.display_point_to_anchor(selection.head(), Bias::Right);
|
||||
selection_starts.insert(selection.id, anchor);
|
||||
motion.expand_selection(
|
||||
map,
|
||||
selection,
|
||||
times,
|
||||
&text_layout_details,
|
||||
forced_motion,
|
||||
);
|
||||
});
|
||||
});
|
||||
editor.toggle_comments(&Default::default(), window, cx);
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
s.move_with(&mut |map, selection| {
|
||||
let anchor = selection_starts.remove(&selection.id).unwrap();
|
||||
selection.collapse_to(anchor.to_display_point(map), SelectionGoal::None);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
pub fn toggle_comments_object(
|
||||
&mut self,
|
||||
object: Object,
|
||||
around: bool,
|
||||
times: Option<usize>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.stop_recording(cx);
|
||||
self.update_editor(cx, |_, editor, cx| {
|
||||
editor.transact(window, cx, |editor, window, cx| {
|
||||
let mut original_positions: HashMap<_, _> = Default::default();
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
s.move_with(&mut |map, selection| {
|
||||
let anchor = map.display_point_to_anchor(selection.head(), Bias::Right);
|
||||
original_positions.insert(selection.id, anchor);
|
||||
object.expand_selection(map, selection, around, times);
|
||||
});
|
||||
});
|
||||
editor.toggle_comments(&Default::default(), window, cx);
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
s.move_with(&mut |map, selection| {
|
||||
let anchor = original_positions.remove(&selection.id).unwrap();
|
||||
selection.collapse_to(anchor.to_display_point(map), SelectionGoal::None);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
pub fn toggle_block_comments_motion(
|
||||
&mut self,
|
||||
motion: Motion,
|
||||
times: Option<usize>,
|
||||
forced_motion: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.stop_recording(cx);
|
||||
self.update_editor(cx, |_, editor, cx| {
|
||||
let text_layout_details = editor.text_layout_details(window, cx);
|
||||
editor.transact(window, cx, |editor, window, cx| {
|
||||
editor.set_clip_at_line_ends(false, cx);
|
||||
let mut selection_starts: HashMap<_, _> = Default::default();
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
s.move_with(&mut |map, selection| {
|
||||
let anchor = map.display_point_to_anchor(selection.head(), Bias::Right);
|
||||
selection_starts.insert(selection.id, anchor);
|
||||
motion.expand_selection(
|
||||
map,
|
||||
selection,
|
||||
times,
|
||||
&text_layout_details,
|
||||
forced_motion,
|
||||
);
|
||||
});
|
||||
});
|
||||
editor.toggle_block_comments(&Default::default(), window, cx);
|
||||
editor.set_clip_at_line_ends(true, cx);
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
s.move_with(&mut |map, selection| {
|
||||
let anchor = selection_starts.remove(&selection.id).unwrap();
|
||||
selection.collapse_to(anchor.to_display_point(map), SelectionGoal::None);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
pub fn toggle_block_comments_object(
|
||||
&mut self,
|
||||
object: Object,
|
||||
around: bool,
|
||||
times: Option<usize>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.stop_recording(cx);
|
||||
self.update_editor(cx, |_, editor, cx| {
|
||||
editor.transact(window, cx, |editor, window, cx| {
|
||||
editor.set_clip_at_line_ends(false, cx);
|
||||
let mut original_positions: HashMap<_, _> = Default::default();
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
s.move_with(&mut |map, selection| {
|
||||
let anchor = map.display_point_to_anchor(selection.head(), Bias::Right);
|
||||
original_positions.insert(selection.id, anchor);
|
||||
object.expand_selection(map, selection, around, times);
|
||||
});
|
||||
});
|
||||
editor.toggle_block_comments(&Default::default(), window, cx);
|
||||
editor.set_clip_at_line_ends(true, cx);
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
s.move_with(&mut |map, selection| {
|
||||
let anchor = original_positions.remove(&selection.id).unwrap();
|
||||
selection.collapse_to(anchor.to_display_point(map), SelectionGoal::None);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
use std::{ops::Range, time::Duration};
|
||||
|
||||
use crate::{
|
||||
Vim, VimSettings,
|
||||
motion::{Motion, MotionKind},
|
||||
object::Object,
|
||||
state::{Mode, Register},
|
||||
};
|
||||
use collections::HashMap;
|
||||
use editor::{ClipboardSelection, Editor, HighlightKey, SelectionEffects};
|
||||
use gpui::Context;
|
||||
use gpui::Window;
|
||||
use language::Point;
|
||||
use settings::Settings;
|
||||
|
||||
impl Vim {
|
||||
pub fn yank_motion(
|
||||
&mut self,
|
||||
motion: Motion,
|
||||
times: Option<usize>,
|
||||
forced_motion: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.update_editor(cx, |vim, editor, cx| {
|
||||
let text_layout_details = editor.text_layout_details(window, cx);
|
||||
editor.transact(window, cx, |editor, window, cx| {
|
||||
editor.set_clip_at_line_ends(false, cx);
|
||||
let mut original_positions: HashMap<_, _> = Default::default();
|
||||
let mut kind = None;
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
s.move_with(&mut |map, selection| {
|
||||
let original_position = (selection.head(), selection.goal);
|
||||
kind = motion.expand_selection(
|
||||
map,
|
||||
selection,
|
||||
times,
|
||||
&text_layout_details,
|
||||
forced_motion,
|
||||
);
|
||||
if kind == Some(MotionKind::Exclusive) {
|
||||
original_positions
|
||||
.insert(selection.id, (selection.start, selection.goal));
|
||||
} else {
|
||||
original_positions.insert(selection.id, original_position);
|
||||
}
|
||||
})
|
||||
});
|
||||
let Some(kind) = kind else { return };
|
||||
vim.yank_selections_content(editor, kind, window, cx);
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
s.move_with(&mut |_, selection| {
|
||||
let (head, goal) = original_positions.remove(&selection.id).unwrap();
|
||||
selection.collapse_to(head, goal);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
self.exit_temporary_normal(window, cx);
|
||||
}
|
||||
|
||||
pub fn yank_object(
|
||||
&mut self,
|
||||
object: Object,
|
||||
around: bool,
|
||||
times: Option<usize>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.update_editor(cx, |vim, editor, cx| {
|
||||
editor.transact(window, cx, |editor, window, cx| {
|
||||
editor.set_clip_at_line_ends(false, cx);
|
||||
let mut start_positions: HashMap<_, _> = Default::default();
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
s.move_with(&mut |map, selection| {
|
||||
object.expand_selection(map, selection, around, times);
|
||||
let start_position = (selection.start, selection.goal);
|
||||
start_positions.insert(selection.id, start_position);
|
||||
});
|
||||
});
|
||||
let kind = match object.target_visual_mode(vim.mode, around) {
|
||||
Mode::VisualLine => MotionKind::Linewise,
|
||||
_ => MotionKind::Exclusive,
|
||||
};
|
||||
vim.yank_selections_content(editor, kind, window, cx);
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
s.move_with(&mut |_, selection| {
|
||||
let (head, goal) = start_positions.remove(&selection.id).unwrap();
|
||||
selection.collapse_to(head, goal);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
self.exit_temporary_normal(window, cx);
|
||||
}
|
||||
|
||||
pub fn yank_selections_content(
|
||||
&mut self,
|
||||
editor: &mut Editor,
|
||||
kind: MotionKind,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) {
|
||||
self.copy_ranges(
|
||||
editor,
|
||||
kind,
|
||||
true,
|
||||
editor
|
||||
.selections
|
||||
.all_adjusted(&editor.display_snapshot(cx))
|
||||
.iter()
|
||||
.map(|s| s.range())
|
||||
.collect(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn copy_selections_content(
|
||||
&mut self,
|
||||
editor: &mut Editor,
|
||||
kind: MotionKind,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) {
|
||||
self.copy_ranges(
|
||||
editor,
|
||||
kind,
|
||||
false,
|
||||
editor
|
||||
.selections
|
||||
.all_adjusted(&editor.display_snapshot(cx))
|
||||
.iter()
|
||||
.map(|s| s.range())
|
||||
.collect(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn copy_ranges(
|
||||
&mut self,
|
||||
editor: &mut Editor,
|
||||
kind: MotionKind,
|
||||
is_yank: bool,
|
||||
selections: Vec<Range<Point>>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) {
|
||||
let buffer = editor.buffer().read(cx).snapshot(cx);
|
||||
self.set_mark(
|
||||
"[".to_string(),
|
||||
selections
|
||||
.iter()
|
||||
.map(|s| buffer.anchor_before(s.start))
|
||||
.collect(),
|
||||
editor.buffer(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
self.set_mark(
|
||||
"]".to_string(),
|
||||
selections
|
||||
.iter()
|
||||
.map(|s| buffer.anchor_after(s.end))
|
||||
.collect(),
|
||||
editor.buffer(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
||||
let mut text = String::new();
|
||||
let mut clipboard_selections = Vec::with_capacity(selections.len());
|
||||
let mut ranges_to_highlight = Vec::new();
|
||||
|
||||
{
|
||||
let mut is_first = true;
|
||||
for selection in selections.iter() {
|
||||
let start = selection.start;
|
||||
let end = selection.end;
|
||||
if is_first {
|
||||
is_first = false;
|
||||
} else {
|
||||
text.push('\n');
|
||||
}
|
||||
let initial_len = text.len();
|
||||
|
||||
let start_anchor = buffer.anchor_after(start);
|
||||
let end_anchor = buffer.anchor_before(end);
|
||||
ranges_to_highlight.push(start_anchor..end_anchor);
|
||||
|
||||
for chunk in buffer.text_for_range(start..end) {
|
||||
text.push_str(chunk);
|
||||
}
|
||||
if kind.linewise() {
|
||||
text.push('\n');
|
||||
}
|
||||
clipboard_selections.push(ClipboardSelection::for_buffer(
|
||||
text.len() - initial_len,
|
||||
false,
|
||||
start..end,
|
||||
&buffer,
|
||||
editor.project(),
|
||||
cx,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let selected_register = self.selected_register.take();
|
||||
Vim::update_globals(cx, |globals, cx| {
|
||||
globals.write_registers(
|
||||
Register {
|
||||
text: text.into(),
|
||||
clipboard_selections: Some(clipboard_selections),
|
||||
},
|
||||
selected_register,
|
||||
is_yank,
|
||||
kind,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let highlight_duration = VimSettings::get_global(cx).highlight_on_yank_duration;
|
||||
if !is_yank || self.mode == Mode::Visual || highlight_duration == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
editor.highlight_background(
|
||||
HighlightKey::HighlightOnYank,
|
||||
&ranges_to_highlight,
|
||||
|_, colors| colors.colors().vim_yank_background,
|
||||
cx,
|
||||
);
|
||||
cx.spawn(async move |this, cx| {
|
||||
cx.background_executor()
|
||||
.timer(Duration::from_millis(highlight_duration))
|
||||
.await;
|
||||
this.update(cx, |editor, cx| {
|
||||
editor.clear_background_highlights(HighlightKey::HighlightOnYank, cx)
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,559 @@
|
||||
use crate::{
|
||||
Operator, Vim,
|
||||
motion::{self, Motion},
|
||||
object::Object,
|
||||
state::Mode,
|
||||
};
|
||||
use editor::{
|
||||
Anchor, Bias, Editor, EditorSnapshot, HighlightKey, SelectionEffects, ToOffset, ToPoint,
|
||||
display_map::ToDisplayPoint,
|
||||
};
|
||||
use gpui::{ClipboardEntry, Context, Window, actions};
|
||||
use language::{Point, SelectionGoal};
|
||||
use std::ops::Range;
|
||||
use std::sync::Arc;
|
||||
|
||||
actions!(
|
||||
vim,
|
||||
[
|
||||
/// Toggles replace mode.
|
||||
ToggleReplace,
|
||||
/// Undoes the last replacement.
|
||||
UndoReplace
|
||||
]
|
||||
);
|
||||
|
||||
pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
|
||||
Vim::action(editor, cx, |vim, _: &ToggleReplace, window, cx| {
|
||||
vim.replacements = vec![];
|
||||
vim.start_recording(cx);
|
||||
vim.switch_mode(Mode::Replace, false, window, cx);
|
||||
});
|
||||
|
||||
Vim::action(editor, cx, |vim, _: &UndoReplace, window, cx| {
|
||||
if vim.mode != Mode::Replace {
|
||||
return;
|
||||
}
|
||||
let count = Vim::take_count(cx);
|
||||
Vim::take_forced_motion(cx);
|
||||
vim.undo_replace(count, window, cx)
|
||||
});
|
||||
}
|
||||
|
||||
impl Vim {
|
||||
pub(crate) fn multi_replace(
|
||||
&mut self,
|
||||
text: Arc<str>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.update_editor(cx, |vim, editor, cx| {
|
||||
editor.transact(window, cx, |editor, window, cx| {
|
||||
editor.set_clip_at_line_ends(false, cx);
|
||||
let map = editor.snapshot(window, cx);
|
||||
let display_selections = editor.selections.all::<Point>(&map.display_snapshot);
|
||||
|
||||
// Handles all string that require manipulation, including inserts and replaces
|
||||
let edits = display_selections
|
||||
.into_iter()
|
||||
.map(|selection| {
|
||||
let is_new_line = text.as_ref() == "\n";
|
||||
let mut range = selection.range();
|
||||
// "\n" need to be handled separately, because when a "\n" is typing,
|
||||
// we don't do a replace, we need insert a "\n"
|
||||
if !is_new_line {
|
||||
range.end.column += 1;
|
||||
range.end = map.buffer_snapshot().clip_point(range.end, Bias::Right);
|
||||
}
|
||||
let replace_range = map.buffer_snapshot().anchor_before(range.start)
|
||||
..map.buffer_snapshot().anchor_after(range.end);
|
||||
let current_text = map
|
||||
.buffer_snapshot()
|
||||
.text_for_range(replace_range.clone())
|
||||
.collect();
|
||||
vim.replacements.push((replace_range.clone(), current_text));
|
||||
(replace_range, text.clone())
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
editor.edit_with_block_indent(edits.clone(), Vec::new(), cx);
|
||||
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
s.select_anchor_ranges(edits.iter().map(|(range, _)| range.end..range.end));
|
||||
});
|
||||
editor.set_clip_at_line_ends(true, cx);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn undo_replace(
|
||||
&mut self,
|
||||
maybe_times: Option<usize>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.update_editor(cx, |vim, editor, cx| {
|
||||
editor.transact(window, cx, |editor, window, cx| {
|
||||
editor.set_clip_at_line_ends(false, cx);
|
||||
let map = editor.snapshot(window, cx);
|
||||
let selections = editor.selections.all::<Point>(&map.display_snapshot);
|
||||
let mut new_selections = vec![];
|
||||
let edits: Vec<(Range<Point>, String)> = selections
|
||||
.into_iter()
|
||||
.filter_map(|selection| {
|
||||
let end = selection.head();
|
||||
let start = motion::wrapping_left(
|
||||
&map,
|
||||
end.to_display_point(&map),
|
||||
maybe_times.unwrap_or(1),
|
||||
)
|
||||
.to_point(&map);
|
||||
new_selections.push(
|
||||
map.buffer_snapshot().anchor_before(start)
|
||||
..map.buffer_snapshot().anchor_before(start),
|
||||
);
|
||||
|
||||
let mut undo = None;
|
||||
let edit_range = start..end;
|
||||
for (i, (range, inverse)) in vim.replacements.iter().rev().enumerate() {
|
||||
if range.start.to_point(&map.buffer_snapshot()) <= edit_range.start
|
||||
&& range.end.to_point(&map.buffer_snapshot()) >= edit_range.end
|
||||
{
|
||||
undo = Some(inverse.clone());
|
||||
vim.replacements.remove(vim.replacements.len() - i - 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
Some((edit_range, undo?))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
editor.edit(edits, cx);
|
||||
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
s.select_ranges(new_selections);
|
||||
});
|
||||
editor.set_clip_at_line_ends(true, cx);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
pub fn exchange_object(
|
||||
&mut self,
|
||||
object: Object,
|
||||
around: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.stop_recording(cx);
|
||||
self.update_editor(cx, |vim, editor, cx| {
|
||||
editor.set_clip_at_line_ends(false, cx);
|
||||
let mut selection = editor
|
||||
.selections
|
||||
.newest_display(&editor.display_snapshot(cx));
|
||||
let snapshot = editor.snapshot(window, cx);
|
||||
object.expand_selection(&snapshot, &mut selection, around, None);
|
||||
let start = snapshot
|
||||
.buffer_snapshot()
|
||||
.anchor_before(selection.start.to_point(&snapshot));
|
||||
let end = snapshot
|
||||
.buffer_snapshot()
|
||||
.anchor_before(selection.end.to_point(&snapshot));
|
||||
let new_range = start..end;
|
||||
vim.exchange_impl(new_range, editor, &snapshot, window, cx);
|
||||
editor.set_clip_at_line_ends(true, cx);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn exchange_visual(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.stop_recording(cx);
|
||||
self.update_editor(cx, |vim, editor, cx| {
|
||||
let selection = editor.selections.newest_anchor();
|
||||
let new_range = selection.start..selection.end;
|
||||
let snapshot = editor.snapshot(window, cx);
|
||||
vim.exchange_impl(new_range, editor, &snapshot, window, cx);
|
||||
});
|
||||
self.switch_mode(Mode::Normal, false, window, cx);
|
||||
}
|
||||
|
||||
pub fn clear_exchange(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.stop_recording(cx);
|
||||
self.update_editor(cx, |_, editor, cx| {
|
||||
editor.clear_background_highlights(HighlightKey::VimExchange, cx);
|
||||
});
|
||||
self.clear_operator(window, cx);
|
||||
}
|
||||
|
||||
pub fn exchange_motion(
|
||||
&mut self,
|
||||
motion: Motion,
|
||||
times: Option<usize>,
|
||||
forced_motion: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.stop_recording(cx);
|
||||
self.update_editor(cx, |vim, editor, cx| {
|
||||
editor.set_clip_at_line_ends(false, cx);
|
||||
let text_layout_details = editor.text_layout_details(window, cx);
|
||||
let mut selection = editor
|
||||
.selections
|
||||
.newest_display(&editor.display_snapshot(cx));
|
||||
let snapshot = editor.snapshot(window, cx);
|
||||
motion.expand_selection(
|
||||
&snapshot,
|
||||
&mut selection,
|
||||
times,
|
||||
&text_layout_details,
|
||||
forced_motion,
|
||||
);
|
||||
let start = snapshot
|
||||
.buffer_snapshot()
|
||||
.anchor_before(selection.start.to_point(&snapshot));
|
||||
let end = snapshot
|
||||
.buffer_snapshot()
|
||||
.anchor_before(selection.end.to_point(&snapshot));
|
||||
let new_range = start..end;
|
||||
vim.exchange_impl(new_range, editor, &snapshot, window, cx);
|
||||
editor.set_clip_at_line_ends(true, cx);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn exchange_impl(
|
||||
&self,
|
||||
new_range: Range<Anchor>,
|
||||
editor: &mut Editor,
|
||||
snapshot: &EditorSnapshot,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) {
|
||||
if let Some((_, ranges)) = editor.clear_background_highlights(HighlightKey::VimExchange, cx)
|
||||
{
|
||||
let previous_range = ranges[0].clone();
|
||||
|
||||
let new_range_start = new_range.start.to_offset(&snapshot.buffer_snapshot());
|
||||
let new_range_end = new_range.end.to_offset(&snapshot.buffer_snapshot());
|
||||
let previous_range_end = previous_range.end.to_offset(&snapshot.buffer_snapshot());
|
||||
let previous_range_start = previous_range.start.to_offset(&snapshot.buffer_snapshot());
|
||||
|
||||
let text_for = |range: Range<Anchor>| {
|
||||
snapshot
|
||||
.buffer_snapshot()
|
||||
.text_for_range(range)
|
||||
.collect::<String>()
|
||||
};
|
||||
|
||||
let mut final_cursor_position = None;
|
||||
|
||||
if previous_range_end < new_range_start || new_range_end < previous_range_start {
|
||||
let previous_text = text_for(previous_range.clone());
|
||||
let new_text = text_for(new_range.clone());
|
||||
final_cursor_position = Some(new_range.start.to_display_point(snapshot));
|
||||
|
||||
editor.edit([(previous_range, new_text), (new_range, previous_text)], cx);
|
||||
} else if new_range_start <= previous_range_start && new_range_end >= previous_range_end
|
||||
{
|
||||
final_cursor_position = Some(new_range.start.to_display_point(snapshot));
|
||||
editor.edit([(new_range, text_for(previous_range))], cx);
|
||||
} else if previous_range_start <= new_range_start && previous_range_end >= new_range_end
|
||||
{
|
||||
final_cursor_position = Some(previous_range.start.to_display_point(snapshot));
|
||||
editor.edit([(previous_range, text_for(new_range))], cx);
|
||||
}
|
||||
|
||||
if let Some(position) = final_cursor_position {
|
||||
editor.change_selections(Default::default(), window, cx, |s| {
|
||||
s.move_with(&mut |_map, selection| {
|
||||
selection.collapse_to(position, SelectionGoal::None);
|
||||
});
|
||||
})
|
||||
}
|
||||
} else {
|
||||
let ranges = [new_range];
|
||||
editor.highlight_background(
|
||||
HighlightKey::VimExchange,
|
||||
&ranges,
|
||||
|_, theme| theme.colors().editor_document_highlight_read_background,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Pastes the clipboard contents, replacing the same number of characters
|
||||
/// as the clipboard's contents.
|
||||
pub fn paste_replace(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let clipboard_text = cx.read_from_clipboard().and_then(|item| {
|
||||
item.entries().iter().find_map(|entry| match entry {
|
||||
ClipboardEntry::String(text) => Some(text.text().to_string()),
|
||||
_ => None,
|
||||
})
|
||||
});
|
||||
|
||||
if let Some(text) = clipboard_text {
|
||||
self.push_operator(Operator::Replace, window, cx);
|
||||
self.normal_replace(Arc::from(text), window, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use gpui::ClipboardItem;
|
||||
use indoc::indoc;
|
||||
|
||||
use crate::{
|
||||
state::Mode,
|
||||
test::{NeovimBackedTestContext, VimTestContext},
|
||||
};
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_enter_and_exit_replace_mode(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
cx.simulate_keystrokes("shift-r");
|
||||
assert_eq!(cx.mode(), Mode::Replace);
|
||||
cx.simulate_keystrokes("escape");
|
||||
assert_eq!(cx.mode(), Mode::Normal);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
#[cfg(not(any(target_os = "linux", target_os = "freebsd")))]
|
||||
async fn test_replace_mode(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
// test normal replace
|
||||
cx.set_shared_state(indoc! {"
|
||||
ˇThe quick brown
|
||||
fox jumps over
|
||||
the lazy dog."})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes("shift-r O n e").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {"
|
||||
Oneˇ quick brown
|
||||
fox jumps over
|
||||
the lazy dog."});
|
||||
|
||||
// test replace with line ending
|
||||
cx.set_shared_state(indoc! {"
|
||||
The quick browˇn
|
||||
fox jumps over
|
||||
the lazy dog."})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes("shift-r O n e").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {"
|
||||
The quick browOneˇ
|
||||
fox jumps over
|
||||
the lazy dog."});
|
||||
|
||||
// test replace with blank line
|
||||
cx.set_shared_state(indoc! {"
|
||||
The quick brown
|
||||
ˇ
|
||||
fox jumps over
|
||||
the lazy dog."})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes("shift-r O n e").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {"
|
||||
The quick brown
|
||||
Oneˇ
|
||||
fox jumps over
|
||||
the lazy dog."});
|
||||
|
||||
// test replace with newline
|
||||
cx.set_shared_state(indoc! {"
|
||||
The quˇick brown
|
||||
fox jumps over
|
||||
the lazy dog."})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes("shift-r enter O n e").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {"
|
||||
The qu
|
||||
Oneˇ brown
|
||||
fox jumps over
|
||||
the lazy dog."});
|
||||
|
||||
// test replace with multi cursor and newline
|
||||
cx.set_state(
|
||||
indoc! {"
|
||||
ˇThe quick brown
|
||||
fox jumps over
|
||||
the lazy ˇdog."},
|
||||
Mode::Normal,
|
||||
);
|
||||
cx.simulate_keystrokes("shift-r O n e");
|
||||
cx.assert_state(
|
||||
indoc! {"
|
||||
Oneˇ quick brown
|
||||
fox jumps over
|
||||
the lazy Oneˇ."},
|
||||
Mode::Replace,
|
||||
);
|
||||
cx.simulate_keystrokes("enter T w o");
|
||||
cx.assert_state(
|
||||
indoc! {"
|
||||
One
|
||||
Twoˇck brown
|
||||
fox jumps over
|
||||
the lazy One
|
||||
Twoˇ"},
|
||||
Mode::Replace,
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_replace_mode_with_counts(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_shared_state("ˇhello\n").await;
|
||||
cx.simulate_shared_keystrokes("3 shift-r - escape").await;
|
||||
cx.shared_state().await.assert_eq("--ˇ-lo\n");
|
||||
|
||||
cx.set_shared_state("ˇhello\n").await;
|
||||
cx.simulate_shared_keystrokes("3 shift-r a b c escape")
|
||||
.await;
|
||||
cx.shared_state().await.assert_eq("abcabcabˇc\n");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_replace_mode_repeat(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_shared_state("ˇhello world\n").await;
|
||||
cx.simulate_shared_keystrokes("shift-r - - - escape 4 l .")
|
||||
.await;
|
||||
cx.shared_state().await.assert_eq("---lo --ˇ-ld\n");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_replace_mode_undo(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
const UNDO_REPLACE_EXAMPLES: &[&str] = &[
|
||||
// replace undo with single line
|
||||
"ˇThe quick brown fox jumps over the lazy dog.",
|
||||
// replace undo with ending line
|
||||
indoc! {"
|
||||
The quick browˇn
|
||||
fox jumps over
|
||||
the lazy dog."
|
||||
},
|
||||
// replace undo with empty line
|
||||
indoc! {"
|
||||
The quick brown
|
||||
ˇ
|
||||
fox jumps over
|
||||
the lazy dog."
|
||||
},
|
||||
];
|
||||
|
||||
for example in UNDO_REPLACE_EXAMPLES {
|
||||
// normal undo
|
||||
cx.simulate("shift-r O n e backspace backspace backspace", example)
|
||||
.await
|
||||
.assert_matches();
|
||||
// undo with new line
|
||||
cx.simulate("shift-r O enter e backspace backspace backspace", example)
|
||||
.await
|
||||
.assert_matches();
|
||||
cx.simulate(
|
||||
"shift-r O enter n enter e backspace backspace backspace backspace backspace",
|
||||
example,
|
||||
)
|
||||
.await
|
||||
.assert_matches();
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_replace_multicursor(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
cx.set_state("ˇabcˇabcabc", Mode::Normal);
|
||||
cx.simulate_keystrokes("shift-r 1 2 3 4");
|
||||
cx.assert_state("1234ˇ234ˇbc", Mode::Replace);
|
||||
assert_eq!(cx.mode(), Mode::Replace);
|
||||
cx.simulate_keystrokes("backspace backspace backspace backspace backspace");
|
||||
cx.assert_state("ˇabˇcabcabc", Mode::Replace);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_replace_undo(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
|
||||
cx.set_state("ˇaaaa", Mode::Normal);
|
||||
cx.simulate_keystrokes("0 shift-r b b b escape u");
|
||||
cx.assert_state("ˇaaaa", Mode::Normal);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_exchange_separate_range(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
|
||||
cx.set_state("ˇhello world", Mode::Normal);
|
||||
cx.simulate_keystrokes("c x i w w c x i w");
|
||||
cx.assert_state("world ˇhello", Mode::Normal);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_exchange_complete_overlap(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
|
||||
cx.set_state("ˇhello world", Mode::Normal);
|
||||
cx.simulate_keystrokes("c x x w c x i w");
|
||||
cx.assert_state("ˇworld", Mode::Normal);
|
||||
|
||||
// the focus should still be at the start of the word if we reverse the
|
||||
// order of selections (smaller -> larger)
|
||||
cx.set_state("ˇhello world", Mode::Normal);
|
||||
cx.simulate_keystrokes("c x i w c x x");
|
||||
cx.assert_state("ˇhello", Mode::Normal);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_exchange_partial_overlap(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
|
||||
cx.set_state("ˇhello world", Mode::Normal);
|
||||
cx.simulate_keystrokes("c x t r w c x i w");
|
||||
cx.assert_state("hello ˇworld", Mode::Normal);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_clear_exchange_clears_operator(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
|
||||
cx.set_state("ˇirrelevant", Mode::Normal);
|
||||
cx.simulate_keystrokes("c x c");
|
||||
|
||||
assert_eq!(cx.active_operator(), None);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_clear_exchange(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
|
||||
cx.set_state("ˇhello world", Mode::Normal);
|
||||
cx.simulate_keystrokes("c x i w c x c");
|
||||
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
let highlights = editor.all_text_background_highlights(window, cx);
|
||||
assert_eq!(0, highlights.len());
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_paste_replace(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
|
||||
cx.set_state(indoc! {"ˇ123"}, Mode::Replace);
|
||||
cx.write_to_clipboard(ClipboardItem::new_string("456".to_string()));
|
||||
cx.dispatch_action(editor::actions::Paste);
|
||||
cx.assert_state(indoc! {"45ˇ6"}, Mode::Replace);
|
||||
|
||||
// If the clipboard's contents length is greater than the remaining text
|
||||
// length, nothing sould be replace and cursor should remain in the same
|
||||
// position.
|
||||
cx.set_state(indoc! {"ˇ123"}, Mode::Replace);
|
||||
cx.write_to_clipboard(ClipboardItem::new_string("4567".to_string()));
|
||||
cx.dispatch_action(editor::actions::Paste);
|
||||
cx.assert_state(indoc! {"ˇ123"}, Mode::Replace);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
use crate::{Vim, motion::Motion, object::Object, state::Mode};
|
||||
use collections::HashMap;
|
||||
use editor::{Bias, Editor, RewrapOptions, SelectionEffects, display_map::ToDisplayPoint};
|
||||
use gpui::{Action, Context, Window};
|
||||
use language::SelectionGoal;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
|
||||
/// Rewraps the selected text to fit within the line width.
|
||||
#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
|
||||
#[action(namespace = vim)]
|
||||
pub(crate) struct Rewrap {
|
||||
pub line_length: Option<usize>,
|
||||
}
|
||||
|
||||
pub(crate) fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
|
||||
Vim::action(editor, cx, |vim, action: &Rewrap, window, cx| {
|
||||
vim.record_current_action(cx);
|
||||
Vim::take_count(cx);
|
||||
Vim::take_forced_motion(cx);
|
||||
vim.store_visual_marks(window, cx);
|
||||
vim.update_editor(cx, |vim, editor, cx| {
|
||||
editor.transact(window, cx, |editor, window, cx| {
|
||||
let mut positions = vim.save_selection_starts(editor, cx);
|
||||
editor.rewrap(
|
||||
RewrapOptions {
|
||||
override_language_settings: true,
|
||||
line_length: action.line_length,
|
||||
..Default::default()
|
||||
},
|
||||
cx,
|
||||
);
|
||||
editor.change_selections(Default::default(), window, cx, |s| {
|
||||
s.move_with(&mut |map, selection| {
|
||||
if let Some(anchor) = positions.remove(&selection.id) {
|
||||
let mut point = anchor.to_display_point(map);
|
||||
*point.column_mut() = 0;
|
||||
selection.collapse_to(point, SelectionGoal::None);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
if vim.mode.is_visual() {
|
||||
vim.switch_mode(Mode::Normal, true, window, cx)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
impl Vim {
|
||||
pub(crate) fn rewrap_motion(
|
||||
&mut self,
|
||||
motion: Motion,
|
||||
times: Option<usize>,
|
||||
forced_motion: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.stop_recording(cx);
|
||||
self.update_editor(cx, |_, editor, cx| {
|
||||
let text_layout_details = editor.text_layout_details(window, cx);
|
||||
editor.transact(window, cx, |editor, window, cx| {
|
||||
let mut selection_starts: HashMap<_, _> = Default::default();
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
s.move_with(&mut |map, selection| {
|
||||
let anchor = map.display_point_to_anchor(selection.head(), Bias::Right);
|
||||
selection_starts.insert(selection.id, anchor);
|
||||
motion.expand_selection(
|
||||
map,
|
||||
selection,
|
||||
times,
|
||||
&text_layout_details,
|
||||
forced_motion,
|
||||
);
|
||||
});
|
||||
});
|
||||
editor.rewrap(
|
||||
RewrapOptions {
|
||||
override_language_settings: true,
|
||||
..Default::default()
|
||||
},
|
||||
cx,
|
||||
);
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
s.move_with(&mut |map, selection| {
|
||||
let anchor = selection_starts.remove(&selection.id).unwrap();
|
||||
let mut point = anchor.to_display_point(map);
|
||||
*point.column_mut() = 0;
|
||||
selection.collapse_to(point, SelectionGoal::None);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn rewrap_object(
|
||||
&mut self,
|
||||
object: Object,
|
||||
around: bool,
|
||||
times: Option<usize>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.stop_recording(cx);
|
||||
self.update_editor(cx, |_, editor, cx| {
|
||||
editor.transact(window, cx, |editor, window, cx| {
|
||||
let mut original_positions: HashMap<_, _> = Default::default();
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
s.move_with(&mut |map, selection| {
|
||||
let anchor = map.display_point_to_anchor(selection.head(), Bias::Right);
|
||||
original_positions.insert(selection.id, anchor);
|
||||
object.expand_selection(map, selection, around, times);
|
||||
});
|
||||
});
|
||||
editor.rewrap(
|
||||
RewrapOptions {
|
||||
override_language_settings: true,
|
||||
..Default::default()
|
||||
},
|
||||
cx,
|
||||
);
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
s.move_with(&mut |map, selection| {
|
||||
let anchor = original_positions.remove(&selection.id).unwrap();
|
||||
let mut point = anchor.to_display_point(map);
|
||||
*point.column_mut() = 0;
|
||||
selection.collapse_to(point, SelectionGoal::None);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::test::NeovimBackedTestContext;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_indent_gv(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
cx.set_neovim_option("shiftwidth=4").await;
|
||||
|
||||
cx.set_shared_state("ˇhello\nworld\n").await;
|
||||
cx.simulate_shared_keystrokes("v j > g v").await;
|
||||
cx.shared_state()
|
||||
.await
|
||||
.assert_eq("« hello\n ˇ» world\n");
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,426 @@
|
||||
use gpui::{AppContext as _, UpdateGlobal, px, size};
|
||||
use indoc::indoc;
|
||||
use settings::SettingsStore;
|
||||
use std::{
|
||||
ops::{Deref, DerefMut},
|
||||
panic, thread,
|
||||
};
|
||||
|
||||
use language::language_settings::SoftWrap;
|
||||
use util::test::marked_text_offsets;
|
||||
|
||||
use super::{VimTestContext, neovim_connection::NeovimConnection};
|
||||
use crate::state::{Mode, VimGlobals};
|
||||
|
||||
pub struct NeovimBackedTestContext {
|
||||
pub(crate) cx: VimTestContext,
|
||||
pub(crate) neovim: NeovimConnection,
|
||||
|
||||
last_set_state: Option<String>,
|
||||
recent_keystrokes: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct SharedState {
|
||||
neovim: String,
|
||||
editor: String,
|
||||
initial: String,
|
||||
neovim_mode: Mode,
|
||||
editor_mode: Mode,
|
||||
recent_keystrokes: String,
|
||||
}
|
||||
|
||||
impl SharedState {
|
||||
/// Assert that both Zed and NeoVim have the same content and mode.
|
||||
#[track_caller]
|
||||
pub fn assert_matches(&self) {
|
||||
if self.neovim != self.editor || self.neovim_mode != self.editor_mode {
|
||||
panic!(
|
||||
indoc! {"Test failed (zed does not match nvim behavior)
|
||||
# initial state:
|
||||
{}
|
||||
# keystrokes:
|
||||
{}
|
||||
# neovim ({}):
|
||||
{}
|
||||
# zed ({}):
|
||||
{}"},
|
||||
self.initial,
|
||||
self.recent_keystrokes,
|
||||
self.neovim_mode,
|
||||
self.neovim,
|
||||
self.editor_mode,
|
||||
self.editor,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn assert_eq(&mut self, marked_text: &str) {
|
||||
let marked_text = marked_text.replace('•', " ");
|
||||
if self.neovim == marked_text
|
||||
&& self.neovim == self.editor
|
||||
&& self.neovim_mode == self.editor_mode
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let message = if self.neovim != marked_text {
|
||||
"Test is incorrect (currently expected != neovim_state)"
|
||||
} else {
|
||||
"Editor does not match nvim behavior"
|
||||
};
|
||||
panic!(
|
||||
indoc! {"{}
|
||||
# initial state:
|
||||
{}
|
||||
# keystrokes:
|
||||
{}
|
||||
# currently expected:
|
||||
{}
|
||||
# neovim ({}):
|
||||
{}
|
||||
# zed ({}):
|
||||
{}"},
|
||||
message,
|
||||
self.initial,
|
||||
self.recent_keystrokes,
|
||||
marked_text.replace(" \n", "•\n"),
|
||||
self.neovim_mode,
|
||||
self.neovim.replace(" \n", "•\n"),
|
||||
self.editor_mode,
|
||||
self.editor.replace(" \n", "•\n"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SharedClipboard {
|
||||
register: char,
|
||||
neovim: String,
|
||||
editor: String,
|
||||
state: SharedState,
|
||||
}
|
||||
|
||||
impl SharedClipboard {
|
||||
#[track_caller]
|
||||
pub fn assert_eq(&self, expected: &str) {
|
||||
if expected == self.neovim && self.neovim == self.editor {
|
||||
return;
|
||||
}
|
||||
|
||||
let message = if expected != self.neovim {
|
||||
"Test is incorrect (currently expected != neovim_state)"
|
||||
} else {
|
||||
"Editor does not match nvim behavior"
|
||||
};
|
||||
|
||||
panic!(
|
||||
indoc! {"{}
|
||||
# initial state:
|
||||
{}
|
||||
# keystrokes:
|
||||
{}
|
||||
# currently expected: {:?}
|
||||
# neovim register \"{}: {:?}
|
||||
# zed register \"{}: {:?}"},
|
||||
message,
|
||||
self.state.initial,
|
||||
self.state.recent_keystrokes,
|
||||
expected,
|
||||
self.register,
|
||||
self.neovim,
|
||||
self.register,
|
||||
self.editor
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl NeovimBackedTestContext {
|
||||
pub async fn new(cx: &mut gpui::TestAppContext) -> NeovimBackedTestContext {
|
||||
#[cfg(feature = "neovim")]
|
||||
cx.executor().allow_parking();
|
||||
// rust stores the name of the test on the current thread.
|
||||
// We use this to automatically name a file that will store
|
||||
// the neovim connection's requests/responses so that we can
|
||||
// run without neovim on CI.
|
||||
let thread = thread::current();
|
||||
let test_name = thread
|
||||
.name()
|
||||
.expect("thread is not named")
|
||||
.split(':')
|
||||
.next_back()
|
||||
.unwrap()
|
||||
.to_string();
|
||||
Self {
|
||||
cx: VimTestContext::new(cx, true).await,
|
||||
neovim: NeovimConnection::new(test_name).await,
|
||||
|
||||
last_set_state: None,
|
||||
recent_keystrokes: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn new_html(cx: &mut gpui::TestAppContext) -> NeovimBackedTestContext {
|
||||
#[cfg(feature = "neovim")]
|
||||
cx.executor().allow_parking();
|
||||
// rust stores the name of the test on the current thread.
|
||||
// We use this to automatically name a file that will store
|
||||
// the neovim connection's requests/responses so that we can
|
||||
// run without neovim on CI.
|
||||
let thread = thread::current();
|
||||
let test_name = thread
|
||||
.name()
|
||||
.expect("thread is not named")
|
||||
.split(':')
|
||||
.next_back()
|
||||
.unwrap()
|
||||
.to_string();
|
||||
Self {
|
||||
cx: VimTestContext::new_html(cx).await,
|
||||
neovim: NeovimConnection::new(test_name).await,
|
||||
|
||||
last_set_state: None,
|
||||
recent_keystrokes: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn new_markdown_with_rust(cx: &mut gpui::TestAppContext) -> NeovimBackedTestContext {
|
||||
#[cfg(feature = "neovim")]
|
||||
cx.executor().allow_parking();
|
||||
let thread = thread::current();
|
||||
let test_name = thread
|
||||
.name()
|
||||
.expect("thread is not named")
|
||||
.split(':')
|
||||
.next_back()
|
||||
.unwrap()
|
||||
.to_string();
|
||||
Self {
|
||||
cx: VimTestContext::new_markdown_with_rust(cx).await,
|
||||
neovim: NeovimConnection::new(test_name).await,
|
||||
|
||||
last_set_state: None,
|
||||
recent_keystrokes: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn new_typescript(cx: &mut gpui::TestAppContext) -> NeovimBackedTestContext {
|
||||
#[cfg(feature = "neovim")]
|
||||
cx.executor().allow_parking();
|
||||
// rust stores the name of the test on the current thread.
|
||||
// We use this to automatically name a file that will store
|
||||
// the neovim connection's requests/responses so that we can
|
||||
// run without neovim on CI.
|
||||
let thread = thread::current();
|
||||
let test_name = thread
|
||||
.name()
|
||||
.expect("thread is not named")
|
||||
.split(':')
|
||||
.next_back()
|
||||
.unwrap()
|
||||
.to_string();
|
||||
Self {
|
||||
cx: VimTestContext::new_typescript(cx).await,
|
||||
neovim: NeovimConnection::new(test_name).await,
|
||||
|
||||
last_set_state: None,
|
||||
recent_keystrokes: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn new_tsx(cx: &mut gpui::TestAppContext) -> NeovimBackedTestContext {
|
||||
#[cfg(feature = "neovim")]
|
||||
cx.executor().allow_parking();
|
||||
let thread = thread::current();
|
||||
let test_name = thread
|
||||
.name()
|
||||
.expect("thread is not named")
|
||||
.split(':')
|
||||
.next_back()
|
||||
.unwrap()
|
||||
.to_string();
|
||||
Self {
|
||||
cx: VimTestContext::new_tsx(cx).await,
|
||||
neovim: NeovimConnection::new(test_name).await,
|
||||
|
||||
last_set_state: None,
|
||||
recent_keystrokes: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn set_shared_state(&mut self, marked_text: &str) {
|
||||
let mode = if marked_text.contains('»') {
|
||||
Mode::Visual
|
||||
} else {
|
||||
Mode::Normal
|
||||
};
|
||||
self.set_state(marked_text, mode);
|
||||
self.last_set_state = Some(marked_text.to_string());
|
||||
self.recent_keystrokes = Vec::new();
|
||||
self.neovim.set_state(marked_text).await;
|
||||
}
|
||||
|
||||
pub async fn simulate_shared_keystrokes(&mut self, keystroke_texts: &str) {
|
||||
for keystroke_text in keystroke_texts.split(' ') {
|
||||
self.recent_keystrokes.push(keystroke_text.to_string());
|
||||
self.neovim.send_keystroke(keystroke_text).await;
|
||||
}
|
||||
self.simulate_keystrokes(keystroke_texts);
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub async fn simulate(&mut self, keystrokes: &str, initial_state: &str) -> SharedState {
|
||||
self.set_shared_state(initial_state).await;
|
||||
self.simulate_shared_keystrokes(keystrokes).await;
|
||||
self.shared_state().await
|
||||
}
|
||||
|
||||
pub async fn set_shared_wrap(&mut self, columns: u32) {
|
||||
if columns < 12 {
|
||||
panic!("nvim doesn't support columns < 12")
|
||||
}
|
||||
self.neovim.set_option("wrap").await;
|
||||
self.neovim
|
||||
.set_option(&format!("columns={}", columns))
|
||||
.await;
|
||||
|
||||
self.update(|_, cx| {
|
||||
SettingsStore::update_global(cx, |settings, cx| {
|
||||
settings.update_user_settings(cx, |settings| {
|
||||
settings.project.all_languages.defaults.soft_wrap = Some(SoftWrap::Bounded);
|
||||
settings
|
||||
.project
|
||||
.all_languages
|
||||
.defaults
|
||||
.preferred_line_length = Some(columns);
|
||||
});
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn set_scroll_height(&mut self, rows: u32) {
|
||||
// match Zed's scrolling behavior
|
||||
self.neovim.set_option(&format!("scrolloff={}", 3)).await;
|
||||
// +2 to account for the vim command UI at the bottom.
|
||||
self.neovim.set_option(&format!("lines={}", rows + 2)).await;
|
||||
let (line_height, visible_line_count) = self.update_editor(|editor, window, cx| {
|
||||
(
|
||||
editor
|
||||
.style(cx)
|
||||
.text
|
||||
.line_height_in_pixels(window.rem_size()),
|
||||
editor.visible_line_count().unwrap(),
|
||||
)
|
||||
});
|
||||
|
||||
let window = self.window;
|
||||
let margin = self
|
||||
.update_window(window, |_, window, _cx| {
|
||||
window.viewport_size().height - line_height * (visible_line_count as f32)
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
self.simulate_window_resize(
|
||||
self.window,
|
||||
size(px(1000.), margin + (rows as f32) * line_height),
|
||||
);
|
||||
}
|
||||
|
||||
pub async fn set_neovim_option(&mut self, option: &str) {
|
||||
self.neovim.set_option(option).await;
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub async fn shared_clipboard(&mut self) -> SharedClipboard {
|
||||
SharedClipboard {
|
||||
register: '"',
|
||||
state: self.shared_state().await,
|
||||
neovim: self.neovim.read_register('"').await,
|
||||
editor: self.read_from_clipboard().unwrap().text().unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub async fn shared_register(&mut self, register: char) -> SharedClipboard {
|
||||
SharedClipboard {
|
||||
register,
|
||||
state: self.shared_state().await,
|
||||
neovim: self.neovim.read_register(register).await,
|
||||
editor: self.update(|_, cx| {
|
||||
cx.global::<VimGlobals>()
|
||||
.registers
|
||||
.get(®ister)
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
.text
|
||||
.into()
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub async fn shared_state(&mut self) -> SharedState {
|
||||
let (mode, marked_text) = self.neovim.state().await;
|
||||
SharedState {
|
||||
neovim: marked_text,
|
||||
neovim_mode: mode,
|
||||
editor: self.editor_state(),
|
||||
editor_mode: self.mode(),
|
||||
initial: self
|
||||
.last_set_state
|
||||
.as_ref()
|
||||
.cloned()
|
||||
.unwrap_or("N/A".to_string()),
|
||||
recent_keystrokes: self.recent_keystrokes.join(" "),
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub async fn simulate_at_each_offset(
|
||||
&mut self,
|
||||
keystrokes: &str,
|
||||
marked_positions: &str,
|
||||
) -> SharedState {
|
||||
let (unmarked_text, cursor_offsets) = marked_text_offsets(marked_positions);
|
||||
|
||||
for cursor_offset in cursor_offsets.iter() {
|
||||
let mut marked_text = unmarked_text.clone();
|
||||
marked_text.insert(*cursor_offset, 'ˇ');
|
||||
|
||||
let state = self.simulate(keystrokes, &marked_text).await;
|
||||
if state.neovim != state.editor || state.neovim_mode != state.editor_mode {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
SharedState::default()
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for NeovimBackedTestContext {
|
||||
type Target = VimTestContext;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.cx
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for NeovimBackedTestContext {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.cx
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::test::NeovimBackedTestContext;
|
||||
use gpui::TestAppContext;
|
||||
|
||||
#[gpui::test]
|
||||
async fn neovim_backed_test_context_works(cx: &mut TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
cx.shared_state().await.assert_matches();
|
||||
cx.set_shared_state("This is a tesˇt").await;
|
||||
cx.shared_state().await.assert_matches();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,624 @@
|
||||
use std::path::PathBuf;
|
||||
#[cfg(feature = "neovim")]
|
||||
use std::{
|
||||
cmp,
|
||||
ops::{Deref, DerefMut, Range},
|
||||
};
|
||||
|
||||
#[cfg(feature = "neovim")]
|
||||
use async_compat::Compat;
|
||||
#[cfg(feature = "neovim")]
|
||||
use async_trait::async_trait;
|
||||
#[cfg(feature = "neovim")]
|
||||
use gpui::Keystroke;
|
||||
|
||||
#[cfg(feature = "neovim")]
|
||||
use language::Point;
|
||||
|
||||
#[cfg(feature = "neovim")]
|
||||
use nvim_rs::{
|
||||
Handler, Neovim, UiAttachOptions, Value, create::tokio::new_child_cmd, error::LoopError,
|
||||
};
|
||||
#[cfg(feature = "neovim")]
|
||||
use parking_lot::ReentrantMutex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
#[cfg(feature = "neovim")]
|
||||
use tokio::{
|
||||
process::{Child, ChildStdin, Command},
|
||||
task::JoinHandle,
|
||||
};
|
||||
|
||||
use crate::state::Mode;
|
||||
use collections::VecDeque;
|
||||
|
||||
// Neovim doesn't like to be started simultaneously from multiple threads. We use this lock
|
||||
// to ensure we are only constructing one neovim connection at a time.
|
||||
#[cfg(feature = "neovim")]
|
||||
static NEOVIM_LOCK: ReentrantMutex<()> = ReentrantMutex::new(());
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub enum NeovimData {
|
||||
Put { state: String },
|
||||
Key(String),
|
||||
Get { state: String, mode: Mode },
|
||||
ReadRegister { name: char, value: String },
|
||||
Exec { command: String },
|
||||
SetOption { value: String },
|
||||
}
|
||||
|
||||
pub struct NeovimConnection {
|
||||
data: VecDeque<NeovimData>,
|
||||
#[cfg(feature = "neovim")]
|
||||
test_case_id: String,
|
||||
#[cfg(feature = "neovim")]
|
||||
nvim: Neovim<nvim_rs::compat::tokio::Compat<ChildStdin>>,
|
||||
#[cfg(feature = "neovim")]
|
||||
_join_handle: JoinHandle<Result<(), Box<LoopError>>>,
|
||||
#[cfg(feature = "neovim")]
|
||||
_child: Child,
|
||||
}
|
||||
|
||||
impl NeovimConnection {
|
||||
pub async fn new(mut test_case_id: String) -> Self {
|
||||
// When running under perf, don't create duplicate files.
|
||||
if cfg!(perf_enabled) {
|
||||
if test_case_id.ends_with(perf::consts::SUF_NORMAL) {
|
||||
test_case_id.truncate(test_case_id.len() - perf::consts::SUF_NORMAL.len());
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "neovim")]
|
||||
let handler = NvimHandler {};
|
||||
#[cfg(feature = "neovim")]
|
||||
let (nvim, join_handle, child) = Compat::new(async {
|
||||
// Ensure we don't create neovim connections in parallel
|
||||
let _lock = NEOVIM_LOCK.lock();
|
||||
let (nvim, join_handle, child) = new_child_cmd(
|
||||
Command::new("nvim")
|
||||
.arg("--embed")
|
||||
.arg("--clean")
|
||||
// disable swap (otherwise after about 1000 test runs you run out of swap file names)
|
||||
.arg("-n")
|
||||
// disable writing files (just in case)
|
||||
.arg("-m"),
|
||||
handler,
|
||||
)
|
||||
.await
|
||||
.expect("Could not connect to neovim process");
|
||||
|
||||
nvim.ui_attach(100, 100, &UiAttachOptions::default())
|
||||
.await
|
||||
.expect("Could not attach to ui");
|
||||
|
||||
// Makes system act a little more like zed in terms of indentation
|
||||
nvim.set_option("smartindent", nvim_rs::Value::Boolean(true))
|
||||
.await
|
||||
.expect("Could not set smartindent on startup");
|
||||
|
||||
(nvim, join_handle, child)
|
||||
})
|
||||
.await;
|
||||
|
||||
Self {
|
||||
#[cfg(feature = "neovim")]
|
||||
data: Default::default(),
|
||||
#[cfg(not(feature = "neovim"))]
|
||||
data: Self::read_test_data(&test_case_id),
|
||||
#[cfg(feature = "neovim")]
|
||||
test_case_id,
|
||||
#[cfg(feature = "neovim")]
|
||||
nvim,
|
||||
#[cfg(feature = "neovim")]
|
||||
_join_handle: join_handle,
|
||||
#[cfg(feature = "neovim")]
|
||||
_child: child,
|
||||
}
|
||||
}
|
||||
|
||||
// Sends a keystroke to the neovim process.
|
||||
#[cfg(feature = "neovim")]
|
||||
pub async fn send_keystroke(&mut self, keystroke_text: &str) {
|
||||
let mut keystroke = Keystroke::parse(keystroke_text).unwrap();
|
||||
|
||||
if keystroke.key == "<" {
|
||||
keystroke.key = "lt".to_string()
|
||||
}
|
||||
|
||||
let special = keystroke.modifiers.shift
|
||||
|| keystroke.modifiers.control
|
||||
|| keystroke.modifiers.alt
|
||||
|| keystroke.modifiers.platform
|
||||
|| keystroke.key.len() > 1;
|
||||
let start = if special { "<" } else { "" };
|
||||
let shift = if keystroke.modifiers.shift { "S-" } else { "" };
|
||||
let ctrl = if keystroke.modifiers.control {
|
||||
"C-"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
let alt = if keystroke.modifiers.alt { "M-" } else { "" };
|
||||
let cmd = if keystroke.modifiers.platform {
|
||||
"D-"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
let end = if special { ">" } else { "" };
|
||||
|
||||
let key = format!("{start}{shift}{ctrl}{alt}{cmd}{}{end}", keystroke.key);
|
||||
|
||||
self.data
|
||||
.push_back(NeovimData::Key(keystroke_text.to_string()));
|
||||
self.nvim
|
||||
.input(&key)
|
||||
.await
|
||||
.expect("Could not input keystroke");
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "neovim"))]
|
||||
pub async fn send_keystroke(&mut self, keystroke_text: &str) {
|
||||
if matches!(self.data.front(), Some(NeovimData::Get { .. })) {
|
||||
self.data.pop_front();
|
||||
}
|
||||
assert_eq!(
|
||||
self.data.pop_front(),
|
||||
Some(NeovimData::Key(keystroke_text.to_string())),
|
||||
"operation does not match recorded script. re-record with --features=neovim"
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "neovim")]
|
||||
pub async fn set_state(&mut self, marked_text: &str) {
|
||||
let (text, selections) = parse_state(marked_text);
|
||||
|
||||
let nvim_buffer = self
|
||||
.nvim
|
||||
.get_current_buf()
|
||||
.await
|
||||
.expect("Could not get neovim buffer");
|
||||
let lines = text
|
||||
.split('\n')
|
||||
.map(|line| line.to_string())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
nvim_buffer
|
||||
.set_lines(0, -1, false, lines)
|
||||
.await
|
||||
.expect("Could not set nvim buffer text");
|
||||
|
||||
self.nvim
|
||||
.input("<escape>")
|
||||
.await
|
||||
.expect("Could not send escape to nvim");
|
||||
self.nvim
|
||||
.input("<escape>")
|
||||
.await
|
||||
.expect("Could not send escape to nvim");
|
||||
|
||||
let nvim_window = self
|
||||
.nvim
|
||||
.get_current_win()
|
||||
.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))
|
||||
.await
|
||||
.expect("Could not set nvim cursor position");
|
||||
|
||||
if !selection.is_empty() {
|
||||
self.nvim
|
||||
.input("v")
|
||||
.await
|
||||
.expect("could not enter visual mode");
|
||||
|
||||
let cursor = selection.end;
|
||||
nvim_window
|
||||
.set_cursor((cursor.row as i64 + 1, cursor.column as i64))
|
||||
.await
|
||||
.expect("Could not set nvim cursor position");
|
||||
}
|
||||
|
||||
if let Some(NeovimData::Get { mode, state }) = self.data.back()
|
||||
&& *mode == Mode::Normal
|
||||
&& *state == marked_text
|
||||
{
|
||||
return;
|
||||
}
|
||||
self.data.push_back(NeovimData::Put {
|
||||
state: marked_text.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "neovim"))]
|
||||
pub async fn set_state(&mut self, marked_text: &str) {
|
||||
if let Some(NeovimData::Get { mode, state: text }) = self.data.front() {
|
||||
if *mode == Mode::Normal && *text == marked_text {
|
||||
return;
|
||||
}
|
||||
self.data.pop_front();
|
||||
}
|
||||
assert_eq!(
|
||||
self.data.pop_front(),
|
||||
Some(NeovimData::Put {
|
||||
state: marked_text.to_string()
|
||||
}),
|
||||
"operation does not match recorded script. re-record with --features=neovim"
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "neovim")]
|
||||
pub async fn set_option(&mut self, value: &str) {
|
||||
self.nvim
|
||||
.command_output(format!("set {}", value).as_str())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
self.data.push_back(NeovimData::SetOption {
|
||||
value: value.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "neovim"))]
|
||||
pub async fn set_option(&mut self, value: &str) {
|
||||
if let Some(NeovimData::Get { .. }) = self.data.front() {
|
||||
self.data.pop_front();
|
||||
};
|
||||
assert_eq!(
|
||||
self.data.pop_front(),
|
||||
Some(NeovimData::SetOption {
|
||||
value: value.to_string(),
|
||||
}),
|
||||
"operation does not match recorded script. re-record with --features=neovim"
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "neovim")]
|
||||
pub async fn exec(&mut self, value: &str) {
|
||||
self.nvim.command_output(value).await.unwrap();
|
||||
|
||||
self.data.push_back(NeovimData::Exec {
|
||||
command: value.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "neovim"))]
|
||||
pub async fn exec(&mut self, value: &str) {
|
||||
if let Some(NeovimData::Get { .. }) = self.data.front() {
|
||||
self.data.pop_front();
|
||||
};
|
||||
assert_eq!(
|
||||
self.data.pop_front(),
|
||||
Some(NeovimData::Exec {
|
||||
command: value.to_string(),
|
||||
}),
|
||||
"operation does not match recorded script. re-record with --features=neovim"
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "neovim"))]
|
||||
pub async fn read_register(&mut self, register: char) -> String {
|
||||
if let Some(NeovimData::Get { .. }) = self.data.front() {
|
||||
self.data.pop_front();
|
||||
};
|
||||
if let Some(NeovimData::ReadRegister { name, value }) = self.data.pop_front()
|
||||
&& name == register
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
panic!("operation does not match recorded script. re-record with --features=neovim")
|
||||
}
|
||||
|
||||
#[cfg(feature = "neovim")]
|
||||
pub async fn read_register(&mut self, name: char) -> String {
|
||||
let value = self
|
||||
.nvim
|
||||
.command_output(format!("echo getreg('{}')", name).as_str())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
self.data.push_back(NeovimData::ReadRegister {
|
||||
name,
|
||||
value: value.clone(),
|
||||
});
|
||||
|
||||
value
|
||||
}
|
||||
|
||||
#[cfg(feature = "neovim")]
|
||||
async fn read_position(&mut self, cmd: &str) -> u32 {
|
||||
self.nvim
|
||||
.command_output(cmd)
|
||||
.await
|
||||
.unwrap()
|
||||
.parse::<u32>()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[cfg(feature = "neovim")]
|
||||
pub async fn state(&mut self) -> (Mode, String) {
|
||||
let nvim_buffer = self
|
||||
.nvim
|
||||
.get_current_buf()
|
||||
.await
|
||||
.expect("Could not get neovim buffer");
|
||||
let text = nvim_buffer
|
||||
.get_lines(0, -1, false)
|
||||
.await
|
||||
.expect("Could not get buffer text")
|
||||
.join("\n");
|
||||
|
||||
// nvim columns are 1-based, so -1.
|
||||
let mut cursor_row = self.read_position("echo line('.')").await - 1;
|
||||
let mut cursor_col = self.read_position("echo col('.')").await - 1;
|
||||
let mut selection_row = self.read_position("echo line('v')").await - 1;
|
||||
let mut selection_col = self.read_position("echo col('v')").await - 1;
|
||||
let total_rows = self.read_position("echo line('$')").await - 1;
|
||||
|
||||
let nvim_mode_text = self
|
||||
.nvim
|
||||
.get_mode()
|
||||
.await
|
||||
.expect("Could not get mode")
|
||||
.into_iter()
|
||||
.find_map(|(key, value)| {
|
||||
if key.as_str() == Some("mode") {
|
||||
Some(value.as_str().unwrap().to_owned())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.expect("Could not find mode value");
|
||||
|
||||
let mode = match nvim_mode_text.as_ref() {
|
||||
"i" => Mode::Insert,
|
||||
"n" => Mode::Normal,
|
||||
"v" => Mode::Visual,
|
||||
"V" => Mode::VisualLine,
|
||||
"R" => Mode::Replace,
|
||||
"\x16" => Mode::VisualBlock,
|
||||
_ => panic!("unexpected vim mode: {nvim_mode_text}"),
|
||||
};
|
||||
|
||||
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 {
|
||||
Mode::VisualBlock if selection_row != cursor_row => {
|
||||
// in zed we fake a block selection 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Mode::Visual | Mode::VisualLine | Mode::VisualBlock => {
|
||||
if (selection_row, selection_col) > (cursor_row, cursor_col) {
|
||||
let selection_line_length =
|
||||
self.read_position("echo strlen(getline(line('v')))").await;
|
||||
if selection_line_length > selection_col {
|
||||
selection_col += 1;
|
||||
} else if selection_row < total_rows {
|
||||
selection_col = 0;
|
||||
selection_row += 1;
|
||||
}
|
||||
} else {
|
||||
let cursor_line_length =
|
||||
self.read_position("echo strlen(getline(line('.')))").await;
|
||||
if cursor_line_length > cursor_col {
|
||||
cursor_col += 1;
|
||||
} else if cursor_row < total_rows {
|
||||
cursor_col = 0;
|
||||
cursor_row += 1;
|
||||
}
|
||||
}
|
||||
selections.push(
|
||||
Point::new(selection_row, selection_col)..Point::new(cursor_row, cursor_col),
|
||||
)
|
||||
}
|
||||
Mode::Insert | Mode::Normal | Mode::Replace => selections
|
||||
.push(Point::new(selection_row, selection_col)..Point::new(cursor_row, cursor_col)),
|
||||
Mode::HelixNormal | Mode::HelixSelect => unreachable!(),
|
||||
}
|
||||
|
||||
let ranges = encode_ranges(&text, &selections);
|
||||
let state = NeovimData::Get {
|
||||
mode,
|
||||
state: ranges.clone(),
|
||||
};
|
||||
|
||||
if self.data.back() != Some(&state) {
|
||||
self.data.push_back(state);
|
||||
}
|
||||
|
||||
(mode, ranges)
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "neovim"))]
|
||||
pub async fn state(&mut self) -> (Mode, String) {
|
||||
if let Some(NeovimData::Get { state: raw, mode }) = self.data.front() {
|
||||
(*mode, raw.to_string())
|
||||
} else {
|
||||
panic!("operation does not match recorded script. re-record with --features=neovim");
|
||||
}
|
||||
}
|
||||
|
||||
fn test_data_path(test_case_id: &str) -> PathBuf {
|
||||
let mut data_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
data_path.push("test_data");
|
||||
data_path.push(format!("{}.json", test_case_id));
|
||||
data_path
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "neovim"))]
|
||||
fn read_test_data(test_case_id: &str) -> VecDeque<NeovimData> {
|
||||
let path = Self::test_data_path(test_case_id);
|
||||
let json = std::fs::read_to_string(path).expect(
|
||||
"Could not read test data. Is it generated? Try running test with '--features neovim'",
|
||||
);
|
||||
|
||||
let mut result = VecDeque::new();
|
||||
for line in json.lines() {
|
||||
result.push_back(
|
||||
serde_json::from_str(line)
|
||||
.expect("invalid test data. regenerate it with '--features neovim'"),
|
||||
);
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
#[cfg(feature = "neovim")]
|
||||
fn write_test_data(test_case_id: &str, data: &VecDeque<NeovimData>) {
|
||||
let path = Self::test_data_path(test_case_id);
|
||||
let mut json = Vec::new();
|
||||
for entry in data {
|
||||
serde_json::to_writer(&mut json, entry).unwrap();
|
||||
json.push(b'\n');
|
||||
}
|
||||
std::fs::create_dir_all(path.parent().unwrap())
|
||||
.expect("could not create test data directory");
|
||||
std::fs::write(path, json).expect("could not write out test data");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "neovim")]
|
||||
impl Deref for NeovimConnection {
|
||||
type Target = Neovim<nvim_rs::compat::tokio::Compat<ChildStdin>>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.nvim
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "neovim")]
|
||||
impl DerefMut for NeovimConnection {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.nvim
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "neovim")]
|
||||
impl Drop for NeovimConnection {
|
||||
fn drop(&mut self) {
|
||||
Self::write_test_data(&self.test_case_id, &self.data);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "neovim")]
|
||||
#[derive(Clone)]
|
||||
struct NvimHandler {}
|
||||
|
||||
#[cfg(feature = "neovim")]
|
||||
#[async_trait]
|
||||
impl Handler for NvimHandler {
|
||||
type Writer = nvim_rs::compat::tokio::Compat<ChildStdin>;
|
||||
|
||||
async fn handle_request(
|
||||
&self,
|
||||
_event_name: String,
|
||||
_arguments: Vec<Value>,
|
||||
_neovim: Neovim<Self::Writer>,
|
||||
) -> Result<Value, Value> {
|
||||
unimplemented!();
|
||||
}
|
||||
|
||||
async fn handle_notify(
|
||||
&self,
|
||||
_event_name: String,
|
||||
_arguments: Vec<Value>,
|
||||
_neovim: Neovim<Self::Writer>,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "neovim")]
|
||||
fn parse_state(marked_text: &str) -> (String, Vec<Range<Point>>) {
|
||||
let (text, ranges) = util::test::marked_text_ranges(marked_text, true);
|
||||
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_ranges(text: &str, point_ranges: &Vec<Range<Point>>) -> String {
|
||||
let byte_ranges = point_ranges
|
||||
.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<_>>();
|
||||
util::test::generate_marked_text(text, &byte_ranges[..], true)
|
||||
}
|
||||
@@ -0,0 +1,291 @@
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
use editor::test::editor_lsp_test_context::EditorLspTestContext;
|
||||
use gpui::{Context, Entity, UpdateGlobal};
|
||||
use search::{BufferSearchBar, project_search::ProjectSearchBar};
|
||||
use semver::Version;
|
||||
|
||||
use crate::{state::Operator, *};
|
||||
|
||||
pub struct VimTestContext {
|
||||
cx: EditorLspTestContext,
|
||||
}
|
||||
|
||||
impl VimTestContext {
|
||||
pub fn init(cx: &mut gpui::TestAppContext) {
|
||||
if cx.has_global::<VimGlobals>() {
|
||||
return;
|
||||
}
|
||||
env_logger::try_init().ok();
|
||||
cx.update(|cx| {
|
||||
let settings = SettingsStore::test(cx);
|
||||
cx.set_global(settings);
|
||||
release_channel::init(Version::new(0, 0, 0), cx);
|
||||
command_palette::init(cx);
|
||||
project_panel::init(cx);
|
||||
outline_panel::init(cx);
|
||||
git_ui::init(cx);
|
||||
crate::init(cx);
|
||||
search::init(cx);
|
||||
theme_settings::init(theme::LoadThemes::JustBase, cx);
|
||||
settings_ui::init(cx);
|
||||
markdown_preview::init(cx);
|
||||
zed_actions::init();
|
||||
});
|
||||
}
|
||||
|
||||
pub async fn new(cx: &mut gpui::TestAppContext, enabled: bool) -> VimTestContext {
|
||||
Self::init(cx);
|
||||
let lsp = EditorLspTestContext::new_rust(Default::default(), cx).await;
|
||||
Self::new_with_lsp(lsp, enabled)
|
||||
}
|
||||
|
||||
pub async fn new_html(cx: &mut gpui::TestAppContext) -> VimTestContext {
|
||||
Self::init(cx);
|
||||
Self::new_with_lsp(EditorLspTestContext::new_html(cx).await, true)
|
||||
}
|
||||
|
||||
pub async fn new_markdown_with_rust(cx: &mut gpui::TestAppContext) -> VimTestContext {
|
||||
Self::init(cx);
|
||||
Self::new_with_lsp(EditorLspTestContext::new_markdown_with_rust(cx).await, true)
|
||||
}
|
||||
|
||||
pub async fn new_typescript(cx: &mut gpui::TestAppContext) -> VimTestContext {
|
||||
Self::init(cx);
|
||||
Self::new_with_lsp(
|
||||
EditorLspTestContext::new_typescript(
|
||||
lsp::ServerCapabilities {
|
||||
completion_provider: Some(lsp::CompletionOptions {
|
||||
trigger_characters: Some(vec![".".to_string()]),
|
||||
..Default::default()
|
||||
}),
|
||||
rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
|
||||
prepare_provider: Some(true),
|
||||
work_done_progress_options: Default::default(),
|
||||
})),
|
||||
definition_provider: Some(lsp::OneOf::Left(true)),
|
||||
..Default::default()
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.await,
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn new_tsx(cx: &mut gpui::TestAppContext) -> VimTestContext {
|
||||
Self::init(cx);
|
||||
Self::new_with_lsp(
|
||||
EditorLspTestContext::new_tsx(
|
||||
lsp::ServerCapabilities {
|
||||
completion_provider: Some(lsp::CompletionOptions {
|
||||
trigger_characters: Some(vec![".".to_string()]),
|
||||
..Default::default()
|
||||
}),
|
||||
rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
|
||||
prepare_provider: Some(true),
|
||||
work_done_progress_options: Default::default(),
|
||||
})),
|
||||
..Default::default()
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.await,
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn init_keybindings(enabled: bool, cx: &mut App) {
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store.update_user_settings(cx, |s| s.vim_mode = Some(enabled));
|
||||
});
|
||||
let mut default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
|
||||
"keymaps/default-macos.json",
|
||||
cx,
|
||||
)
|
||||
.unwrap();
|
||||
for key_binding in &mut default_key_bindings {
|
||||
key_binding.set_meta(settings::KeybindSource::Default.meta());
|
||||
}
|
||||
cx.bind_keys(default_key_bindings);
|
||||
if enabled {
|
||||
let mut vim_key_bindings =
|
||||
settings::KeymapFile::load_asset_allow_partial_failure("keymaps/vim.json", cx)
|
||||
.unwrap();
|
||||
for key_binding in &mut vim_key_bindings {
|
||||
key_binding.set_meta(settings::KeybindSource::Vim.meta());
|
||||
}
|
||||
cx.bind_keys(vim_key_bindings);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_with_lsp(mut cx: EditorLspTestContext, enabled: bool) -> VimTestContext {
|
||||
cx.update(|_, cx| {
|
||||
Self::init_keybindings(enabled, cx);
|
||||
});
|
||||
|
||||
// Setup search toolbars and keypress hook
|
||||
cx.update_workspace(|workspace, window, cx| {
|
||||
workspace.active_pane().update(cx, |pane, cx| {
|
||||
pane.toolbar().update(cx, |toolbar, cx| {
|
||||
let buffer_search_bar = cx.new(|cx| BufferSearchBar::new(None, window, cx));
|
||||
toolbar.add_item(buffer_search_bar, window, cx);
|
||||
|
||||
let project_search_bar = cx.new(|_| ProjectSearchBar::new());
|
||||
toolbar.add_item(project_search_bar, window, cx);
|
||||
})
|
||||
});
|
||||
workspace.status_bar().update(cx, |status_bar, cx| {
|
||||
let vim_mode_indicator = cx.new(|cx| ModeIndicator::new(window, cx));
|
||||
status_bar.add_right_item(vim_mode_indicator, window, cx);
|
||||
});
|
||||
});
|
||||
|
||||
Self { cx }
|
||||
}
|
||||
|
||||
pub fn update_entity<F, T, R>(&mut self, entity: Entity<T>, update: F) -> R
|
||||
where
|
||||
T: 'static,
|
||||
F: FnOnce(&mut T, &mut Window, &mut Context<T>) -> R + 'static,
|
||||
{
|
||||
let window = self.window;
|
||||
self.update_window(window, move |_, window, cx| {
|
||||
entity.update(cx, |t, cx| update(t, window, cx))
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub fn workspace<F, T>(&mut self, update: F) -> T
|
||||
where
|
||||
F: FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) -> T,
|
||||
{
|
||||
self.cx.update_workspace(update)
|
||||
}
|
||||
|
||||
pub fn enable_vim(&mut self) {
|
||||
self.cx.update(|_, cx| {
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store.update_user_settings(cx, |s| s.vim_mode = Some(true));
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
pub fn disable_vim(&mut self) {
|
||||
self.cx.update(|_, cx| {
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store.update_user_settings(cx, |s| s.vim_mode = Some(false));
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
pub fn enable_helix(&mut self) {
|
||||
self.cx.update(|_, cx| {
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store.update_user_settings(cx, |s| s.helix_mode = Some(true));
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
pub fn mode(&mut self) -> Mode {
|
||||
self.update_editor(|editor, _, cx| editor.addon::<VimAddon>().unwrap().entity.read(cx).mode)
|
||||
}
|
||||
|
||||
pub fn forced_motion(&mut self) -> bool {
|
||||
self.update_editor(|_, _, cx| cx.global::<VimGlobals>().forced_motion)
|
||||
}
|
||||
|
||||
pub fn active_operator(&mut self) -> Option<Operator> {
|
||||
self.update_editor(|editor, _, cx| {
|
||||
editor
|
||||
.addon::<VimAddon>()
|
||||
.unwrap()
|
||||
.entity
|
||||
.read(cx)
|
||||
.operator_stack
|
||||
.last()
|
||||
.cloned()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn set_state(&mut self, text: &str, mode: Mode) {
|
||||
self.cx.set_state(text);
|
||||
let vim =
|
||||
self.update_editor(|editor, _window, _cx| editor.addon::<VimAddon>().cloned().unwrap());
|
||||
|
||||
self.update(|window, cx| {
|
||||
vim.entity.update(cx, |vim, cx| {
|
||||
vim.switch_mode(mode, true, window, cx);
|
||||
});
|
||||
});
|
||||
self.cx.cx.cx.run_until_parked();
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn assert_state(&mut self, text: &str, mode: Mode) {
|
||||
self.assert_editor_state(text);
|
||||
assert_eq!(self.mode(), mode, "{}", self.assertion_context());
|
||||
}
|
||||
|
||||
pub fn assert_binding(
|
||||
&mut self,
|
||||
keystrokes: &str,
|
||||
initial_state: &str,
|
||||
initial_mode: Mode,
|
||||
state_after: &str,
|
||||
mode_after: Mode,
|
||||
) {
|
||||
self.set_state(initial_state, initial_mode);
|
||||
self.cx.simulate_keystrokes(keystrokes);
|
||||
self.cx.assert_editor_state(state_after);
|
||||
assert_eq!(self.mode(), mode_after, "{}", self.assertion_context());
|
||||
assert_eq!(self.active_operator(), None, "{}", self.assertion_context());
|
||||
}
|
||||
|
||||
pub fn assert_binding_normal(
|
||||
&mut self,
|
||||
keystrokes: &str,
|
||||
initial_state: &str,
|
||||
state_after: &str,
|
||||
) {
|
||||
self.set_state(initial_state, Mode::Normal);
|
||||
self.cx.simulate_keystrokes(keystrokes);
|
||||
self.cx.assert_editor_state(state_after);
|
||||
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())
|
||||
.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 {
|
||||
type Target = EditorLspTestContext;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.cx
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for VimTestContext {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.cx
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,3 @@
|
||||
{"Get":{"state":"ˇ","mode":"Normal"}}
|
||||
{"Put":{"state":"This is a tesˇt"}}
|
||||
{"Get":{"state":"This is a tesˇt","mode":"Normal"}}
|
||||
@@ -0,0 +1,6 @@
|
||||
{"Put":{"state":"The qˇuick"}}
|
||||
{"Key":"a"}
|
||||
{"Get":{"state":"The quˇick","mode":"Insert"}}
|
||||
{"Put":{"state":"The quicˇk"}}
|
||||
{"Key":"a"}
|
||||
{"Get":{"state":"The quickˇ","mode":"Insert"}}
|
||||
@@ -0,0 +1,23 @@
|
||||
{"Put":{"state":" ˇconst f = (x: unknown) => {"}}
|
||||
{"Key":"v"}
|
||||
{"Key":"a"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":" «const ˇ»f = (x: unknown) => {","mode":"Visual"}}
|
||||
{"Put":{"state":" ˇconst f = (x: unknown) => {"}}
|
||||
{"Key":"y"}
|
||||
{"Key":"a"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":" ˇconst f = (x: unknown) => {","mode":"Normal"}}
|
||||
{"ReadRegister":{"name":"\"","value":"const "}}
|
||||
{"Put":{"state":" ˇconst f = (x: unknown) => {"}}
|
||||
{"Key":"d"}
|
||||
{"Key":"a"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":" ˇf = (x: unknown) => {","mode":"Normal"}}
|
||||
{"ReadRegister":{"name":"\"","value":"const "}}
|
||||
{"Put":{"state":" ˇconst f = (x: unknown) => {"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":" ˇf = (x: unknown) => {","mode":"Insert"}}
|
||||
{"ReadRegister":{"name":"\"","value":"const "}}
|
||||
@@ -0,0 +1,54 @@
|
||||
{"Put":{"state":"ˇThe quick-brown\n\n\nfox_jumps over\nthe"}}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"ˇThe quick-brown\n\n\nfox_jumps over\nthe","mode":"Normal"}}
|
||||
{"Put":{"state":"The ˇquick-brown\n\n\nfox_jumps over\nthe"}}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"ˇThe quick-brown\n\n\nfox_jumps over\nthe","mode":"Normal"}}
|
||||
{"Put":{"state":"The quickˇ-brown\n\n\nfox_jumps over\nthe"}}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"The ˇquick-brown\n\n\nfox_jumps over\nthe","mode":"Normal"}}
|
||||
{"Put":{"state":"The quick-ˇbrown\n\n\nfox_jumps over\nthe"}}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"The quickˇ-brown\n\n\nfox_jumps over\nthe","mode":"Normal"}}
|
||||
{"Put":{"state":"The quick-brown\nˇ\n\nfox_jumps over\nthe"}}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"The quick-ˇbrown\n\n\nfox_jumps over\nthe","mode":"Normal"}}
|
||||
{"Put":{"state":"The quick-brown\n\nˇ\nfox_jumps over\nthe"}}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"The quick-brown\nˇ\n\nfox_jumps over\nthe","mode":"Normal"}}
|
||||
{"Put":{"state":"The quick-brown\n\n\nˇfox_jumps over\nthe"}}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"The quick-brown\n\nˇ\nfox_jumps over\nthe","mode":"Normal"}}
|
||||
{"Put":{"state":"The quick-brown\n\n\nfox_jumps ˇover\nthe"}}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"The quick-brown\n\n\nˇfox_jumps over\nthe","mode":"Normal"}}
|
||||
{"Put":{"state":"The quick-brown\n\n\nfox_jumps over\nˇthe"}}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"The quick-brown\n\n\nfox_jumps ˇover\nthe","mode":"Normal"}}
|
||||
{"Put":{"state":"ˇThe quick-brown\n\n\nfox_jumps over\nthe"}}
|
||||
{"Key":"shift-b"}
|
||||
{"Get":{"state":"ˇThe quick-brown\n\n\nfox_jumps over\nthe","mode":"Normal"}}
|
||||
{"Put":{"state":"The ˇquick-brown\n\n\nfox_jumps over\nthe"}}
|
||||
{"Key":"shift-b"}
|
||||
{"Get":{"state":"ˇThe quick-brown\n\n\nfox_jumps over\nthe","mode":"Normal"}}
|
||||
{"Put":{"state":"The quickˇ-brown\n\n\nfox_jumps over\nthe"}}
|
||||
{"Key":"shift-b"}
|
||||
{"Get":{"state":"The ˇquick-brown\n\n\nfox_jumps over\nthe","mode":"Normal"}}
|
||||
{"Put":{"state":"The quick-ˇbrown\n\n\nfox_jumps over\nthe"}}
|
||||
{"Key":"shift-b"}
|
||||
{"Get":{"state":"The ˇquick-brown\n\n\nfox_jumps over\nthe","mode":"Normal"}}
|
||||
{"Put":{"state":"The quick-brown\nˇ\n\nfox_jumps over\nthe"}}
|
||||
{"Key":"shift-b"}
|
||||
{"Get":{"state":"The ˇquick-brown\n\n\nfox_jumps over\nthe","mode":"Normal"}}
|
||||
{"Put":{"state":"The quick-brown\n\nˇ\nfox_jumps over\nthe"}}
|
||||
{"Key":"shift-b"}
|
||||
{"Get":{"state":"The quick-brown\nˇ\n\nfox_jumps over\nthe","mode":"Normal"}}
|
||||
{"Put":{"state":"The quick-brown\n\n\nˇfox_jumps over\nthe"}}
|
||||
{"Key":"shift-b"}
|
||||
{"Get":{"state":"The quick-brown\n\nˇ\nfox_jumps over\nthe","mode":"Normal"}}
|
||||
{"Put":{"state":"The quick-brown\n\n\nfox_jumps ˇover\nthe"}}
|
||||
{"Key":"shift-b"}
|
||||
{"Get":{"state":"The quick-brown\n\n\nˇfox_jumps over\nthe","mode":"Normal"}}
|
||||
{"Put":{"state":"The quick-brown\n\n\nfox_jumps over\nˇthe"}}
|
||||
{"Key":"shift-b"}
|
||||
{"Get":{"state":"The quick-brown\n\n\nfox_jumps ˇover\nthe","mode":"Normal"}}
|
||||
@@ -0,0 +1,9 @@
|
||||
{"Put":{"state":"ˇThe quick\nbrown"}}
|
||||
{"Key":"backspace"}
|
||||
{"Get":{"state":"ˇThe quick\nbrown","mode":"Normal"}}
|
||||
{"Put":{"state":"The qˇuick\nbrown"}}
|
||||
{"Key":"backspace"}
|
||||
{"Get":{"state":"The ˇquick\nbrown","mode":"Normal"}}
|
||||
{"Put":{"state":"The quick\nˇbrown"}}
|
||||
{"Key":"backspace"}
|
||||
{"Get":{"state":"The quicˇk\nbrown","mode":"Normal"}}
|
||||
@@ -0,0 +1,4 @@
|
||||
{"Put":{"state":"ππππ\nπanˇotherline"}}
|
||||
{"Key":"4"}
|
||||
{"Key":"backspace"}
|
||||
{"Get":{"state":"πππˇπ\nπanotherline","mode":"Normal"}}
|
||||
@@ -0,0 +1,8 @@
|
||||
{"Put":{"state":"ˇa b a b a b a"}}
|
||||
{"Key":"*"}
|
||||
{"Key":"n"}
|
||||
{"Get":{"state":"a b a b ˇa b a","mode":"Normal"}}
|
||||
{"Key":"#"}
|
||||
{"Get":{"state":"a b ˇa b a b a","mode":"Normal"}}
|
||||
{"Key":"n"}
|
||||
{"Get":{"state":"ˇa b a b a b a","mode":"Normal"}}
|
||||
@@ -0,0 +1,11 @@
|
||||
{"Put":{"state":"ˇhello world"}}
|
||||
{"Key":"d"}
|
||||
{"Key":"i"}
|
||||
{"Key":"w"}
|
||||
{"Key":"\""}
|
||||
{"Key":"_"}
|
||||
{"Key":"d"}
|
||||
{"Key":"a"}
|
||||
{"Key":"w"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"hellˇo","mode":"Normal"}}
|
||||
@@ -0,0 +1,36 @@
|
||||
{"Put":{"state":"Line one\nLine two\nLine ˇthree\nLine four\nLine five\n"}}
|
||||
{"Key":"v"}
|
||||
{"Key":"j"}
|
||||
{"Key":"escape"}
|
||||
{"Key":"k"}
|
||||
{"Key":"k"}
|
||||
{"Key":"'"}
|
||||
{"Key":"<"}
|
||||
{"Get":{"state":"Line one\nLine two\nˇLine three\nLine four\nLine five\n","mode":"Normal"}}
|
||||
{"Key":"`"}
|
||||
{"Key":"<"}
|
||||
{"Get":{"state":"Line one\nLine two\nLine ˇthree\nLine four\nLine five\n","mode":"Normal"}}
|
||||
{"Key":"'"}
|
||||
{"Key":">"}
|
||||
{"Get":{"state":"Line one\nLine two\nLine three\nˇLine four\nLine five\n","mode":"Normal"}}
|
||||
{"Key":"`"}
|
||||
{"Key":">"}
|
||||
{"Get":{"state":"Line one\nLine two\nLine three\nLine ˇfour\nLine five\n","mode":"Normal"}}
|
||||
{"Key":"g"}
|
||||
{"Key":"g"}
|
||||
{"Key":"^"}
|
||||
{"Key":"j"}
|
||||
{"Key":"j"}
|
||||
{"Key":"l"}
|
||||
{"Key":"l"}
|
||||
{"Key":"c"}
|
||||
{"Key":"e"}
|
||||
{"Key":"k"}
|
||||
{"Key":"e"}
|
||||
{"Key":"escape"}
|
||||
{"Key":"'"}
|
||||
{"Key":"."}
|
||||
{"Get":{"state":"Line one\nLine two\nˇLike three\nLine four\nLine five\n","mode":"Normal"}}
|
||||
{"Key":"`"}
|
||||
{"Key":"."}
|
||||
{"Get":{"state":"Line one\nLine two\nLiˇke three\nLine four\nLine five\n","mode":"Normal"}}
|
||||
@@ -0,0 +1,570 @@
|
||||
{"Put":{"state":"ˇaaab b bb aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"1"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"ˇaaab b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaaˇb b bb aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"1"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaaˇb b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaabˇ b bb aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"1"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaaˇb b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab ˇb bb aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"1"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaaˇb b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab bˇ bb aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"1"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab ˇb bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b ˇbb aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"1"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab ˇb bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bˇb aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"1"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b ˇbb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bbˇ aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"1"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bˇb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aˇaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"1"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bˇb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaaˇbaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"1"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bˇb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\nˇ baaa bbb\n \nb\n"}}
|
||||
{"Key":"1"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\nˇ baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n ˇbaaa bbb\n \nb\n"}}
|
||||
{"Key":"1"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n ˇbaaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n bˇaaa bbb\n \nb\n"}}
|
||||
{"Key":"1"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n ˇbaaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n baaˇa bbb\n \nb\n"}}
|
||||
{"Key":"1"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n ˇbaaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n baaa ˇbbb\n \nb\n"}}
|
||||
{"Key":"1"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n ˇbaaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n baaa bˇbb\n \nb\n"}}
|
||||
{"Key":"1"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n baaa ˇbbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n baaa bbˇb\n \nb\n"}}
|
||||
{"Key":"1"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n baaa bˇbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n baaa bbb\nˇ \nb\n"}}
|
||||
{"Key":"1"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n baaa bbb\nˇ \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n baaa bbb\n \nˇb\n"}}
|
||||
{"Key":"1"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n baaa bbb\n \nˇb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"ˇaaab b bb aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"1"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"ˇaaab b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaaˇb b bb aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"1"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaaˇb b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaabˇ b bb aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"1"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaabˇ b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab ˇb bb aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"1"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaabˇ b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab bˇ bb aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"1"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab bˇ bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b ˇbb aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"1"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab bˇ bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bˇb aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"1"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bˇb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bbˇ aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"1"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bbˇ aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aˇaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"1"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bbˇ aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaaˇbaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"1"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bbˇ aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\nˇ baaa bbb\n \nb\n"}}
|
||||
{"Key":"1"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\nˇ baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n ˇbaaa bbb\n \nb\n"}}
|
||||
{"Key":"1"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n ˇbaaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n bˇaaa bbb\n \nb\n"}}
|
||||
{"Key":"1"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n bˇaaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n baaˇa bbb\n \nb\n"}}
|
||||
{"Key":"1"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n bˇaaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n baaa ˇbbb\n \nb\n"}}
|
||||
{"Key":"1"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n bˇaaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n baaa bˇbb\n \nb\n"}}
|
||||
{"Key":"1"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n baaa bˇbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n baaa bbˇb\n \nb\n"}}
|
||||
{"Key":"1"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n baaa bbˇb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n baaa bbb\nˇ \nb\n"}}
|
||||
{"Key":"1"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n baaa bbb\nˇ \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n baaa bbb\n \nˇb\n"}}
|
||||
{"Key":"1"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n baaa bbb\n \nˇb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"ˇaaab b bb aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"ˇaaab b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaaˇb b bb aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaaˇb b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaabˇ b bb aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaabˇ b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab ˇb bb aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab ˇb bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab bˇ bb aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaaˇb b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b ˇbb aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaaˇb b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bˇb aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab ˇb bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bbˇ aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b ˇbb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aˇaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b ˇbb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaaˇbaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b ˇbb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\nˇ baaa bbb\n \nb\n"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\nˇ baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n ˇbaaa bbb\n \nb\n"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n ˇbaaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n bˇaaa bbb\n \nb\n"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n bˇaaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n baaˇa bbb\n \nb\n"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n baaˇa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n baaa ˇbbb\n \nb\n"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n baaa ˇbbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n baaa bˇbb\n \nb\n"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n ˇbaaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n baaa bbˇb\n \nb\n"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n baaa ˇbbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n baaa bbb\nˇ \nb\n"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n baaa bbb\nˇ \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n baaa bbb\n \nˇb\n"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n baaa bbb\n \nˇb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"ˇaaab b bb aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"ˇaaab b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaaˇb b bb aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaaˇb b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaabˇ b bb aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaabˇ b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab ˇb bb aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab ˇb bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab bˇ bb aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaabˇ b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b ˇbb aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaabˇ b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bˇb aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab bˇ bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bbˇ aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bˇb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aˇaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bˇb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaaˇbaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bˇb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\nˇ baaa bbb\n \nb\n"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\nˇ baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n ˇbaaa bbb\n \nb\n"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n ˇbaaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n bˇaaa bbb\n \nb\n"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n bˇaaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n baaˇa bbb\n \nb\n"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n baaˇa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n baaa ˇbbb\n \nb\n"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n baaa ˇbbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n baaa bˇbb\n \nb\n"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n bˇaaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n baaa bbˇb\n \nb\n"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n baaa bˇbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n baaa bbb\nˇ \nb\n"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n baaa bbb\nˇ \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n baaa bbb\n \nˇb\n"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n baaa bbb\n \nˇb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"ˇaaab b bb aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"3"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"ˇaaab b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaaˇb b bb aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"3"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaaˇb b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaabˇ b bb aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"3"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaabˇ b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab ˇb bb aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"3"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab ˇb bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab bˇ bb aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"3"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab bˇ bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b ˇbb aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"3"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b ˇbb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bˇb aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"3"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaaˇb b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bbˇ aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"3"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab ˇb bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aˇaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"3"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab ˇb bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaaˇbaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"3"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab ˇb bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\nˇ baaa bbb\n \nb\n"}}
|
||||
{"Key":"3"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\nˇ baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n ˇbaaa bbb\n \nb\n"}}
|
||||
{"Key":"3"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n ˇbaaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n bˇaaa bbb\n \nb\n"}}
|
||||
{"Key":"3"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n bˇaaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n baaˇa bbb\n \nb\n"}}
|
||||
{"Key":"3"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n baaˇa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n baaa ˇbbb\n \nb\n"}}
|
||||
{"Key":"3"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n baaa ˇbbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n baaa bˇbb\n \nb\n"}}
|
||||
{"Key":"3"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n baaa bˇbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n baaa bbˇb\n \nb\n"}}
|
||||
{"Key":"3"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n ˇbaaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n baaa bbb\nˇ \nb\n"}}
|
||||
{"Key":"3"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n baaa bbb\nˇ \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n baaa bbb\n \nˇb\n"}}
|
||||
{"Key":"3"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n baaa bbb\n \nˇb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"ˇaaab b bb aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"3"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"ˇaaab b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaaˇb b bb aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"3"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaaˇb b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaabˇ b bb aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"3"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaabˇ b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab ˇb bb aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"3"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab ˇb bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab bˇ bb aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"3"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab bˇ bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b ˇbb aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"3"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b ˇbb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bˇb aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"3"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaabˇ b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bbˇ aaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"3"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab bˇ bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aˇaabaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"3"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab bˇ bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaaˇbaaa\n baaa bbb\n \nb\n"}}
|
||||
{"Key":"3"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab bˇ bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\nˇ baaa bbb\n \nb\n"}}
|
||||
{"Key":"3"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\nˇ baaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n ˇbaaa bbb\n \nb\n"}}
|
||||
{"Key":"3"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n ˇbaaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n bˇaaa bbb\n \nb\n"}}
|
||||
{"Key":"3"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n bˇaaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n baaˇa bbb\n \nb\n"}}
|
||||
{"Key":"3"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n baaˇa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n baaa ˇbbb\n \nb\n"}}
|
||||
{"Key":"3"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n baaa ˇbbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n baaa bˇbb\n \nb\n"}}
|
||||
{"Key":"3"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n baaa bˇbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n baaa bbˇb\n \nb\n"}}
|
||||
{"Key":"3"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n bˇaaa bbb\n \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n baaa bbb\nˇ \nb\n"}}
|
||||
{"Key":"3"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n baaa bbb\nˇ \nb\n","mode":"Normal"}}
|
||||
{"Put":{"state":"aaab b bb aaabaaa\n baaa bbb\n \nˇb\n"}}
|
||||
{"Key":"3"}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"aaab b bb aaabaaa\n baaa bbb\n \nˇb\n","mode":"Normal"}}
|
||||
@@ -0,0 +1,35 @@
|
||||
{"Put":{"state":"Line one\nLine two\nLine three\nˇLine four\nLine five\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"w"}
|
||||
{"Key":"shift-s"}
|
||||
{"Key":"t"}
|
||||
{"Key":"r"}
|
||||
{"Key":"a"}
|
||||
{"Key":"i"}
|
||||
{"Key":"g"}
|
||||
{"Key":"h"}
|
||||
{"Key":"t"}
|
||||
{"Key":"space"}
|
||||
{"Key":"t"}
|
||||
{"Key":"h"}
|
||||
{"Key":"i"}
|
||||
{"Key":"n"}
|
||||
{"Key":"g"}
|
||||
{"Key":"escape"}
|
||||
{"Key":"j"}
|
||||
{"Key":"j"}
|
||||
{"Key":"'"}
|
||||
{"Key":"^"}
|
||||
{"Get":{"state":"Line one\nLine two\nLine three\nˇStraight thing four\nLine five\n","mode":"Normal"}}
|
||||
{"Key":"`"}
|
||||
{"Key":"^"}
|
||||
{"Get":{"state":"Line one\nLine two\nLine three\nStraight thingˇ four\nLine five\n","mode":"Normal"}}
|
||||
{"Key":"k"}
|
||||
{"Key":"a"}
|
||||
{"Key":"!"}
|
||||
{"Key":"escape"}
|
||||
{"Key":"k"}
|
||||
{"Key":"g"}
|
||||
{"Key":"i"}
|
||||
{"Key":"?"}
|
||||
{"Get":{"state":"Line one\nLine two\nLine three!?ˇ\nStraight thing four\nLine five\n","mode":"Insert"}}
|
||||
@@ -0,0 +1,24 @@
|
||||
{"Put":{"state":"ˇ"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"c"}
|
||||
{"Get":{"state":"ˇ","mode":"Insert"}}
|
||||
{"Put":{"state":"The ˇquick"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"c"}
|
||||
{"Get":{"state":"ˇ","mode":"Insert"}}
|
||||
{"Put":{"state":"The quˇick\nbrown fox\njumps over"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"c"}
|
||||
{"Get":{"state":"ˇ\nbrown fox\njumps over","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick\nbrown ˇfox\njumps over"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"c"}
|
||||
{"Get":{"state":"The quick\nˇ\njumps over","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick\nbrown fox\njumps ˇover"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"c"}
|
||||
{"Get":{"state":"The quick\nbrown fox\nˇ","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick\nˇ\nbrown fox"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"c"}
|
||||
{"Get":{"state":"The quick\nˇ\nbrown fox","mode":"Insert"}}
|
||||
@@ -0,0 +1,28 @@
|
||||
{"Put":{"state":"aaˇ aa aa aa aa"}}
|
||||
{"Key":"/"}
|
||||
{"Key":"b"}
|
||||
{"Key":"b"}
|
||||
{"Key":"enter"}
|
||||
{"Get":{"state":"aaˇ aa aa aa aa","mode":"Normal"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"g"}
|
||||
{"Key":"n"}
|
||||
{"Key":"x"}
|
||||
{"Key":"escape"}
|
||||
{"Get":{"state":"aaˇaa aa aa aa","mode":"Normal"}}
|
||||
{"Key":"."}
|
||||
{"Get":{"state":"aaˇa aa aa aa","mode":"Normal"}}
|
||||
{"Put":{"state":"aaˇ bb aa aa aa"}}
|
||||
{"Key":"/"}
|
||||
{"Key":"b"}
|
||||
{"Key":"b"}
|
||||
{"Key":"enter"}
|
||||
{"Get":{"state":"aa ˇbb aa aa aa","mode":"Normal"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"g"}
|
||||
{"Key":"n"}
|
||||
{"Key":"x"}
|
||||
{"Key":"escape"}
|
||||
{"Get":{"state":"aa ˇx aa aa aa","mode":"Normal"}}
|
||||
{"Key":"."}
|
||||
{"Get":{"state":"aa ˇx aa aa aa","mode":"Normal"}}
|
||||
@@ -0,0 +1,14 @@
|
||||
{"Put":{"state":"aaˇ aa aa aa aa"}}
|
||||
{"Key":"/"}
|
||||
{"Key":"a"}
|
||||
{"Key":"a"}
|
||||
{"Key":"enter"}
|
||||
{"Get":{"state":"aa ˇaa aa aa aa","mode":"Normal"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"g"}
|
||||
{"Key":"n"}
|
||||
{"Key":"x"}
|
||||
{"Key":"escape"}
|
||||
{"Get":{"state":"aa ˇx aa aa aa","mode":"Normal"}}
|
||||
{"Key":"."}
|
||||
{"Get":{"state":"aa x ˇx aa aa","mode":"Normal"}}
|
||||
@@ -0,0 +1,8 @@
|
||||
{"Put":{"state":"The qˇuick\nbrown fox"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"0"}
|
||||
{"Get":{"state":"ˇuick\nbrown fox","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick\nˇ\nbrown fox"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"0"}
|
||||
{"Get":{"state":"The quick\nˇ\nbrown fox","mode":"Insert"}}
|
||||
@@ -0,0 +1,24 @@
|
||||
{"Put":{"state":"Teˇst Test"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"ˇst Test","mode":"Insert"}}
|
||||
{"Put":{"state":"Test ˇtest"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"ˇtest","mode":"Insert"}}
|
||||
{"Put":{"state":"Test1 test2 ˇtest3"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"Test1 ˇtest3","mode":"Insert"}}
|
||||
{"Put":{"state":"Test test\nˇtest"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"Test ˇ\ntest","mode":"Insert"}}
|
||||
{"Put":{"state":"Test test\nˇ\ntest"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"b"}
|
||||
{"Get":{"state":"Test ˇ\n\ntest","mode":"Insert"}}
|
||||
{"Put":{"state":"Test test-test ˇtest"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"shift-b"}
|
||||
{"Get":{"state":"Test ˇtest","mode":"Insert"}}
|
||||
@@ -0,0 +1,16 @@
|
||||
{"Put":{"state":"Teˇst"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"backspace"}
|
||||
{"Get":{"state":"Tˇst","mode":"Insert"}}
|
||||
{"Put":{"state":"Tˇest"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"backspace"}
|
||||
{"Get":{"state":"ˇest","mode":"Insert"}}
|
||||
{"Put":{"state":"ˇTest"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"backspace"}
|
||||
{"Get":{"state":"ˇTest","mode":"Insert"}}
|
||||
{"Put":{"state":"Test\nˇtest"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"backspace"}
|
||||
{"Get":{"state":"Testˇtest","mode":"Insert"}}
|
||||
@@ -0,0 +1,23 @@
|
||||
{"Put":{"state":"ˇabC\n"}}
|
||||
{"Key":"~"}
|
||||
{"Get":{"state":"AˇbC\n","mode":"Normal"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"~"}
|
||||
{"Get":{"state":"ABˇc\n","mode":"Normal"}}
|
||||
{"Put":{"state":"a😀C«dÉ1*fˇ»\n"}}
|
||||
{"Key":"~"}
|
||||
{"Get":{"state":"a😀CˇDé1*F\n","mode":"Normal"}}
|
||||
{"Key":"~"}
|
||||
{"Put":{"state":"aˇC😀é1*F\n"}}
|
||||
{"Key":"4"}
|
||||
{"Key":"~"}
|
||||
{"Get":{"state":"ac😀É1ˇ*F\n","mode":"Normal"}}
|
||||
{"Put":{"state":"abˇC\n"}}
|
||||
{"Key":"shift-v"}
|
||||
{"Key":"~"}
|
||||
{"Get":{"state":"ˇABc\n","mode":"Normal"}}
|
||||
{"Put":{"state":"ˇaa\nbb\ncc"}}
|
||||
{"Key":"ctrl-v"}
|
||||
{"Key":"j"}
|
||||
{"Key":"~"}
|
||||
{"Get":{"state":"ˇAa\nBb\ncc","mode":"Normal"}}
|
||||
@@ -0,0 +1,27 @@
|
||||
{"Put":{"state":"ˇabc def"}}
|
||||
{"Key":"g"}
|
||||
{"Key":"shift-u"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"ˇABC def","mode":"Normal"}}
|
||||
{"Key":"g"}
|
||||
{"Key":"u"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"ˇabc def","mode":"Normal"}}
|
||||
{"Key":"g"}
|
||||
{"Key":"~"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"ˇABC def","mode":"Normal"}}
|
||||
{"Key":"."}
|
||||
{"Get":{"state":"ˇabc def","mode":"Normal"}}
|
||||
{"Put":{"state":"abˇc def"}}
|
||||
{"Key":"g"}
|
||||
{"Key":"~"}
|
||||
{"Key":"i"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"ˇABC def","mode":"Normal"}}
|
||||
{"Key":"."}
|
||||
{"Get":{"state":"ˇabc def","mode":"Normal"}}
|
||||
{"Key":"g"}
|
||||
{"Key":"shift-u"}
|
||||
{"Key":"$"}
|
||||
{"Get":{"state":"ˇABC DEF","mode":"Normal"}}
|
||||
@@ -0,0 +1,6 @@
|
||||
{"Put":{"state":"abc dˇef\n"}}
|
||||
{"Key":"g"}
|
||||
{"Key":"shift-u"}
|
||||
{"Key":"i"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"abc ˇDEF\n","mode":"Normal"}}
|
||||
@@ -0,0 +1,12 @@
|
||||
{"Put":{"state":"The quick\n brownˇ fox\njumps over\nthe lazy"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"c"}
|
||||
{"Get":{"state":"The quick\n ˇ\njumps over\nthe lazy","mode":"Insert"}}
|
||||
{"Put":{"state":"ˇThe quick\nbrown fox\njumps over\nthe lazy"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"c"}
|
||||
{"Get":{"state":"ˇ\nbrown fox\njumps over\nthe lazy","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick\n broˇwn fox\njumps over\nthe lazy"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"c"}
|
||||
{"Get":{"state":"The quick\n ˇ\njumps over\nthe lazy","mode":"Insert"}}
|
||||
@@ -0,0 +1,24 @@
|
||||
{"Put":{"state":"Teˇst Test"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"e"}
|
||||
{"Get":{"state":"Teˇ Test","mode":"Insert"}}
|
||||
{"Put":{"state":"Tˇest test"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"e"}
|
||||
{"Get":{"state":"Tˇ test","mode":"Insert"}}
|
||||
{"Put":{"state":"Test teˇst\ntest"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"e"}
|
||||
{"Get":{"state":"Test teˇ\ntest","mode":"Insert"}}
|
||||
{"Put":{"state":"Test tesˇt\ntest"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"e"}
|
||||
{"Get":{"state":"Test tesˇ","mode":"Insert"}}
|
||||
{"Put":{"state":"Test test\nˇ\ntest"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"e"}
|
||||
{"Get":{"state":"Test test\nˇ","mode":"Insert"}}
|
||||
{"Put":{"state":"Test teˇst-test test"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"shift-e"}
|
||||
{"Get":{"state":"Test teˇ test","mode":"Insert"}}
|
||||
@@ -0,0 +1,16 @@
|
||||
{"Put":{"state":"The quick\nbrownˇ fox\njumps over\nthe lazy"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"shift-g"}
|
||||
{"Get":{"state":"The quick\nˇ","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick\nbrownˇ fox\njumps over\nthe lazy"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"shift-g"}
|
||||
{"Get":{"state":"The quick\nˇ","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick\nbrown fox\njumps over\nthe lˇazy"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"shift-g"}
|
||||
{"Get":{"state":"The quick\nbrown fox\njumps over\nˇ","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick\nbrown fox\njumps over\nˇ"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"shift-g"}
|
||||
{"Get":{"state":"The quick\nbrown fox\njumps over\nˇ","mode":"Insert"}}
|
||||
@@ -0,0 +1,8 @@
|
||||
{"Put":{"state":"The qˇuick\nbrown fox"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"$"}
|
||||
{"Get":{"state":"The qˇ\nbrown fox","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick\nˇ\nbrown fox"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"$"}
|
||||
{"Get":{"state":"The quick\nˇ\nbrown fox","mode":"Insert"}}
|
||||
@@ -0,0 +1,20 @@
|
||||
{"Put":{"state":"The quick\nbrownˇ fox\njumps over\nthe lazy"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"g"}
|
||||
{"Key":"g"}
|
||||
{"Get":{"state":"ˇ\njumps over\nthe lazy","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick\nbrown fox\njumps over\nthe lˇazy"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"g"}
|
||||
{"Key":"g"}
|
||||
{"Get":{"state":"ˇ","mode":"Insert"}}
|
||||
{"Put":{"state":"The qˇuick\nbrown fox\njumps over\nthe lazy"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"g"}
|
||||
{"Key":"g"}
|
||||
{"Get":{"state":"ˇ\nbrown fox\njumps over\nthe lazy","mode":"Insert"}}
|
||||
{"Put":{"state":"ˇ\nbrown fox\njumps over\nthe lazy"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"g"}
|
||||
{"Key":"g"}
|
||||
{"Get":{"state":"ˇ\nbrown fox\njumps over\nthe lazy","mode":"Insert"}}
|
||||
@@ -0,0 +1,16 @@
|
||||
{"Put":{"state":"Teˇst"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"h"}
|
||||
{"Get":{"state":"Tˇst","mode":"Insert"}}
|
||||
{"Put":{"state":"Tˇest"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"h"}
|
||||
{"Get":{"state":"ˇest","mode":"Insert"}}
|
||||
{"Put":{"state":"ˇTest"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"h"}
|
||||
{"Get":{"state":"ˇTest","mode":"Insert"}}
|
||||
{"Put":{"state":"Test\nˇtest"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"h"}
|
||||
{"Get":{"state":"Test\nˇtest","mode":"Insert"}}
|
||||
@@ -0,0 +1,20 @@
|
||||
{"Put":{"state":"The quick\nbrown ˇfox\njumps over"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"j"}
|
||||
{"Get":{"state":"The quick\nˇ","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick\nbrown fox\njumps ˇover"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"j"}
|
||||
{"Get":{"state":"The quick\nbrown fox\njumps ˇover","mode":"Normal"}}
|
||||
{"Put":{"state":"The qˇuick\nbrown fox\njumps over"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"j"}
|
||||
{"Get":{"state":"ˇ\njumps over","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick\nbrown fox\nˇ"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"j"}
|
||||
{"Get":{"state":"The quick\nbrown fox\nˇ","mode":"Normal"}}
|
||||
{"Put":{"state":"The quick\n ˇbrown fox\n jumps over"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"j"}
|
||||
{"Get":{"state":"The quick\n ˇ","mode":"Insert"}}
|
||||
@@ -0,0 +1,20 @@
|
||||
{"Put":{"state":"The quick\nbrown ˇfox\njumps over"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"k"}
|
||||
{"Get":{"state":"ˇ\njumps over","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick\nbrown fox\njumps ˇover"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"k"}
|
||||
{"Get":{"state":"The quick\nˇ","mode":"Insert"}}
|
||||
{"Put":{"state":"The qˇuick\nbrown fox\njumps over"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"k"}
|
||||
{"Get":{"state":"The qˇuick\nbrown fox\njumps over","mode":"Normal"}}
|
||||
{"Put":{"state":"ˇ\nbrown fox\njumps over"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"k"}
|
||||
{"Get":{"state":"ˇ\nbrown fox\njumps over","mode":"Normal"}}
|
||||
{"Put":{"state":"The quick\n brown fox\n ˇjumps over"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"k"}
|
||||
{"Get":{"state":"The quick\n ˇ","mode":"Insert"}}
|
||||
@@ -0,0 +1,8 @@
|
||||
{"Put":{"state":"Teˇst"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"l"}
|
||||
{"Get":{"state":"Teˇt","mode":"Insert"}}
|
||||
{"Put":{"state":"Tesˇt"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"l"}
|
||||
{"Get":{"state":"Tesˇ","mode":"Insert"}}
|
||||
@@ -0,0 +1,16 @@
|
||||
{"Put":{"state":"one two\nthree fˇour"}}
|
||||
{"Key":"x"}
|
||||
{"Key":"k"}
|
||||
{"Key":"d"}
|
||||
{"Key":"i"}
|
||||
{"Key":"w"}
|
||||
{"Key":"^"}
|
||||
{"Key":"x"}
|
||||
{"Get":{"state":"ˇne \nthree fur","mode":"Normal"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"g"}
|
||||
{"Key":";"}
|
||||
{"Get":{"state":"ne \nthree fˇur","mode":"Normal"}}
|
||||
{"Key":"g"}
|
||||
{"Key":","}
|
||||
{"Get":{"state":"ˇne \nthree fur","mode":"Normal"}}
|
||||
@@ -0,0 +1,32 @@
|
||||
{"Put":{"state":"ˇ"}}
|
||||
{"Key":"i"}
|
||||
{"Key":"1"}
|
||||
{"Key":"1"}
|
||||
{"Key":"escape"}
|
||||
{"Key":"shift-o"}
|
||||
{"Key":"2"}
|
||||
{"Key":"2"}
|
||||
{"Key":"escape"}
|
||||
{"Key":"shift-g"}
|
||||
{"Key":"o"}
|
||||
{"Key":"3"}
|
||||
{"Key":"3"}
|
||||
{"Key":"escape"}
|
||||
{"Get":{"state":"22\n11\n3ˇ3","mode":"Normal"}}
|
||||
{"Key":"g"}
|
||||
{"Key":";"}
|
||||
{"Key":"g"}
|
||||
{"Key":";"}
|
||||
{"Key":"g"}
|
||||
{"Key":";"}
|
||||
{"Key":"g"}
|
||||
{"Key":","}
|
||||
{"Key":"shift-g"}
|
||||
{"Key":"i"}
|
||||
{"Key":"4"}
|
||||
{"Key":"4"}
|
||||
{"Key":"escape"}
|
||||
{"Key":"g"}
|
||||
{"Key":";"}
|
||||
{"Key":"g"}
|
||||
{"Key":";"}
|
||||
@@ -0,0 +1,8 @@
|
||||
{"Put":{"state":"first paragraph\nˇstill first\n\nsecond paragraph\nstill second\n\nthird paragraph\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"ˇ\nsecond paragraph\nstill second\n\nthird paragraph\n","mode":"Insert"}}
|
||||
{"ReadRegister":{"name":"\"","value":"first paragraph\nstill first\n\n"}}
|
||||
{"Key":"escape"}
|
||||
{"Get":{"state":"ˇ\nsecond paragraph\nstill second\n\nthird paragraph\n","mode":"Normal"}}
|
||||
@@ -0,0 +1,430 @@
|
||||
{"Put":{"state":"ˇThe quick brown fox jumps over the lazy dog."}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"ˇ","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown fox jumpˇs over the lazy dog."}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"ˇ","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown fox jumps over the lazy dogˇ."}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"ˇ","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown fox jumps over the lazy dog.ˇ"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"ˇ","mode":"Insert"}}
|
||||
{"Put":{"state":"ˇThe quick brown fox jumps over the lazy dog."}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"ˇ","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown fox jumpˇs over the lazy dog."}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"ˇ","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown fox jumps over the lazy dogˇ."}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"ˇ","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown fox jumps over the lazy dog.ˇ"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"ˇ","mode":"Insert"}}
|
||||
{"Put":{"state":"ˇThe quick brown\nfox jumps over\nthe lazy dog.\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"ˇ\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brownˇ\nfox jumps over\nthe lazy dog.\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"ˇ\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown\nˇfox jumps over\nthe lazy dog.\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"ˇ\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown\nfox jumps overˇ\nthe lazy dog.\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"ˇ\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown\nfox jumps over\nthe lazy dog.ˇ\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"ˇ\n","mode":"Insert"}}
|
||||
{"Put":{"state":"ˇThe quick brown\nfox jumps over\nthe lazy dog.\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"ˇ","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brownˇ\nfox jumps over\nthe lazy dog.\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"ˇ","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown\nˇfox jumps over\nthe lazy dog.\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"ˇ","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown\nfox jumps overˇ\nthe lazy dog.\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"ˇ","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown\nfox jumps over\nthe lazy dog.ˇ\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"ˇ","mode":"Insert"}}
|
||||
{"Put":{"state":"ˇ\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"ˇ\nThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n","mode":"Insert"}}
|
||||
{"Put":{"state":"\nˇ\nThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"ˇ\nThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n","mode":"Insert"}}
|
||||
{"Put":{"state":"\n\nˇThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"\n\nˇ\n\n\nThe quick brown fox jumps\nover the lazy dog.\n","mode":"Insert"}}
|
||||
{"Put":{"state":"\n\nThe quick brown fox jumps\nˇover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"\n\nˇ\n\n\nThe quick brown fox jumps\nover the lazy dog.\n","mode":"Insert"}}
|
||||
{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\nˇ\n\nThe quick brown fox jumps\nover the lazy dog.\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\nˇ\nThe quick brown fox jumps\nover the lazy dog.\n","mode":"Insert"}}
|
||||
{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\nˇ\nThe quick brown fox jumps\nover the lazy dog.\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\nˇ\nThe quick brown fox jumps\nover the lazy dog.\n","mode":"Insert"}}
|
||||
{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nˇThe quick brown fox jumps\nover the lazy dog.\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nˇ\n","mode":"Insert"}}
|
||||
{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumpsˇ\nover the lazy dog.\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nˇ\n","mode":"Insert"}}
|
||||
{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nˇover the lazy dog.\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nˇ\n","mode":"Insert"}}
|
||||
{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.ˇ\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nˇ\n","mode":"Insert"}}
|
||||
{"Put":{"state":"ˇ\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"ˇ\n\n\nThe quick brown fox jumps\nover the lazy dog.\n","mode":"Insert"}}
|
||||
{"Put":{"state":"\nˇ\nThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"ˇ\n\n\nThe quick brown fox jumps\nover the lazy dog.\n","mode":"Insert"}}
|
||||
{"Put":{"state":"\n\nˇThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"\n\nˇ\nThe quick brown fox jumps\nover the lazy dog.\n","mode":"Insert"}}
|
||||
{"Put":{"state":"\n\nThe quick brown fox jumps\nˇover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"\n\nˇ\nThe quick brown fox jumps\nover the lazy dog.\n","mode":"Insert"}}
|
||||
{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\nˇ\n\nThe quick brown fox jumps\nover the lazy dog.\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\nˇ\n","mode":"Insert"}}
|
||||
{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\nˇ\nThe quick brown fox jumps\nover the lazy dog.\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\nˇ\n","mode":"Insert"}}
|
||||
{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nˇThe quick brown fox jumps\nover the lazy dog.\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nˇ","mode":"Insert"}}
|
||||
{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumpsˇ\nover the lazy dog.\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nˇ","mode":"Insert"}}
|
||||
{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nˇover the lazy dog.\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nˇ","mode":"Insert"}}
|
||||
{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.ˇ\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nˇ","mode":"Insert"}}
|
||||
{"Put":{"state":"ˇThe quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"ˇ\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown fox jumps\nˇover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"ˇ\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\nˇ\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\nˇ\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\nˇ\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\nˇ\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\nˇ\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\nˇ\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nˇThe quick brown fox jumps\nover the lazy dog.\n\n\n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nˇ\n\n\n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumpsˇ\nover the lazy dog.\n\n\n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nˇ\n\n\n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nˇover the lazy dog.\n\n\n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nˇ\n\n\n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.ˇ\n\n\n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nˇ\n\n\n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\nˇ\n\n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\nˇ","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\nˇ\n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\nˇ","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nˇ\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\nˇ","mode":"Insert"}}
|
||||
{"Put":{"state":"ˇThe quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"ˇ\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown fox jumps\nˇover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"ˇ\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\nˇ\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\nˇ\n\n\n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\nˇ\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\nˇ\n\n\n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\nˇ\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\nˇ\n\n\n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nˇThe quick brown fox jumps\nover the lazy dog.\n\n\n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nˇ","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumpsˇ\nover the lazy dog.\n\n\n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nˇ","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nˇover the lazy dog.\n\n\n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nˇ","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.ˇ\n\n\n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nˇ","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\nˇ\n\n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\nˇ\n\n\n","mode":"Normal"}}
|
||||
{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\nˇ\n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\nˇ\n\n","mode":"Normal"}}
|
||||
{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nˇ\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nˇ\n","mode":"Normal"}}
|
||||
{"Put":{"state":"ˇThe quick brown fox jumps\nover the lazy dog.\n\n \t\n\nThe quick brown fox jumps\nover the lazy dog.\n\n \t\n\t \t\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"ˇ\n\n \t\n\nThe quick brown fox jumps\nover the lazy dog.\n\n \t\n\t \t\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\nˇ \t\n\nThe quick brown fox jumps\nover the lazy dog.\n\n \t\n\t \t\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\nˇ\nThe quick brown fox jumps\nover the lazy dog.\n\n \t\n\t \t\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nˇThe quick brown fox jumps\nover the lazy dog.\n\n \t\n\t \t\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nˇ\n\n \t\n\t \t\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nThe quick brown fox jumps\nover the lazy dog.ˇ\n\n \t\n\t \t\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nˇ\n\n \t\n\t \t\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nThe quick brown fox jumps\nover the lazy dog.\nˇ\n \t\n\t \t\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nThe quick brown fox jumps\nover the lazy dog.\nˇ","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nThe quick brown fox jumps\nover the lazy dog.\n\nˇ \t\n\t \t\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nThe quick brown fox jumps\nover the lazy dog.\nˇ","mode":"Insert"}}
|
||||
{"Put":{"state":"ˇThe quick brown fox jumps\nover the lazy dog.\n\n \t\n\nThe quick brown fox jumps\nover the lazy dog.\n\n \t\n\t \t\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"ˇ\nThe quick brown fox jumps\nover the lazy dog.\n\n \t\n\t \t\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\nˇ \t\n\nThe quick brown fox jumps\nover the lazy dog.\n\n \t\n\t \t\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\nˇ\n\n \t\n\t \t\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nˇThe quick brown fox jumps\nover the lazy dog.\n\n \t\n\t \t\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nˇ","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nThe quick brown fox jumps\nover the lazy dog.ˇ\n\n \t\n\t \t\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nˇ","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nThe quick brown fox jumps\nover the lazy dog.\nˇ\n \t\n\t \t\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nThe quick brown fox jumps\nover the lazy dog.\nˇ\n \t\n\t \t\n","mode":"Normal"}}
|
||||
{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nThe quick brown fox jumps\nover the lazy dog.\n\nˇ \t\n\t \t\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nThe quick brown fox jumps\nover the lazy dog.\n\nˇ \t\n\t \t\n","mode":"Normal"}}
|
||||
{"Put":{"state":"ˇThe quick brown fox jumps over the lazy dog.\n\nThe quick brown fox jumps over the lazy dog.\n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"ˇ\n\nThe quick brown fox jumps over the lazy dog.\n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown fox jumps over the lazy dog.\nˇ\nThe quick brown fox jumps over the lazy dog.\n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"The quick brown fox jumps over the lazy dog.\nˇ\nThe quick brown fox jumps over the lazy dog.\n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown fox jumps over the lazy dog.\n\nˇThe quick brown fox jumps over the lazy dog.\n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"The quick brown fox jumps over the lazy dog.\n\nˇ\n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown fox jumps over the lazy dog.\n\nThe quick brown fox jumpˇs over the lazy dog.\n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"The quick brown fox jumps over the lazy dog.\n\nˇ\n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown fox jumps over the lazy dog.\n\nThe quick brown fox jumps over the lazy dog.ˇ\n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"The quick brown fox jumps over the lazy dog.\n\nˇ\n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown fox jumps over the lazy dog.\n\nThe quick brown fox jumps over the lazy dog.\nˇ\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"The quick brown fox jumps over the lazy dog.\n\nThe quick brown fox jumps over the lazy dog.\nˇ","mode":"Insert"}}
|
||||
{"Put":{"state":"ˇThe quick brown fox jumps over the lazy dog.\n\nThe quick brown fox jumps over the lazy dog.\n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"ˇ\nThe quick brown fox jumps over the lazy dog.\n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown fox jumps over the lazy dog.\nˇ\nThe quick brown fox jumps over the lazy dog.\n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"The quick brown fox jumps over the lazy dog.\nˇ\n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown fox jumps over the lazy dog.\n\nˇThe quick brown fox jumps over the lazy dog.\n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"The quick brown fox jumps over the lazy dog.\n\nˇ","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown fox jumps over the lazy dog.\n\nThe quick brown fox jumpˇs over the lazy dog.\n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"The quick brown fox jumps over the lazy dog.\n\nˇ","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown fox jumps over the lazy dog.\n\nThe quick brown fox jumps over the lazy dog.ˇ\n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"The quick brown fox jumps over the lazy dog.\n\nˇ","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown fox jumps over the lazy dog.\n\nThe quick brown fox jumps over the lazy dog.\nˇ\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"The quick brown fox jumps over the lazy dog.\n\nThe quick brown fox jumps over the lazy dog.\nˇ\n","mode":"Normal"}}
|
||||
@@ -0,0 +1,72 @@
|
||||
{"SetOption":{"value":"wrap"}}
|
||||
{"SetOption":{"value":"columns=20"}}
|
||||
{"Put":{"state":"ˇ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.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"ˇ\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"Insert"}}
|
||||
{"Put":{"state":"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.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"ˇ\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"Insert"}}
|
||||
{"Put":{"state":"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.\n\nˇSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"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.\n\nˇ\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"Insert"}}
|
||||
{"Put":{"state":"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.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and ˇshould be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"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.\n\nˇ\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"Insert"}}
|
||||
{"Put":{"state":"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.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nˇThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"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.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nˇ\n","mode":"Insert"}}
|
||||
{"Put":{"state":"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.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping ˇsettings.\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"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.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nˇ\n","mode":"Insert"}}
|
||||
{"Put":{"state":"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.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.ˇ\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"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.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nˇ\n","mode":"Insert"}}
|
||||
{"Put":{"state":"ˇ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.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"ˇ\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"Insert"}}
|
||||
{"Put":{"state":"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.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"ˇ\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"Insert"}}
|
||||
{"Put":{"state":"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.\n\nˇSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"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.\n\nˇ\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"Insert"}}
|
||||
{"Put":{"state":"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.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and ˇshould be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"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.\n\nˇ\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"Insert"}}
|
||||
{"Put":{"state":"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.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nˇThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"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.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nˇ","mode":"Insert"}}
|
||||
{"Put":{"state":"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.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping ˇsettings.\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"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.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nˇ","mode":"Insert"}}
|
||||
{"Put":{"state":"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.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.ˇ\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":"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.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nˇ","mode":"Insert"}}
|
||||
@@ -0,0 +1,23 @@
|
||||
{"Put":{"state":"ˇabc def"}}
|
||||
{"Key":"g"}
|
||||
{"Key":"?"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"ˇnop def","mode":"Normal"}}
|
||||
{"Key":"g"}
|
||||
{"Key":"?"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"ˇabc def","mode":"Normal"}}
|
||||
{"Key":"."}
|
||||
{"Get":{"state":"ˇnop def","mode":"Normal"}}
|
||||
{"Put":{"state":"abˇc def"}}
|
||||
{"Key":"g"}
|
||||
{"Key":"?"}
|
||||
{"Key":"i"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"ˇnop def","mode":"Normal"}}
|
||||
{"Key":"."}
|
||||
{"Get":{"state":"ˇabc def","mode":"Normal"}}
|
||||
{"Key":"g"}
|
||||
{"Key":"?"}
|
||||
{"Key":"$"}
|
||||
{"Get":{"state":"ˇnop qrs","mode":"Normal"}}
|
||||
@@ -0,0 +1,6 @@
|
||||
{"Put":{"state":"ˇabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"}}
|
||||
{"Key":"g"}
|
||||
{"Key":"?"}
|
||||
{"Key":"i"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"ˇnopqrstuvwxyzabcdefghijklmNOPQRSTUVWXYZABCDEFGHIJKLM","mode":"Normal"}}
|
||||
@@ -0,0 +1,270 @@
|
||||
{"Put":{"state":"ˇThe quick brown? Fox Jumps! Over the lazy."}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"ˇ Fox Jumps! Over the lazy.","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick ˇbrown? Fox Jumps! Over the lazy."}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"ˇ Fox Jumps! Over the lazy.","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brownˇ? Fox Jumps! Over the lazy."}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"ˇ Fox Jumps! Over the lazy.","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown?ˇ Fox Jumps! Over the lazy."}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"The quick brown?ˇFox Jumps! Over the lazy.","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown? ˇFox Jumps! Over the lazy."}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"The quick brown? ˇ Over the lazy.","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown? Fox Jˇumps! Over the lazy."}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"The quick brown? ˇ Over the lazy.","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown? Fox Jumpsˇ! Over the lazy."}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"The quick brown? ˇ Over the lazy.","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown? Fox Jumps!ˇ Over the lazy."}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"The quick brown? Fox Jumps!ˇOver the lazy.","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown? Fox Jumps! Ovˇer the lazy."}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"The quick brown? Fox Jumps! ˇ","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown? Fox Jumps! Over theˇ lazy."}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"The quick brown? Fox Jumps! ˇ","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown? Fox Jumps! Over the lazyˇ."}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"The quick brown? Fox Jumps! ˇ","mode":"Insert"}}
|
||||
{"Put":{"state":"ˇThe quick brown\nfox jumps over\nthe lazy dog. The quick \nbrown fox jumps over\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"ˇ The quick \nbrown fox jumps over\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick ˇbrown\nfox jumps over\nthe lazy dog. The quick \nbrown fox jumps over\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"ˇ The quick \nbrown fox jumps over\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brownˇ\nfox jumps over\nthe lazy dog. The quick \nbrown fox jumps over\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"ˇ The quick \nbrown fox jumps over\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown\nfox jumps over\nthe lazy doˇg. The quick \nbrown fox jumps over\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"ˇ The quick \nbrown fox jumps over\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown\nfox jumps over\nthe lazy dogˇ. The quick \nbrown fox jumps over\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"ˇ The quick \nbrown fox jumps over\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown\nfox jumps over\nthe lazy dog.ˇ The quick \nbrown fox jumps over\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"The quick brown\nfox jumps over\nthe lazy dog.ˇThe quick \nbrown fox jumps over\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown\nfox jumps over\nthe lazy dog. ˇThe quick \nbrown fox jumps over\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"The quick brown\nfox jumps over\nthe lazy dog. ˇ\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown\nfox jumps over\nthe lazy dog. The quick ˇ\nbrown fox jumps over\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"The quick brown\nfox jumps over\nthe lazy dog. ˇ\n","mode":"Insert"}}
|
||||
{"Put":{"state":"ˇThe quick brown.)]'\" Brown fox jumps. "}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"ˇ Brown fox jumps. ","mode":"Insert"}}
|
||||
{"Put":{"state":"The ˇquick brown.)]'\" Brown fox jumps. "}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"ˇ Brown fox jumps. ","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brownˇ.)]'\" Brown fox jumps. "}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"ˇ Brown fox jumps. ","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown.)ˇ]'\" Brown fox jumps. "}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"ˇ Brown fox jumps. ","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown.)]ˇ'\" Brown fox jumps. "}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"ˇ Brown fox jumps. ","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown.)]'ˇ\" Brown fox jumps. "}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"ˇ Brown fox jumps. ","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown.)]'\" Brown ˇfox jumps. "}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"The quick brown.)]'\" ˇ ","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown.)]'\" Brown fox jumpsˇ. "}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"The quick brown.)]'\" ˇ ","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown.)]'\" Brown fox jumps.ˇ "}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"The quick brown.)]'\" Brown fox jumps.ˇ","mode":"Insert"}}
|
||||
{"Put":{"state":"ˇThe quick brown? Fox Jumps! Over the lazy."}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"ˇFox Jumps! Over the lazy.","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick ˇbrown? Fox Jumps! Over the lazy."}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"ˇFox Jumps! Over the lazy.","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brownˇ? Fox Jumps! Over the lazy."}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"ˇFox Jumps! Over the lazy.","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown? ˇFox Jumps! Over the lazy."}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"The quick brown? ˇOver the lazy.","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown? Fox Jˇumps! Over the lazy."}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"The quick brown? ˇOver the lazy.","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown? Fox Jumpsˇ! Over the lazy."}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"The quick brown? ˇOver the lazy.","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown? Fox Jumps!ˇ Over the lazy."}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"The quick brown? Fox Jumps!ˇ","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown? Fox Jumps! Ovˇer the lazy."}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"The quick brown? Fox Jumps!ˇ","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown? Fox Jumps! Over theˇ lazy."}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"The quick brown? Fox Jumps!ˇ","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown? Fox Jumps! Over the lazyˇ."}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"The quick brown? Fox Jumps!ˇ","mode":"Insert"}}
|
||||
{"Put":{"state":"ˇThe quick brown\nfox jumps over\nthe lazy dog. The quick \nbrown fox jumps over\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"ˇThe quick \nbrown fox jumps over\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick ˇbrown\nfox jumps over\nthe lazy dog. The quick \nbrown fox jumps over\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"ˇThe quick \nbrown fox jumps over\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brownˇ\nfox jumps over\nthe lazy dog. The quick \nbrown fox jumps over\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"ˇThe quick \nbrown fox jumps over\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown\nfox jumps over\nthe lazy doˇg. The quick \nbrown fox jumps over\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"ˇThe quick \nbrown fox jumps over\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown\nfox jumps over\nthe lazy dogˇ. The quick \nbrown fox jumps over\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"ˇThe quick \nbrown fox jumps over\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown\nfox jumps over\nthe lazy dog.ˇ The quick \nbrown fox jumps over\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"The quick brown\nfox jumps over\nthe lazy dog.ˇ\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown\nfox jumps over\nthe lazy dog. ˇThe quick \nbrown fox jumps over\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"The quick brown\nfox jumps over\nthe lazy dog.ˇ\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown\nfox jumps over\nthe lazy dog. The quick ˇ\nbrown fox jumps over\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"The quick brown\nfox jumps over\nthe lazy dog.ˇ\n","mode":"Insert"}}
|
||||
{"Put":{"state":"ˇThe quick brown.)]'\" Brown fox jumps. "}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"ˇBrown fox jumps. ","mode":"Insert"}}
|
||||
{"Put":{"state":"The ˇquick brown.)]'\" Brown fox jumps. "}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"ˇBrown fox jumps. ","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brownˇ.)]'\" Brown fox jumps. "}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"ˇBrown fox jumps. ","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown.)ˇ]'\" Brown fox jumps. "}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"ˇBrown fox jumps. ","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown.)]ˇ'\" Brown fox jumps. "}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"ˇBrown fox jumps. ","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown.)]'ˇ\" Brown fox jumps. "}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"ˇBrown fox jumps. ","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown.)]'\" Brown ˇfox jumps. "}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"The quick brown.)]'\" ˇ","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown.)]'\" Brown fox jumpsˇ. "}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"s"}
|
||||
{"Get":{"state":"The quick brown.)]'\" ˇ","mode":"Insert"}}
|
||||
@@ -0,0 +1,520 @@
|
||||
{"Put":{"state":"ˇTh\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"\""}
|
||||
{"Get":{"state":"Th\"ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}}
|
||||
{"Put":{"state":"Th\"ˇe \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"\""}
|
||||
{"Get":{"state":"Th\"ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}}
|
||||
{"Put":{"state":"Th\"e ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"\""}
|
||||
{"Get":{"state":"Th\"ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}}
|
||||
{"Put":{"state":"Th\"e \"ˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"\""}
|
||||
{"Get":{"state":"Th\"e \"\"ˇ\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}}
|
||||
{"Put":{"state":"Th\"e \"\"ˇqui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"\""}
|
||||
{"Get":{"state":"Th\"e \"\"ˇ\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}}
|
||||
{"Put":{"state":"Th\"e \"\"quˇi\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"\""}
|
||||
{"Get":{"state":"Th\"e \"\"ˇ\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}}
|
||||
{"Put":{"state":"Th\"e \"\"qui\"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"\""}
|
||||
{"Get":{"state":"Th\"e \"\"qui\"ˇ\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}}
|
||||
{"Put":{"state":"Th\"e \"\"qui\"ck broˇ\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"\""}
|
||||
{"Get":{"state":"Th\"e \"\"qui\"ck bro\"ˇ\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}}
|
||||
{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇfox jumps ov\"er\nthe lazy d\"o\"g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"\""}
|
||||
{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇ\"er\nthe lazy d\"o\"g","mode":"Insert"}}
|
||||
{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox juˇmps ov\"er\nthe lazy d\"o\"g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"\""}
|
||||
{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇ\"er\nthe lazy d\"o\"g","mode":"Insert"}}
|
||||
{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"ˇer\nthe lazy d\"o\"g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"\""}
|
||||
{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"ˇer\nthe lazy d\"o\"g","mode":"Normal"}}
|
||||
{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe ˇlazy d\"o\"g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"\""}
|
||||
{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"ˇ\"g","mode":"Insert"}}
|
||||
{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"ˇg"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"\""}
|
||||
{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"ˇg","mode":"Normal"}}
|
||||
{"Put":{"state":"ˇTh\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"\""}
|
||||
{"Get":{"state":"Th\"ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}}
|
||||
{"Put":{"state":"Th\"ˇe \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"\""}
|
||||
{"Get":{"state":"Th\"ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}}
|
||||
{"Put":{"state":"Th\"e ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"\""}
|
||||
{"Get":{"state":"Th\"ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}}
|
||||
{"Put":{"state":"Th\"e \"ˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"\""}
|
||||
{"Get":{"state":"Th\"e \"\"ˇ\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}}
|
||||
{"Put":{"state":"Th\"e \"\"ˇqui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"\""}
|
||||
{"Get":{"state":"Th\"e \"\"ˇ\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}}
|
||||
{"Put":{"state":"Th\"e \"\"quˇi\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"\""}
|
||||
{"Get":{"state":"Th\"e \"\"ˇ\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}}
|
||||
{"Put":{"state":"Th\"e \"\"qui\"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"\""}
|
||||
{"Get":{"state":"Th\"e \"\"qui\"ˇ\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}}
|
||||
{"Put":{"state":"Th\"e \"\"qui\"ck broˇ\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"\""}
|
||||
{"Get":{"state":"Th\"e \"\"qui\"ck bro\"ˇ\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}}
|
||||
{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇfox jumps ov\"er\nthe lazy d\"o\"g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"\""}
|
||||
{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇ\"er\nthe lazy d\"o\"g","mode":"Insert"}}
|
||||
{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox juˇmps ov\"er\nthe lazy d\"o\"g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"\""}
|
||||
{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇ\"er\nthe lazy d\"o\"g","mode":"Insert"}}
|
||||
{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"ˇer\nthe lazy d\"o\"g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"\""}
|
||||
{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"ˇer\nthe lazy d\"o\"g","mode":"Normal"}}
|
||||
{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe ˇlazy d\"o\"g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"\""}
|
||||
{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"ˇ\"g","mode":"Insert"}}
|
||||
{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"ˇg"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"\""}
|
||||
{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"ˇg","mode":"Normal"}}
|
||||
{"Put":{"state":"ˇTh\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"\""}
|
||||
{"Get":{"state":"Thˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}}
|
||||
{"Put":{"state":"Th\"ˇe \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"\""}
|
||||
{"Get":{"state":"Thˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}}
|
||||
{"Put":{"state":"Th\"e ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"\""}
|
||||
{"Get":{"state":"Thˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}}
|
||||
{"Put":{"state":"Th\"e \"ˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"\""}
|
||||
{"Get":{"state":"Th\"e \"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}}
|
||||
{"Put":{"state":"Th\"e \"\"ˇqui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"\""}
|
||||
{"Get":{"state":"Th\"e \"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}}
|
||||
{"Put":{"state":"Th\"e \"\"quˇi\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"\""}
|
||||
{"Get":{"state":"Th\"e \"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}}
|
||||
{"Put":{"state":"Th\"e \"\"qui\"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"\""}
|
||||
{"Get":{"state":"Th\"e \"\"quiˇwn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}}
|
||||
{"Put":{"state":"Th\"e \"\"qui\"ck broˇ\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"\""}
|
||||
{"Get":{"state":"Th\"e \"\"qui\"ck broˇ\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}}
|
||||
{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇfox jumps ov\"er\nthe lazy d\"o\"g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"\""}
|
||||
{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\nˇer\nthe lazy d\"o\"g","mode":"Insert"}}
|
||||
{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox juˇmps ov\"er\nthe lazy d\"o\"g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"\""}
|
||||
{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\nˇer\nthe lazy d\"o\"g","mode":"Insert"}}
|
||||
{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"ˇer\nthe lazy d\"o\"g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"\""}
|
||||
{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"ˇer\nthe lazy d\"o\"g","mode":"Normal"}}
|
||||
{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe ˇlazy d\"o\"g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"\""}
|
||||
{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy dˇg","mode":"Insert"}}
|
||||
{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"ˇg"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"\""}
|
||||
{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"ˇg","mode":"Normal"}}
|
||||
{"Put":{"state":"ˇTh\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"\""}
|
||||
{"Get":{"state":"Thˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}}
|
||||
{"Put":{"state":"Th\"ˇe \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"\""}
|
||||
{"Get":{"state":"Thˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}}
|
||||
{"Put":{"state":"Th\"e ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"\""}
|
||||
{"Get":{"state":"Thˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}}
|
||||
{"Put":{"state":"Th\"e \"ˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"\""}
|
||||
{"Get":{"state":"Th\"e \"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}}
|
||||
{"Put":{"state":"Th\"e \"\"ˇqui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"\""}
|
||||
{"Get":{"state":"Th\"e \"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}}
|
||||
{"Put":{"state":"Th\"e \"\"quˇi\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"\""}
|
||||
{"Get":{"state":"Th\"e \"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}}
|
||||
{"Put":{"state":"Th\"e \"\"qui\"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"\""}
|
||||
{"Get":{"state":"Th\"e \"\"quiˇwn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}}
|
||||
{"Put":{"state":"Th\"e \"\"qui\"ck broˇ\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"\""}
|
||||
{"Get":{"state":"Th\"e \"\"qui\"ck broˇ\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}}
|
||||
{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇfox jumps ov\"er\nthe lazy d\"o\"g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"\""}
|
||||
{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\nˇer\nthe lazy d\"o\"g","mode":"Insert"}}
|
||||
{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox juˇmps ov\"er\nthe lazy d\"o\"g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"\""}
|
||||
{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\nˇer\nthe lazy d\"o\"g","mode":"Insert"}}
|
||||
{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"ˇer\nthe lazy d\"o\"g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"\""}
|
||||
{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"ˇer\nthe lazy d\"o\"g","mode":"Normal"}}
|
||||
{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe ˇlazy d\"o\"g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"\""}
|
||||
{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy dˇg","mode":"Insert"}}
|
||||
{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"ˇg"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"\""}
|
||||
{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"ˇg","mode":"Normal"}}
|
||||
{"Put":{"state":"ˇTh)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"("}
|
||||
{"Get":{"state":"ˇTh)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Normal"}}
|
||||
{"Put":{"state":"Th)ˇe ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"("}
|
||||
{"Get":{"state":"Th)e (ˇ)qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Insert"}}
|
||||
{"Put":{"state":"Th)e ˇ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"("}
|
||||
{"Get":{"state":"Th)e (ˇ)qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Insert"}}
|
||||
{"Put":{"state":"Th)e (ˇ)qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"("}
|
||||
{"Get":{"state":"Th)e (ˇ)qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Insert"}}
|
||||
{"Put":{"state":"Th)e ()ˇqui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"("}
|
||||
{"Get":{"state":"Th)e ()qui(ˇ)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Insert"}}
|
||||
{"Put":{"state":"Th)e ()quˇi(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"("}
|
||||
{"Get":{"state":"Th)e ()qui(ˇ)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Insert"}}
|
||||
{"Put":{"state":"Th)e ()qui(ˇck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"("}
|
||||
{"Get":{"state":"Th)e ()qui(ˇ)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Insert"}}
|
||||
{"Put":{"state":"Th)e ()qui(ck broˇ)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"("}
|
||||
{"Get":{"state":"Th)e ()qui(ˇ)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Insert"}}
|
||||
{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)ˇfox jumps ov(er\nthe lazy d)o(g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"("}
|
||||
{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(ˇ)o(g","mode":"Insert"}}
|
||||
{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)fox juˇmps ov(er\nthe lazy d)o(g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"("}
|
||||
{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(ˇ)o(g","mode":"Insert"}}
|
||||
{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(ˇer\nthe lazy d)o(g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"("}
|
||||
{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(ˇ)o(g","mode":"Insert"}}
|
||||
{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe ˇlazy d)o(g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"("}
|
||||
{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(ˇ)o(g","mode":"Insert"}}
|
||||
{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(ˇg"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"("}
|
||||
{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(ˇg","mode":"Normal"}}
|
||||
{"Put":{"state":"ˇTh)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":")"}
|
||||
{"Get":{"state":"ˇTh)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Normal"}}
|
||||
{"Put":{"state":"Th)ˇe ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":")"}
|
||||
{"Get":{"state":"Th)e (ˇ)qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Insert"}}
|
||||
{"Put":{"state":"Th)e ˇ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":")"}
|
||||
{"Get":{"state":"Th)e (ˇ)qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Insert"}}
|
||||
{"Put":{"state":"Th)e (ˇ)qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":")"}
|
||||
{"Get":{"state":"Th)e (ˇ)qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Insert"}}
|
||||
{"Put":{"state":"Th)e ()ˇqui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":")"}
|
||||
{"Get":{"state":"Th)e ()qui(ˇ)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Insert"}}
|
||||
{"Put":{"state":"Th)e ()quˇi(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":")"}
|
||||
{"Get":{"state":"Th)e ()qui(ˇ)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Insert"}}
|
||||
{"Put":{"state":"Th)e ()qui(ˇck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":")"}
|
||||
{"Get":{"state":"Th)e ()qui(ˇ)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Insert"}}
|
||||
{"Put":{"state":"Th)e ()qui(ck broˇ)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":")"}
|
||||
{"Get":{"state":"Th)e ()qui(ˇ)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Insert"}}
|
||||
{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)ˇfox jumps ov(er\nthe lazy d)o(g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":")"}
|
||||
{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(ˇ)o(g","mode":"Insert"}}
|
||||
{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)fox juˇmps ov(er\nthe lazy d)o(g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":")"}
|
||||
{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(ˇ)o(g","mode":"Insert"}}
|
||||
{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(ˇer\nthe lazy d)o(g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":")"}
|
||||
{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(ˇ)o(g","mode":"Insert"}}
|
||||
{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe ˇlazy d)o(g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":")"}
|
||||
{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(ˇ)o(g","mode":"Insert"}}
|
||||
{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(ˇg"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":")"}
|
||||
{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(ˇg","mode":"Normal"}}
|
||||
{"Put":{"state":"ˇTh)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"("}
|
||||
{"Get":{"state":"ˇTh)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Normal"}}
|
||||
{"Put":{"state":"Th)ˇe ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"("}
|
||||
{"Get":{"state":"Th)e ˇqui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Insert"}}
|
||||
{"Put":{"state":"Th)e ˇ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"("}
|
||||
{"Get":{"state":"Th)e ˇqui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Insert"}}
|
||||
{"Put":{"state":"Th)e (ˇ)qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"("}
|
||||
{"Get":{"state":"Th)e ˇqui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Insert"}}
|
||||
{"Put":{"state":"Th)e ()ˇqui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"("}
|
||||
{"Get":{"state":"Th)e ()quiˇwn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Insert"}}
|
||||
{"Put":{"state":"Th)e ()quˇi(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"("}
|
||||
{"Get":{"state":"Th)e ()quiˇwn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Insert"}}
|
||||
{"Put":{"state":"Th)e ()qui(ˇck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"("}
|
||||
{"Get":{"state":"Th)e ()quiˇwn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Insert"}}
|
||||
{"Put":{"state":"Th)e ()qui(ck broˇ)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"("}
|
||||
{"Get":{"state":"Th)e ()quiˇwn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Insert"}}
|
||||
{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)ˇfox jumps ov(er\nthe lazy d)o(g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"("}
|
||||
{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ovˇo(g","mode":"Insert"}}
|
||||
{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)fox juˇmps ov(er\nthe lazy d)o(g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"("}
|
||||
{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ovˇo(g","mode":"Insert"}}
|
||||
{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(ˇer\nthe lazy d)o(g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"("}
|
||||
{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ovˇo(g","mode":"Insert"}}
|
||||
{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe ˇlazy d)o(g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"("}
|
||||
{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ovˇo(g","mode":"Insert"}}
|
||||
{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(ˇg"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"("}
|
||||
{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(ˇg","mode":"Normal"}}
|
||||
{"Put":{"state":"ˇTh)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":")"}
|
||||
{"Get":{"state":"ˇTh)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Normal"}}
|
||||
{"Put":{"state":"Th)ˇe ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":")"}
|
||||
{"Get":{"state":"Th)e ˇqui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Insert"}}
|
||||
{"Put":{"state":"Th)e ˇ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":")"}
|
||||
{"Get":{"state":"Th)e ˇqui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Insert"}}
|
||||
{"Put":{"state":"Th)e (ˇ)qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":")"}
|
||||
{"Get":{"state":"Th)e ˇqui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Insert"}}
|
||||
{"Put":{"state":"Th)e ()ˇqui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":")"}
|
||||
{"Get":{"state":"Th)e ()quiˇwn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Insert"}}
|
||||
{"Put":{"state":"Th)e ()quˇi(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":")"}
|
||||
{"Get":{"state":"Th)e ()quiˇwn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Insert"}}
|
||||
{"Put":{"state":"Th)e ()qui(ˇck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":")"}
|
||||
{"Get":{"state":"Th)e ()quiˇwn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Insert"}}
|
||||
{"Put":{"state":"Th)e ()qui(ck broˇ)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":")"}
|
||||
{"Get":{"state":"Th)e ()quiˇwn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Insert"}}
|
||||
{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)ˇfox jumps ov(er\nthe lazy d)o(g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":")"}
|
||||
{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ovˇo(g","mode":"Insert"}}
|
||||
{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)fox juˇmps ov(er\nthe lazy d)o(g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":")"}
|
||||
{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ovˇo(g","mode":"Insert"}}
|
||||
{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(ˇer\nthe lazy d)o(g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":")"}
|
||||
{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ovˇo(g","mode":"Insert"}}
|
||||
{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe ˇlazy d)o(g"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":")"}
|
||||
{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ovˇo(g","mode":"Insert"}}
|
||||
{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(ˇg"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":")"}
|
||||
{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(ˇg","mode":"Normal"}}
|
||||
@@ -0,0 +1,36 @@
|
||||
{"Put":{"state":"Teˇst"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"Teˇ","mode":"Insert"}}
|
||||
{"Put":{"state":"Tˇest test"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"Tˇ test","mode":"Insert"}}
|
||||
{"Put":{"state":"Testˇ test"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"Testˇtest","mode":"Insert"}}
|
||||
{"Put":{"state":"Tesˇt test"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"Tesˇ test","mode":"Insert"}}
|
||||
{"Put":{"state":"Test teˇst\ntest"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"Test teˇ\ntest","mode":"Insert"}}
|
||||
{"Put":{"state":"Test tesˇt\ntest"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"Test tesˇ\ntest","mode":"Insert"}}
|
||||
{"Put":{"state":"Test test\nˇ\ntest"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"Test test\nˇ\ntest","mode":"Insert"}}
|
||||
{"Put":{"state":"Test teˇst-test test"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"Test teˇ test","mode":"Insert"}}
|
||||
{"Put":{"state":"tesˇt-test"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"tesˇ-test","mode":"Insert"}}
|
||||
@@ -0,0 +1,460 @@
|
||||
{"Put":{"state":"The quick ˇbrown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick ˇ \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick browˇn \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick ˇ \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brownˇ \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick brownˇ\nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox ˇjumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick brown \nfox ˇ over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox juˇmps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick brown \nfox ˇ over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumpsˇ over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumpsˇover\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dogˇ \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dogˇ\n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \nˇ\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \nˇ\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\nˇ\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\nˇ\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\nˇ\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\nˇ\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThˇe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nˇ-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nTheˇ-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nTheˇquick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-ˇquick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-ˇ brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quˇick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-ˇ brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quickˇ brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quickˇbrown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick ˇbrown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick ˇ \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brownˇ \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brownˇ\n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \nˇ \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \nˇ\n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \nˇ \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \nˇ\n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \nˇ fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \nˇfox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumpˇs over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-ˇ over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dogˇ \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dogˇ\n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \nˇ\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \nˇ\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick ˇbrown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick ˇ \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick browˇn \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick ˇ \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brownˇ \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick brownˇ\nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox ˇjumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick brown \nfox ˇ over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox juˇmps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick brown \nfox ˇ over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumpsˇ over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumpsˇover\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dogˇ \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dogˇ\n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \nˇ\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \nˇ\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\nˇ\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\nˇ\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\nˇ\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\nˇ\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThˇe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nˇ brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nTheˇ-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nˇ brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-ˇquick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nˇ brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quˇick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nˇ brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quickˇ brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quickˇbrown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick ˇbrown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick ˇ \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brownˇ \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brownˇ\n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \nˇ \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \nˇ\n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \nˇ \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \nˇ\n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \nˇ fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \nˇfox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumpˇs over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n ˇ over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dogˇ \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dogˇ\n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \nˇ\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \nˇ\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick ˇbrown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick ˇ\nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick browˇn \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick ˇ\nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brownˇ \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick brownˇ jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox ˇjumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick brown \nfox ˇover\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox juˇmps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick brown \nfox ˇover\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumpsˇ over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumpsˇ\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dogˇ \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dogˇ\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \nˇ\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \nˇ\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\nˇ\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\nˇ\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\nˇ\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\nˇ-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThˇe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nˇ-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nTheˇ-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nTheˇquick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-ˇquick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-ˇbrown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quˇick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-ˇbrown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quickˇ brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quickˇ \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick ˇbrown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick ˇ\n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brownˇ \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brownˇ-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \nˇ \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \nˇ-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \nˇ \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \nˇ-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \nˇ fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \nˇ-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumpˇs over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-ˇover\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dogˇ \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dogˇ\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \nˇ\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \nˇ","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick ˇbrown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick ˇ\nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick browˇn \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick ˇ\nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brownˇ \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick brownˇ jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox ˇjumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick brown \nfox ˇover\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox juˇmps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick brown \nfox ˇover\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumpsˇ over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumpsˇ\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dogˇ \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dogˇ\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \nˇ\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \nˇ\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\nˇ\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\nˇ\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\nˇ\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\nˇ brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThˇe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nˇbrown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nTheˇ-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nˇbrown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-ˇquick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nˇbrown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quˇick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nˇbrown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quickˇ brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quickˇ \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick ˇbrown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick ˇ\n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brownˇ \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brownˇ over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \nˇ \n \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \nˇ over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \nˇ \n fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \nˇ over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \nˇ fox-jumps over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \nˇ over\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumpˇs over\nthe lazy dog \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n ˇover\nthe lazy dog \n\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dogˇ \n\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dogˇ\n","mode":"Insert"}}
|
||||
{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \nˇ\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"a"}
|
||||
{"Key":"shift-w"}
|
||||
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \nˇ","mode":"Insert"}}
|
||||
@@ -0,0 +1,7 @@
|
||||
{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog"}}
|
||||
{"Key":"4"}
|
||||
{"Key":"escape"}
|
||||
{"Key":"3"}
|
||||
{"Key":"d"}
|
||||
{"Key":"l"}
|
||||
{"Get":{"state":"The quick brown\nfox juˇ over\nthe lazy dog","mode":"Normal"}}
|
||||
@@ -0,0 +1,34 @@
|
||||
{"Put":{"state":"ˇone two three four"}}
|
||||
{"Key":"f"}
|
||||
{"Key":"o"}
|
||||
{"Get":{"state":"one twˇo three four","mode":"Normal"}}
|
||||
{"Key":","}
|
||||
{"Get":{"state":"ˇone two three four","mode":"Normal"}}
|
||||
{"Key":"2"}
|
||||
{"Key":";"}
|
||||
{"Get":{"state":"one two three fˇour","mode":"Normal"}}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"e"}
|
||||
{"Get":{"state":"one two threˇe four","mode":"Normal"}}
|
||||
{"Key":"2"}
|
||||
{"Key":";"}
|
||||
{"Get":{"state":"onˇe two three four","mode":"Normal"}}
|
||||
{"Key":","}
|
||||
{"Get":{"state":"one two thrˇee four","mode":"Normal"}}
|
||||
{"Put":{"state":"ˇone two three four"}}
|
||||
{"Key":"t"}
|
||||
{"Key":"o"}
|
||||
{"Get":{"state":"one tˇwo three four","mode":"Normal"}}
|
||||
{"Key":","}
|
||||
{"Get":{"state":"oˇne two three four","mode":"Normal"}}
|
||||
{"Key":"2"}
|
||||
{"Key":";"}
|
||||
{"Get":{"state":"one two three ˇfour","mode":"Normal"}}
|
||||
{"Key":"shift-t"}
|
||||
{"Key":"e"}
|
||||
{"Get":{"state":"one two threeˇ four","mode":"Normal"}}
|
||||
{"Key":"3"}
|
||||
{"Key":";"}
|
||||
{"Get":{"state":"oneˇ two three four","mode":"Normal"}}
|
||||
{"Key":","}
|
||||
{"Get":{"state":"one two thˇree four","mode":"Normal"}}
|
||||
@@ -0,0 +1,15 @@
|
||||
{"Exec":{"command":"map ,w j"}}
|
||||
{"Put":{"state":"ˇhello hello\nhello hello"}}
|
||||
{"Key":"f"}
|
||||
{"Key":"o"}
|
||||
{"Key":";"}
|
||||
{"Key":","}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"hello hello\nhello hellˇo","mode":"Normal"}}
|
||||
{"Put":{"state":"ˇhello hello\nhello hello"}}
|
||||
{"Key":"f"}
|
||||
{"Key":"o"}
|
||||
{"Key":";"}
|
||||
{"Key":","}
|
||||
{"Key":"i"}
|
||||
{"Get":{"state":"hellˇo hello\nhello hello","mode":"Insert"}}
|
||||
@@ -0,0 +1,6 @@
|
||||
{"Put":{"state":"ˇa\nb\nc"}}
|
||||
{"Key":":"}
|
||||
{"Key":"j"}
|
||||
{"Key":"enter"}
|
||||
{"Key":"^"}
|
||||
{"Get":{"state":"ˇa b\nc","mode":"Normal"}}
|
||||
@@ -0,0 +1,22 @@
|
||||
{"Put":{"state":"ˇfoo\n\nfoo\n"}}
|
||||
{"Key":":"}
|
||||
{"Key":"%"}
|
||||
{"Key":"g"}
|
||||
{"Key":"/"}
|
||||
{"Key":"f"}
|
||||
{"Key":"o"}
|
||||
{"Key":"o"}
|
||||
{"Key":"/"}
|
||||
{"Key":"n"}
|
||||
{"Key":"o"}
|
||||
{"Key":"r"}
|
||||
{"Key":"m"}
|
||||
{"Key":"space"}
|
||||
{"Key":"A"}
|
||||
{"Key":"b"}
|
||||
{"Key":"a"}
|
||||
{"Key":"r"}
|
||||
{"Key":"enter"}
|
||||
{"Get":{"state":"foobar\n\nfoobaˇr\n","mode":"Normal"}}
|
||||
{"Key":"u"}
|
||||
{"Get":{"state":"foˇo\n\nfoo\n","mode":"Normal"}}
|
||||
@@ -0,0 +1,5 @@
|
||||
{"Put":{"state":"ˇa\nb\nc"}}
|
||||
{"Key":":"}
|
||||
{"Key":"3"}
|
||||
{"Key":"enter"}
|
||||
{"Get":{"state":"a\nb\nˇc","mode":"Normal"}}
|
||||
@@ -0,0 +1,19 @@
|
||||
{"Put":{"state":"ˇa\nb\na\nb\na\n"}}
|
||||
{"Key":":"}
|
||||
{"Key":"g"}
|
||||
{"Key":"/"}
|
||||
{"Key":"a"}
|
||||
{"Key":"/"}
|
||||
{"Key":"d"}
|
||||
{"Key":"enter"}
|
||||
{"Get":{"state":"b\nb\nˇ","mode":"Normal"}}
|
||||
{"Key":"u"}
|
||||
{"Get":{"state":"ˇa\nb\na\nb\na\n","mode":"Normal"}}
|
||||
{"Key":":"}
|
||||
{"Key":"v"}
|
||||
{"Key":"/"}
|
||||
{"Key":"a"}
|
||||
{"Key":"/"}
|
||||
{"Key":"d"}
|
||||
{"Key":"enter"}
|
||||
{"Get":{"state":"a\na\nˇa","mode":"Normal"}}
|
||||
@@ -0,0 +1,28 @@
|
||||
{"Put":{"state":"ˇ1\n2\n3\n4\n4\n3\n2\n1"}}
|
||||
{"Key":":"}
|
||||
{"Key":"2"}
|
||||
{"Key":","}
|
||||
{"Key":"4"}
|
||||
{"Key":"d"}
|
||||
{"Key":"enter"}
|
||||
{"Get":{"state":"1\nˇ4\n3\n2\n1","mode":"Normal"}}
|
||||
{"Key":":"}
|
||||
{"Key":"2"}
|
||||
{"Key":","}
|
||||
{"Key":"4"}
|
||||
{"Key":"s"}
|
||||
{"Key":"o"}
|
||||
{"Key":"r"}
|
||||
{"Key":"t"}
|
||||
{"Key":"enter"}
|
||||
{"Get":{"state":"1\nˇ2\n3\n4\n1","mode":"Normal"}}
|
||||
{"Key":":"}
|
||||
{"Key":"2"}
|
||||
{"Key":","}
|
||||
{"Key":"4"}
|
||||
{"Key":"j"}
|
||||
{"Key":"o"}
|
||||
{"Key":"i"}
|
||||
{"Key":"n"}
|
||||
{"Key":"enter"}
|
||||
{"Get":{"state":"1\nˇ2 3 4\n1","mode":"Normal"}}
|
||||
@@ -0,0 +1,33 @@
|
||||
{"Put":{"state":"ˇa\nb\nb\nc"}}
|
||||
{"Key":":"}
|
||||
{"Key":"%"}
|
||||
{"Key":"s"}
|
||||
{"Key":"/"}
|
||||
{"Key":"b"}
|
||||
{"Key":"/"}
|
||||
{"Key":"d"}
|
||||
{"Key":"enter"}
|
||||
{"Get":{"state":"a\nd\nˇd\nc","mode":"Normal"}}
|
||||
{"Key":":"}
|
||||
{"Key":"%"}
|
||||
{"Key":"s"}
|
||||
{"Key":":"}
|
||||
{"Key":"."}
|
||||
{"Key":":"}
|
||||
{"Key":"\\"}
|
||||
{"Key":"0"}
|
||||
{"Key":"\\"}
|
||||
{"Key":"0"}
|
||||
{"Key":"enter"}
|
||||
{"Get":{"state":"aa\ndd\ndd\nˇcc","mode":"Normal"}}
|
||||
{"Key":"k"}
|
||||
{"Key":":"}
|
||||
{"Key":"s"}
|
||||
{"Key":"/"}
|
||||
{"Key":"d"}
|
||||
{"Key":"d"}
|
||||
{"Key":"/"}
|
||||
{"Key":"e"}
|
||||
{"Key":"e"}
|
||||
{"Key":"enter"}
|
||||
{"Get":{"state":"aa\ndd\nˇee\ncc","mode":"Normal"}}
|
||||
@@ -0,0 +1,11 @@
|
||||
{"Put":{"state":"ˇa\nb\na\nc"}}
|
||||
{"Key":":"}
|
||||
{"Key":"/"}
|
||||
{"Key":"b"}
|
||||
{"Key":"enter"}
|
||||
{"Get":{"state":"a\nˇb\na\nc","mode":"Normal"}}
|
||||
{"Key":":"}
|
||||
{"Key":"?"}
|
||||
{"Key":"a"}
|
||||
{"Key":"enter"}
|
||||
{"Get":{"state":"ˇa\nb\na\nc","mode":"Normal"}}
|
||||
@@ -0,0 +1,12 @@
|
||||
{"Put":{"state":"ˇ1\n2\n3\n4\n4\n3\n2\n1"}}
|
||||
{"Key":"v"}
|
||||
{"Key":"2"}
|
||||
{"Key":"j"}
|
||||
{"Key":":"}
|
||||
{"Key":"s"}
|
||||
{"Key":"/"}
|
||||
{"Key":"."}
|
||||
{"Key":"/"}
|
||||
{"Key":"k"}
|
||||
{"Key":"enter"}
|
||||
{"Get":{"state":"k\nk\nˇk\n4\n4\n3\n2\n1","mode":"Normal"}}
|
||||
@@ -0,0 +1,12 @@
|
||||
{"Put":{"state":"A😀c«DÉ1*fˇ»\n"}}
|
||||
{"Key":"u"}
|
||||
{"Get":{"state":"A😀cˇdé1*f\n","mode":"Normal"}}
|
||||
{"Put":{"state":"ABˇc\n"}}
|
||||
{"Key":"shift-v"}
|
||||
{"Key":"u"}
|
||||
{"Get":{"state":"ˇabc\n","mode":"Normal"}}
|
||||
{"Put":{"state":"ˇAa\nBb\nCc"}}
|
||||
{"Key":"ctrl-v"}
|
||||
{"Key":"j"}
|
||||
{"Key":"u"}
|
||||
{"Get":{"state":"ˇaa\nbb\nCc","mode":"Normal"}}
|
||||
@@ -0,0 +1,15 @@
|
||||
{"Put":{"state":"a😀C«dÉ1*fˇ»\n"}}
|
||||
{"Key":"g"}
|
||||
{"Key":"?"}
|
||||
{"Get":{"state":"a😀CˇqÉ1*s\n","mode":"Normal"}}
|
||||
{"Put":{"state":"abˇC\n"}}
|
||||
{"Key":"shift-v"}
|
||||
{"Key":"g"}
|
||||
{"Key":"?"}
|
||||
{"Get":{"state":"ˇnoP\n","mode":"Normal"}}
|
||||
{"Put":{"state":"ˇaa\nbb\ncc"}}
|
||||
{"Key":"ctrl-v"}
|
||||
{"Key":"j"}
|
||||
{"Key":"g"}
|
||||
{"Key":"?"}
|
||||
{"Get":{"state":"ˇna\nob\ncc","mode":"Normal"}}
|
||||
@@ -0,0 +1,12 @@
|
||||
{"Put":{"state":"a😀C«dÉ1*fˇ»\n"}}
|
||||
{"Key":"shift-u"}
|
||||
{"Get":{"state":"a😀CˇDÉ1*F\n","mode":"Normal"}}
|
||||
{"Put":{"state":"abˇC\n"}}
|
||||
{"Key":"shift-v"}
|
||||
{"Key":"shift-u"}
|
||||
{"Get":{"state":"ˇABC\n","mode":"Normal"}}
|
||||
{"Put":{"state":"ˇaa\nbb\ncc"}}
|
||||
{"Key":"ctrl-v"}
|
||||
{"Key":"j"}
|
||||
{"Key":"shift-u"}
|
||||
{"Get":{"state":"ˇAa\nBb\ncc","mode":"Normal"}}
|
||||
@@ -0,0 +1,28 @@
|
||||
{"SetOption":{"value":"scrolloff=3"}}
|
||||
{"SetOption":{"value":"lines=12"}}
|
||||
{"Put":{"state":"ˇaa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz"}}
|
||||
{"Key":"4"}
|
||||
{"Key":"j"}
|
||||
{"Key":"ctrl-d"}
|
||||
{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\nˇjj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}}
|
||||
{"Key":"ctrl-d"}
|
||||
{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\njj\nkk\nll\nmm\nnn\nˇoo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}}
|
||||
{"Key":"g"}
|
||||
{"Key":"g"}
|
||||
{"Key":"ctrl-d"}
|
||||
{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nhh\nˇii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}}
|
||||
{"Key":"ctrl-u"}
|
||||
{"Get":{"state":"aa\nbb\ncc\nˇdd\nee\nff\ngg\nhh\nii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}}
|
||||
{"Key":"ctrl-d"}
|
||||
{"Key":"ctrl-d"}
|
||||
{"Key":"4"}
|
||||
{"Key":"j"}
|
||||
{"Key":"ctrl-u"}
|
||||
{"Key":"ctrl-u"}
|
||||
{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nˇhh\nii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}}
|
||||
{"Key":"g"}
|
||||
{"Key":"g"}
|
||||
{"Key":"ctrl-d"}
|
||||
{"Key":"ctrl-u"}
|
||||
{"Key":"ctrl-u"}
|
||||
{"Get":{"state":"ˇaa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}}
|
||||
@@ -0,0 +1,24 @@
|
||||
{"SetOption":{"value":"scrolloff=3"}}
|
||||
{"SetOption":{"value":"lines=12"}}
|
||||
{"SetOption":{"value":"scrolloff=0"}}
|
||||
{"Put":{"state":"ˇaa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz"}}
|
||||
{"Key":"ctrl-f"}
|
||||
{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nhh\nˇii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}}
|
||||
{"Key":"ctrl-f"}
|
||||
{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\njj\nkk\nll\nmm\nnn\noo\npp\nˇqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}}
|
||||
{"Key":"ctrl-b"}
|
||||
{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nˇrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}}
|
||||
{"Key":"ctrl-b"}
|
||||
{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\nˇjj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}}
|
||||
{"Key":"g"}
|
||||
{"Key":"g"}
|
||||
{"Get":{"state":"ˇaa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}}
|
||||
{"SetOption":{"value":"scrolloff=3"}}
|
||||
{"Key":"ctrl-f"}
|
||||
{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\njj\nkk\nˇll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}}
|
||||
{"Key":"ctrl-f"}
|
||||
{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\nˇtt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}}
|
||||
{"Key":"ctrl-b"}
|
||||
{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\njj\nkk\nll\nmm\nnn\nˇoo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}}
|
||||
{"Key":"ctrl-b"}
|
||||
{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\nˇgg\nhh\nii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}}
|
||||
@@ -0,0 +1,11 @@
|
||||
{"Put":{"state":"heˇllo world."}}
|
||||
{"Key":"x"}
|
||||
{"Key":"i"}
|
||||
{"Key":"ctrl-o"}
|
||||
{"Key":"."}
|
||||
{"Get":{"state":"heˇo world.","mode":"Insert"}}
|
||||
{"Key":"l"}
|
||||
{"Key":"l"}
|
||||
{"Key":"escape"}
|
||||
{"Key":"."}
|
||||
{"Get":{"state":"hellˇllo world.","mode":"Normal"}}
|
||||
@@ -0,0 +1,10 @@
|
||||
{"Put":{"state":"helˇlo world."}}
|
||||
{"Key":"i"}
|
||||
{"Key":"ctrl-o"}
|
||||
{"Key":"d"}
|
||||
{"Key":"i"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"ˇ world.","mode":"Insert"}}
|
||||
{"Key":"ctrl-o"}
|
||||
{"Key":"p"}
|
||||
{"Get":{"state":" helloˇworld.","mode":"Insert"}}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user