Merge branch 'main' into multi-server-completions-tailwind

This commit is contained in:
Julia 2023-08-30 22:41:12 -04:00
commit ff3865a4ad
427 changed files with 43123 additions and 12861 deletions

View file

@ -1,4 +1,4 @@
use crate::Vim;
use crate::{Vim, VimEvent};
use editor::{EditorBlurred, EditorFocused, EditorReleased};
use gpui::AppContext;
@ -22,6 +22,11 @@ fn focused(EditorFocused(editor): &EditorFocused, cx: &mut AppContext) {
editor.window().update(cx, |cx| {
Vim::update(cx, |vim, cx| {
vim.set_active_editor(editor.clone(), cx);
if vim.enabled {
cx.emit_global(VimEvent::ModeChanged {
mode: vim.state().mode,
});
}
});
});
}
@ -48,6 +53,7 @@ fn released(EditorReleased(editor): &EditorReleased, cx: &mut AppContext) {
vim.active_editor = None;
}
}
vim.editor_states.remove(&editor.id())
});
});
}

View file

@ -34,7 +34,7 @@ impl ModeIndicator {
if settings::get::<VimModeSetting>(cx).0 {
mode_indicator.mode = cx
.has_global::<Vim>()
.then(|| cx.global::<Vim>().state.mode);
.then(|| cx.global::<Vim>().state().mode);
} else {
mode_indicator.mode.take();
}
@ -46,7 +46,7 @@ impl ModeIndicator {
.has_global::<Vim>()
.then(|| {
let vim = cx.global::<Vim>();
vim.enabled.then(|| vim.state.mode)
vim.enabled.then(|| vim.state().mode)
})
.flatten();
@ -80,14 +80,12 @@ impl View for ModeIndicator {
let theme = &theme::current(cx).workspace.status_bar;
// we always choose text to be 12 monospace characters
// so that as the mode indicator changes, the rest of the
// UI stays still.
let text = match mode {
Mode::Normal => "-- NORMAL --",
Mode::Insert => "-- INSERT --",
Mode::Visual { line: false } => "-- VISUAL --",
Mode::Visual { line: true } => "VISUAL LINE",
Mode::Visual => "-- VISUAL --",
Mode::VisualLine => "-- VISUAL LINE --",
Mode::VisualBlock => "-- VISUAL BLOCK --",
};
Label::new(text, theme.vim_mode_indicator.text.clone())
.contained()

View file

@ -1,8 +1,8 @@
use std::sync::Arc;
use std::{cmp, sync::Arc};
use editor::{
char_kind,
display_map::{DisplaySnapshot, ToDisplayPoint},
display_map::{DisplaySnapshot, FoldPoint, ToDisplayPoint},
movement, Bias, CharKind, DisplayPoint, ToOffset,
};
use gpui::{actions, impl_actions, AppContext, WindowContext};
@ -21,16 +21,16 @@ use crate::{
pub enum Motion {
Left,
Backspace,
Down,
Up,
Down { display_lines: bool },
Up { display_lines: bool },
Right,
NextWordStart { ignore_punctuation: bool },
NextWordEnd { ignore_punctuation: bool },
PreviousWordStart { ignore_punctuation: bool },
FirstNonWhitespace,
FirstNonWhitespace { display_lines: bool },
CurrentLine,
StartOfLine,
EndOfLine,
StartOfLine { display_lines: bool },
EndOfLine { display_lines: bool },
StartOfParagraph,
EndOfParagraph,
StartOfDocument,
@ -62,6 +62,41 @@ struct PreviousWordStart {
ignore_punctuation: bool,
}
#[derive(Clone, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
struct Up {
#[serde(default)]
display_lines: bool,
}
#[derive(Clone, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
struct Down {
#[serde(default)]
display_lines: bool,
}
#[derive(Clone, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
struct FirstNonWhitespace {
#[serde(default)]
display_lines: bool,
}
#[derive(Clone, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
struct EndOfLine {
#[serde(default)]
display_lines: bool,
}
#[derive(Clone, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
struct StartOfLine {
#[serde(default)]
display_lines: bool,
}
#[derive(Clone, Deserialize, PartialEq)]
struct RepeatFind {
#[serde(default)]
@ -73,12 +108,7 @@ actions!(
[
Left,
Backspace,
Down,
Up,
Right,
FirstNonWhitespace,
StartOfLine,
EndOfLine,
CurrentLine,
StartOfParagraph,
EndOfParagraph,
@ -90,20 +120,63 @@ actions!(
);
impl_actions!(
vim,
[NextWordStart, NextWordEnd, PreviousWordStart, RepeatFind]
[
NextWordStart,
NextWordEnd,
PreviousWordStart,
RepeatFind,
Up,
Down,
FirstNonWhitespace,
EndOfLine,
StartOfLine,
]
);
pub fn init(cx: &mut AppContext) {
cx.add_action(|_: &mut Workspace, _: &Left, cx: _| motion(Motion::Left, cx));
cx.add_action(|_: &mut Workspace, _: &Backspace, cx: _| motion(Motion::Backspace, cx));
cx.add_action(|_: &mut Workspace, _: &Down, cx: _| motion(Motion::Down, cx));
cx.add_action(|_: &mut Workspace, _: &Up, cx: _| motion(Motion::Up, cx));
cx.add_action(|_: &mut Workspace, _: &Right, cx: _| motion(Motion::Right, cx));
cx.add_action(|_: &mut Workspace, _: &FirstNonWhitespace, cx: _| {
motion(Motion::FirstNonWhitespace, cx)
cx.add_action(|_: &mut Workspace, action: &Down, cx: _| {
motion(
Motion::Down {
display_lines: action.display_lines,
},
cx,
)
});
cx.add_action(|_: &mut Workspace, action: &Up, cx: _| {
motion(
Motion::Up {
display_lines: action.display_lines,
},
cx,
)
});
cx.add_action(|_: &mut Workspace, _: &Right, cx: _| motion(Motion::Right, cx));
cx.add_action(|_: &mut Workspace, action: &FirstNonWhitespace, cx: _| {
motion(
Motion::FirstNonWhitespace {
display_lines: action.display_lines,
},
cx,
)
});
cx.add_action(|_: &mut Workspace, action: &StartOfLine, cx: _| {
motion(
Motion::StartOfLine {
display_lines: action.display_lines,
},
cx,
)
});
cx.add_action(|_: &mut Workspace, action: &EndOfLine, cx: _| {
motion(
Motion::EndOfLine {
display_lines: action.display_lines,
},
cx,
)
});
cx.add_action(|_: &mut Workspace, _: &StartOfLine, cx: _| motion(Motion::StartOfLine, cx));
cx.add_action(|_: &mut Workspace, _: &EndOfLine, cx: _| motion(Motion::EndOfLine, cx));
cx.add_action(|_: &mut Workspace, _: &CurrentLine, cx: _| motion(Motion::CurrentLine, cx));
cx.add_action(|_: &mut Workspace, _: &StartOfParagraph, cx: _| {
motion(Motion::StartOfParagraph, cx)
@ -147,9 +220,9 @@ pub(crate) fn motion(motion: Motion, cx: &mut WindowContext) {
let times = Vim::update(cx, |vim, cx| vim.pop_number_operator(cx));
let operator = Vim::read(cx).active_operator();
match Vim::read(cx).state.mode {
match Vim::read(cx).state().mode {
Mode::Normal => normal_motion(motion, operator, times, cx),
Mode::Visual { .. } => visual_motion(motion, times, cx),
Mode::Visual | Mode::VisualLine | Mode::VisualBlock => visual_motion(motion, times, cx),
Mode::Insert => {
// Shouldn't execute a motion in insert mode. Ignoring
}
@ -158,7 +231,7 @@ pub(crate) fn motion(motion: Motion, cx: &mut WindowContext) {
}
fn repeat_motion(backwards: bool, cx: &mut WindowContext) {
let find = match Vim::read(cx).state.last_find.clone() {
let find = match Vim::read(cx).workspace_state.last_find.clone() {
Some(Motion::FindForward { before, text }) => {
if backwards {
Motion::FindBackward {
@ -192,19 +265,25 @@ impl Motion {
pub fn linewise(&self) -> bool {
use Motion::*;
match self {
Down | Up | StartOfDocument | EndOfDocument | CurrentLine | NextLineStart
| StartOfParagraph | EndOfParagraph => true,
EndOfLine
Down { .. }
| Up { .. }
| StartOfDocument
| EndOfDocument
| CurrentLine
| NextLineStart
| StartOfParagraph
| EndOfParagraph => true,
EndOfLine { .. }
| NextWordEnd { .. }
| Matching
| FindForward { .. }
| Left
| Backspace
| Right
| StartOfLine
| StartOfLine { .. }
| NextWordStart { .. }
| PreviousWordStart { .. }
| FirstNonWhitespace
| FirstNonWhitespace { .. }
| FindBackward { .. } => false,
}
}
@ -213,21 +292,21 @@ impl Motion {
use Motion::*;
match self {
StartOfDocument | EndOfDocument | CurrentLine => true,
Down
| Up
| EndOfLine
Down { .. }
| Up { .. }
| EndOfLine { .. }
| NextWordEnd { .. }
| Matching
| FindForward { .. }
| Left
| Backspace
| Right
| StartOfLine
| StartOfLine { .. }
| StartOfParagraph
| EndOfParagraph
| NextWordStart { .. }
| PreviousWordStart { .. }
| FirstNonWhitespace
| FirstNonWhitespace { .. }
| FindBackward { .. }
| NextLineStart => false,
}
@ -236,12 +315,12 @@ impl Motion {
pub fn inclusive(&self) -> bool {
use Motion::*;
match self {
Down
| Up
Down { .. }
| Up { .. }
| StartOfDocument
| EndOfDocument
| CurrentLine
| EndOfLine
| EndOfLine { .. }
| NextWordEnd { .. }
| Matching
| FindForward { .. }
@ -249,12 +328,12 @@ impl Motion {
Left
| Backspace
| Right
| StartOfLine
| StartOfLine { .. }
| StartOfParagraph
| EndOfParagraph
| NextWordStart { .. }
| PreviousWordStart { .. }
| FirstNonWhitespace
| FirstNonWhitespace { .. }
| FindBackward { .. } => false,
}
}
@ -272,8 +351,18 @@ impl Motion {
let (new_point, goal) = match self {
Left => (left(map, point, times), SelectionGoal::None),
Backspace => (backspace(map, point, times), SelectionGoal::None),
Down => down(map, point, goal, times),
Up => up(map, point, goal, times),
Down {
display_lines: false,
} => down(map, point, goal, times),
Down {
display_lines: true,
} => down_display(map, point, goal, times),
Up {
display_lines: false,
} => up(map, point, goal, times),
Up {
display_lines: true,
} => up_display(map, point, goal, times),
Right => (right(map, point, times), SelectionGoal::None),
NextWordStart { ignore_punctuation } => (
next_word_start(map, point, *ignore_punctuation, times),
@ -287,9 +376,17 @@ impl Motion {
previous_word_start(map, point, *ignore_punctuation, times),
SelectionGoal::None,
),
FirstNonWhitespace => (first_non_whitespace(map, point), SelectionGoal::None),
StartOfLine => (start_of_line(map, point), SelectionGoal::None),
EndOfLine => (end_of_line(map, point), SelectionGoal::None),
FirstNonWhitespace { display_lines } => (
first_non_whitespace(map, *display_lines, point),
SelectionGoal::None,
),
StartOfLine { display_lines } => (
start_of_line(map, *display_lines, point),
SelectionGoal::None,
),
EndOfLine { display_lines } => {
(end_of_line(map, *display_lines, point), SelectionGoal::None)
}
StartOfParagraph => (
movement::start_of_paragraph(map, point, times),
SelectionGoal::None,
@ -298,7 +395,7 @@ impl Motion {
map.clip_at_line_end(movement::end_of_paragraph(map, point, times)),
SelectionGoal::None,
),
CurrentLine => (end_of_line(map, point), SelectionGoal::None),
CurrentLine => (end_of_line(map, false, point), SelectionGoal::None),
StartOfDocument => (start_of_document(map, point, times), SelectionGoal::None),
EndOfDocument => (
end_of_document(map, point, maybe_times),
@ -399,6 +496,33 @@ fn backspace(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> Di
}
fn down(
map: &DisplaySnapshot,
point: DisplayPoint,
mut goal: SelectionGoal,
times: usize,
) -> (DisplayPoint, SelectionGoal) {
let start = map.display_point_to_fold_point(point, Bias::Left);
let goal_column = match goal {
SelectionGoal::Column(column) => column,
SelectionGoal::ColumnRange { end, .. } => end,
_ => {
goal = SelectionGoal::Column(start.column());
start.column()
}
};
let new_row = cmp::min(
start.row() + times as u32,
map.buffer_snapshot.max_point().row,
);
let new_col = cmp::min(goal_column, map.fold_snapshot.line_len(new_row));
let point = map.fold_point_to_display_point(FoldPoint::new(new_row, new_col));
(map.clip_point(point, Bias::Left), goal)
}
fn down_display(
map: &DisplaySnapshot,
mut point: DisplayPoint,
mut goal: SelectionGoal,
@ -407,10 +531,35 @@ fn down(
for _ in 0..times {
(point, goal) = movement::down(map, point, goal, true);
}
(point, goal)
}
fn up(
pub(crate) fn up(
map: &DisplaySnapshot,
point: DisplayPoint,
mut goal: SelectionGoal,
times: usize,
) -> (DisplayPoint, SelectionGoal) {
let start = map.display_point_to_fold_point(point, Bias::Left);
let goal_column = match goal {
SelectionGoal::Column(column) => column,
SelectionGoal::ColumnRange { end, .. } => end,
_ => {
goal = SelectionGoal::Column(start.column());
start.column()
}
};
let new_row = start.row().saturating_sub(times as u32);
let new_col = cmp::min(goal_column, map.fold_snapshot.line_len(new_row));
let point = map.fold_point_to_display_point(FoldPoint::new(new_row, new_col));
(map.clip_point(point, Bias::Left), goal)
}
fn up_display(
map: &DisplaySnapshot,
mut point: DisplayPoint,
mut goal: SelectionGoal,
@ -419,6 +568,7 @@ fn up(
for _ in 0..times {
(point, goal) = movement::up(map, point, goal, true);
}
(point, goal)
}
@ -509,8 +659,12 @@ fn previous_word_start(
point
}
fn first_non_whitespace(map: &DisplaySnapshot, from: DisplayPoint) -> DisplayPoint {
let mut last_point = DisplayPoint::new(from.row(), 0);
fn first_non_whitespace(
map: &DisplaySnapshot,
display_lines: bool,
from: DisplayPoint,
) -> DisplayPoint {
let mut last_point = start_of_line(map, display_lines, from);
let scope = map.buffer_snapshot.language_scope_at(from.to_point(map));
for (ch, point) in map.chars_at(last_point) {
if ch == '\n' {
@ -527,12 +681,31 @@ fn first_non_whitespace(map: &DisplaySnapshot, from: DisplayPoint) -> DisplayPoi
map.clip_point(last_point, Bias::Left)
}
fn start_of_line(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
map.prev_line_boundary(point.to_point(map)).1
pub(crate) fn start_of_line(
map: &DisplaySnapshot,
display_lines: bool,
point: DisplayPoint,
) -> DisplayPoint {
if display_lines {
map.clip_point(DisplayPoint::new(point.row(), 0), Bias::Right)
} else {
map.prev_line_boundary(point.to_point(map)).1
}
}
fn end_of_line(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left)
pub(crate) fn end_of_line(
map: &DisplaySnapshot,
display_lines: bool,
point: DisplayPoint,
) -> DisplayPoint {
if display_lines {
map.clip_point(
DisplayPoint::new(point.row(), map.line_len(point.row())),
Bias::Left,
)
} else {
map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left)
}
}
fn start_of_document(map: &DisplaySnapshot, point: DisplayPoint, line: usize) -> DisplayPoint {
@ -654,8 +827,8 @@ fn find_backward(
}
fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
let new_row = (point.row() + times as u32).min(map.max_buffer_row());
map.clip_point(DisplayPoint::new(new_row, 0), Bias::Left)
let correct_line = down(map, point, SelectionGoal::None, times).0;
first_non_whitespace(map, false, correct_line)
}
#[cfg(test)]
@ -803,4 +976,12 @@ mod test {
cx.simulate_shared_keystrokes([","]).await;
cx.assert_shared_state("one two thˇree four").await;
}
#[gpui::test]
async fn test_next_line_start(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
cx.set_shared_state("ˇone\n two\nthree").await;
cx.simulate_shared_keystrokes(["enter"]).await;
cx.assert_shared_state("one\n ˇtwo\nthree").await;
}
}

View file

@ -1,26 +1,25 @@
mod case;
mod change;
mod delete;
mod paste;
mod scroll;
mod search;
pub mod substitute;
mod yank;
use std::{borrow::Cow, sync::Arc};
use std::sync::Arc;
use crate::{
motion::Motion,
motion::{self, Motion},
object::Object,
state::{Mode, Operator},
Vim,
};
use collections::{HashMap, HashSet};
use editor::{
display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Anchor, Bias, ClipboardSelection,
DisplayPoint,
};
use collections::HashSet;
use editor::scroll::autoscroll::Autoscroll;
use editor::{Bias, DisplayPoint};
use gpui::{actions, AppContext, ViewContext, WindowContext};
use language::{AutoindentMode, Point, SelectionGoal};
use language::SelectionGoal;
use log::error;
use workspace::Workspace;
@ -44,7 +43,6 @@ actions!(
DeleteRight,
ChangeToEndOfLine,
DeleteToEndOfLine,
Paste,
Yank,
Substitute,
ChangeCase,
@ -80,18 +78,31 @@ pub fn init(cx: &mut AppContext) {
cx.add_action(|_: &mut Workspace, _: &ChangeToEndOfLine, cx| {
Vim::update(cx, |vim, cx| {
let times = vim.pop_number_operator(cx);
change_motion(vim, Motion::EndOfLine, times, cx);
change_motion(
vim,
Motion::EndOfLine {
display_lines: false,
},
times,
cx,
);
})
});
cx.add_action(|_: &mut Workspace, _: &DeleteToEndOfLine, cx| {
Vim::update(cx, |vim, cx| {
let times = vim.pop_number_operator(cx);
delete_motion(vim, Motion::EndOfLine, times, cx);
delete_motion(
vim,
Motion::EndOfLine {
display_lines: false,
},
times,
cx,
);
})
});
cx.add_action(paste);
scroll::init(cx);
paste::init(cx);
}
pub fn normal_motion(
@ -116,8 +127,8 @@ pub fn normal_motion(
pub fn normal_object(object: Object, cx: &mut WindowContext) {
Vim::update(cx, |vim, cx| {
match vim.state.operator_stack.pop() {
Some(Operator::Object { around }) => match vim.state.operator_stack.pop() {
match vim.maybe_pop_operator() {
Some(Operator::Object { around }) => match vim.maybe_pop_operator() {
Some(Operator::Change) => change_object(vim, object, around, cx),
Some(Operator::Delete) => delete_object(vim, object, around, cx),
Some(Operator::Yank) => yank_object(vim, object, around, cx),
@ -168,7 +179,10 @@ fn insert_first_non_whitespace(
vim.update_active_editor(cx, |editor, cx| {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.maybe_move_cursors_with(|map, cursor, goal| {
Motion::FirstNonWhitespace.move_point(map, cursor, goal, None)
Motion::FirstNonWhitespace {
display_lines: false,
}
.move_point(map, cursor, goal, None)
});
});
});
@ -181,7 +195,7 @@ fn insert_end_of_line(_: &mut Workspace, _: &InsertEndOfLine, cx: &mut ViewConte
vim.update_active_editor(cx, |editor, cx| {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.maybe_move_cursors_with(|map, cursor, goal| {
Motion::EndOfLine.move_point(map, cursor, goal, None)
Motion::CurrentLine.move_point(map, cursor, goal, None)
});
});
});
@ -200,19 +214,19 @@ fn insert_line_above(_: &mut Workspace, _: &InsertLineAbove, cx: &mut ViewContex
.collect();
let edits = selection_start_rows.into_iter().map(|row| {
let (indent, _) = map.line_indent(row);
let start_of_line = map
.clip_point(DisplayPoint::new(row, 0), Bias::Left)
.to_point(&map);
let start_of_line =
motion::start_of_line(&map, false, DisplayPoint::new(row, 0))
.to_point(&map);
let mut new_text = " ".repeat(indent as usize);
new_text.push('\n');
(start_of_line..start_of_line, new_text)
});
editor.edit_with_autoindent(edits, cx);
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_cursors_with(|map, mut cursor, _| {
*cursor.row_mut() -= 1;
*cursor.column_mut() = map.line_len(cursor.row());
(map.clip_point(cursor, Bias::Left), SelectionGoal::None)
s.move_cursors_with(|map, cursor, _| {
let previous_line = motion::up(map, cursor, SelectionGoal::None, 1).0;
let insert_point = motion::end_of_line(map, false, previous_line);
(insert_point, SelectionGoal::None)
});
});
});
@ -226,22 +240,23 @@ fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, cx: &mut ViewContex
vim.update_active_editor(cx, |editor, cx| {
editor.transact(cx, |editor, cx| {
let (map, old_selections) = editor.selections.all_display(cx);
let selection_end_rows: HashSet<u32> = old_selections
.into_iter()
.map(|selection| selection.end.row())
.collect();
let edits = selection_end_rows.into_iter().map(|row| {
let (indent, _) = map.line_indent(row);
let end_of_line = map
.clip_point(DisplayPoint::new(row, map.line_len(row)), Bias::Left)
.to_point(&map);
let end_of_line =
motion::end_of_line(&map, false, DisplayPoint::new(row, 0)).to_point(&map);
let mut new_text = "\n".to_string();
new_text.push_str(&" ".repeat(indent as usize));
(end_of_line..end_of_line, new_text)
});
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.maybe_move_cursors_with(|map, cursor, goal| {
Motion::EndOfLine.move_point(map, cursor, goal, None)
Motion::CurrentLine.move_point(map, cursor, goal, None)
});
});
editor.edit_with_autoindent(edits, cx);
@ -250,144 +265,6 @@ fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, cx: &mut ViewContex
});
}
fn paste(_: &mut Workspace, _: &Paste, cx: &mut ViewContext<Workspace>) {
Vim::update(cx, |vim, cx| {
vim.update_active_editor(cx, |editor, cx| {
editor.transact(cx, |editor, cx| {
editor.set_clip_at_line_ends(false, cx);
if let Some(item) = cx.read_from_clipboard() {
let mut clipboard_text = Cow::Borrowed(item.text());
if let Some(mut clipboard_selections) =
item.metadata::<Vec<ClipboardSelection>>()
{
let (display_map, selections) = editor.selections.all_display(cx);
let all_selections_were_entire_line =
clipboard_selections.iter().all(|s| s.is_entire_line);
if clipboard_selections.len() != selections.len() {
let mut newline_separated_text = String::new();
let mut clipboard_selections =
clipboard_selections.drain(..).peekable();
let mut ix = 0;
while let Some(clipboard_selection) = clipboard_selections.next() {
newline_separated_text
.push_str(&clipboard_text[ix..ix + clipboard_selection.len]);
ix += clipboard_selection.len;
if clipboard_selections.peek().is_some() {
newline_separated_text.push('\n');
}
}
clipboard_text = Cow::Owned(newline_separated_text);
}
// If the pasted text is a single line, the cursor should be placed after
// the newly pasted text. This is easiest done with an anchor after the
// insertion, and then with a fixup to move the selection back one position.
// However if the pasted text is linewise, the cursor should be placed at the start
// of the new text on the following line. This is easiest done with a manually adjusted
// point.
// This enum lets us represent both cases
enum NewPosition {
Inside(Point),
After(Anchor),
}
let mut new_selections: HashMap<usize, NewPosition> = Default::default();
editor.buffer().update(cx, |buffer, cx| {
let snapshot = buffer.snapshot(cx);
let mut start_offset = 0;
let mut edits = Vec::new();
for (ix, selection) in selections.iter().enumerate() {
let to_insert;
let linewise;
if let Some(clipboard_selection) = clipboard_selections.get(ix) {
let end_offset = start_offset + clipboard_selection.len;
to_insert = &clipboard_text[start_offset..end_offset];
linewise = clipboard_selection.is_entire_line;
start_offset = end_offset;
} else {
to_insert = clipboard_text.as_str();
linewise = all_selections_were_entire_line;
}
// If the clipboard text was copied linewise, and the current selection
// is empty, then paste the text after this line and move the selection
// to the start of the pasted text
let insert_at = if linewise {
let (point, _) = display_map
.next_line_boundary(selection.start.to_point(&display_map));
if !to_insert.starts_with('\n') {
// Add newline before pasted text so that it shows up
edits.push((point..point, "\n"));
}
// Drop selection at the start of the next line
new_selections.insert(
selection.id,
NewPosition::Inside(Point::new(point.row + 1, 0)),
);
point
} else {
let mut point = selection.end;
// Paste the text after the current selection
*point.column_mut() = point.column() + 1;
let point = display_map
.clip_point(point, Bias::Right)
.to_point(&display_map);
new_selections.insert(
selection.id,
if to_insert.contains('\n') {
NewPosition::Inside(point)
} else {
NewPosition::After(snapshot.anchor_after(point))
},
);
point
};
if linewise && to_insert.ends_with('\n') {
edits.push((
insert_at..insert_at,
&to_insert[0..to_insert.len().saturating_sub(1)],
))
} else {
edits.push((insert_at..insert_at, to_insert));
}
}
drop(snapshot);
buffer.edit(edits, Some(AutoindentMode::EachLine), cx);
});
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_with(|map, selection| {
if let Some(new_position) = new_selections.get(&selection.id) {
match new_position {
NewPosition::Inside(new_point) => {
selection.collapse_to(
new_point.to_display_point(map),
SelectionGoal::None,
);
}
NewPosition::After(after_point) => {
let mut new_point = after_point.to_display_point(map);
*new_point.column_mut() =
new_point.column().saturating_sub(1);
new_point = map.clip_point(new_point, Bias::Left);
selection.collapse_to(new_point, SelectionGoal::None);
}
}
}
});
});
} else {
editor.insert(&clipboard_text, cx);
}
}
editor.set_clip_at_line_ends(true, cx);
});
});
});
}
pub(crate) fn normal_replace(text: Arc<str>, cx: &mut WindowContext) {
Vim::update(cx, |vim, cx| {
vim.update_active_editor(cx, |editor, cx| {
@ -883,36 +760,6 @@ mod test {
.await;
}
#[gpui::test]
async fn test_p(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
cx.set_shared_state(indoc! {"
The quick brown
fox juˇmps over
the lazy dog"})
.await;
cx.simulate_shared_keystrokes(["d", "d"]).await;
cx.assert_state_matches().await;
cx.simulate_shared_keystroke("p").await;
cx.assert_state_matches().await;
cx.set_shared_state(indoc! {"
The quick brown
fox ˇjumps over
the lazy dog"})
.await;
cx.simulate_shared_keystrokes(["v", "w", "y"]).await;
cx.set_shared_state(indoc! {"
The quick brown
fox jumps oveˇr
the lazy dog"})
.await;
cx.simulate_shared_keystroke("p").await;
cx.assert_state_matches().await;
}
#[gpui::test]
async fn test_repeated_word(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;

View file

@ -13,15 +13,15 @@ pub fn change_case(_: &mut Workspace, _: &ChangeCase, cx: &mut ViewContext<Works
let mut cursor_positions = Vec::new();
let snapshot = editor.buffer().read(cx).snapshot(cx);
for selection in editor.selections.all::<Point>(cx) {
match vim.state.mode {
Mode::Visual { line: true } => {
match vim.state().mode {
Mode::VisualLine => {
let start = Point::new(selection.start.row, 0);
let end =
Point::new(selection.end.row, snapshot.line_len(selection.end.row));
ranges.push(start..end);
cursor_positions.push(start..start);
}
Mode::Visual { line: false } => {
Mode::Visual | Mode::VisualBlock => {
ranges.push(selection.start..selection.end);
cursor_positions.push(selection.start..selection.start);
}

View file

@ -10,7 +10,11 @@ pub fn change_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &m
// Some motions ignore failure when switching to normal mode
let mut motion_succeeded = matches!(
motion,
Motion::Left | Motion::Right | Motion::EndOfLine | Motion::Backspace | Motion::StartOfLine
Motion::Left
| Motion::Right
| Motion::EndOfLine { .. }
| Motion::Backspace
| Motion::StartOfLine { .. }
);
vim.update_active_editor(cx, |editor, cx| {
editor.transact(cx, |editor, cx| {

View file

@ -0,0 +1,468 @@
use std::{borrow::Cow, cmp};
use editor::{
display_map::ToDisplayPoint, movement, scroll::autoscroll::Autoscroll, ClipboardSelection,
DisplayPoint,
};
use gpui::{impl_actions, AppContext, ViewContext};
use language::{Bias, SelectionGoal};
use serde::Deserialize;
use workspace::Workspace;
use crate::{state::Mode, utils::copy_selections_content, Vim};
#[derive(Clone, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
struct Paste {
#[serde(default)]
before: bool,
#[serde(default)]
preserve_clipboard: bool,
}
impl_actions!(vim, [Paste]);
pub(crate) fn init(cx: &mut AppContext) {
cx.add_action(paste);
}
fn paste(_: &mut Workspace, action: &Paste, cx: &mut ViewContext<Workspace>) {
Vim::update(cx, |vim, cx| {
vim.update_active_editor(cx, |editor, cx| {
editor.transact(cx, |editor, cx| {
editor.set_clip_at_line_ends(false, cx);
let Some(item) = cx.read_from_clipboard() else {
return;
};
let clipboard_text = Cow::Borrowed(item.text());
if clipboard_text.is_empty() {
return;
}
if !action.preserve_clipboard && vim.state().mode.is_visual() {
copy_selections_content(editor, vim.state().mode == Mode::VisualLine, cx);
}
// if we are copying from multi-cursor (of visual block mode), we want
// to
let clipboard_selections =
item.metadata::<Vec<ClipboardSelection>>()
.filter(|clipboard_selections| {
clipboard_selections.len() > 1 && vim.state().mode != Mode::VisualLine
});
let (display_map, current_selections) = editor.selections.all_adjusted_display(cx);
// unlike zed, if you have a multi-cursor selection from vim block mode,
// pasting it will paste it on subsequent lines, even if you don't yet
// have a cursor there.
let mut selections_to_process = Vec::new();
let mut i = 0;
while i < current_selections.len() {
selections_to_process
.push((current_selections[i].start..current_selections[i].end, true));
i += 1;
}
if let Some(clipboard_selections) = clipboard_selections.as_ref() {
let left = current_selections
.iter()
.map(|selection| cmp::min(selection.start.column(), selection.end.column()))
.min()
.unwrap();
let mut row = current_selections.last().unwrap().end.row() + 1;
while i < clipboard_selections.len() {
let cursor =
display_map.clip_point(DisplayPoint::new(row, left), Bias::Left);
selections_to_process.push((cursor..cursor, false));
i += 1;
row += 1;
}
}
let first_selection_indent_column =
clipboard_selections.as_ref().and_then(|zed_selections| {
zed_selections
.first()
.map(|selection| selection.first_line_indent)
});
let before = action.before || vim.state().mode == Mode::VisualLine;
let mut edits = Vec::new();
let mut new_selections = Vec::new();
let mut original_indent_columns = Vec::new();
let mut start_offset = 0;
for (ix, (selection, preserve)) in selections_to_process.iter().enumerate() {
let (mut to_insert, original_indent_column) =
if let Some(clipboard_selections) = &clipboard_selections {
if let Some(clipboard_selection) = clipboard_selections.get(ix) {
let end_offset = start_offset + clipboard_selection.len;
let text = clipboard_text[start_offset..end_offset].to_string();
start_offset = end_offset + 1;
(text, Some(clipboard_selection.first_line_indent))
} else {
("".to_string(), first_selection_indent_column)
}
} else {
(clipboard_text.to_string(), first_selection_indent_column)
};
let line_mode = to_insert.ends_with("\n");
let is_multiline = to_insert.contains("\n");
if line_mode && !before {
if selection.is_empty() {
to_insert =
"\n".to_owned() + &to_insert[..to_insert.len() - "\n".len()];
} else {
to_insert = "\n".to_owned() + &to_insert;
}
} else if !line_mode && vim.state().mode == Mode::VisualLine {
to_insert = to_insert + "\n";
}
let display_range = if !selection.is_empty() {
selection.start..selection.end
} else if line_mode {
let point = if before {
movement::line_beginning(&display_map, selection.start, false)
} else {
movement::line_end(&display_map, selection.start, false)
};
point..point
} else {
let point = if before {
selection.start
} else {
movement::saturating_right(&display_map, selection.start)
};
point..point
};
let point_range = display_range.start.to_point(&display_map)
..display_range.end.to_point(&display_map);
let anchor = if is_multiline || vim.state().mode == Mode::VisualLine {
display_map.buffer_snapshot.anchor_before(point_range.start)
} else {
display_map.buffer_snapshot.anchor_after(point_range.end)
};
if *preserve {
new_selections.push((anchor, line_mode, is_multiline));
}
edits.push((point_range, to_insert));
original_indent_columns.extend(original_indent_column);
}
editor.edit_with_block_indent(edits, original_indent_columns, cx);
// in line_mode vim will insert the new text on the next (or previous if before) line
// and put the cursor on the first non-blank character of the first inserted line (or at the end if the first line is blank).
// otherwise vim will insert the next text at (or before) the current cursor position,
// the cursor will go to the last (or first, if is_multiline) inserted character.
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.replace_cursors_with(|map| {
let mut cursors = Vec::new();
for (anchor, line_mode, is_multiline) in &new_selections {
let mut cursor = anchor.to_display_point(map);
if *line_mode {
if !before {
cursor =
movement::down(map, cursor, SelectionGoal::None, false).0;
}
cursor = movement::indented_line_beginning(map, cursor, true);
} else if !is_multiline {
cursor = movement::saturating_left(map, cursor)
}
cursors.push(cursor);
if vim.state().mode == Mode::VisualBlock {
break;
}
}
cursors
});
})
});
});
vim.switch_mode(Mode::Normal, true, cx);
});
}
#[cfg(test)]
mod test {
use crate::{
state::Mode,
test::{NeovimBackedTestContext, VimTestContext},
};
use indoc::indoc;
#[gpui::test]
async fn test_paste(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
// single line
cx.set_shared_state(indoc! {"
The quick brown
fox ˇjumps over
the lazy dog"})
.await;
cx.simulate_shared_keystrokes(["v", "w", "y"]).await;
cx.assert_shared_clipboard("jumps o").await;
cx.set_shared_state(indoc! {"
The quick brown
fox jumps oveˇr
the lazy dog"})
.await;
cx.simulate_shared_keystroke("p").await;
cx.assert_shared_state(indoc! {"
The quick brown
fox jumps overjumps ˇo
the lazy dog"})
.await;
cx.set_shared_state(indoc! {"
The quick brown
fox jumps oveˇr
the lazy dog"})
.await;
cx.simulate_shared_keystroke("shift-p").await;
cx.assert_shared_state(indoc! {"
The quick brown
fox jumps ovejumps ˇor
the lazy dog"})
.await;
// line mode
cx.set_shared_state(indoc! {"
The quick brown
fox juˇmps over
the lazy dog"})
.await;
cx.simulate_shared_keystrokes(["d", "d"]).await;
cx.assert_shared_clipboard("fox jumps over\n").await;
cx.assert_shared_state(indoc! {"
The quick brown
the laˇzy dog"})
.await;
cx.simulate_shared_keystroke("p").await;
cx.assert_shared_state(indoc! {"
The quick brown
the lazy dog
ˇfox jumps over"})
.await;
cx.simulate_shared_keystrokes(["k", "shift-p"]).await;
cx.assert_shared_state(indoc! {"
The quick brown
ˇfox jumps over
the lazy dog
fox jumps over"})
.await;
// multiline, cursor to first character of pasted text.
cx.set_shared_state(indoc! {"
The quick brown
fox jumps ˇover
the lazy dog"})
.await;
cx.simulate_shared_keystrokes(["v", "j", "y"]).await;
cx.assert_shared_clipboard("over\nthe lazy do").await;
cx.simulate_shared_keystroke("p").await;
cx.assert_shared_state(indoc! {"
The quick brown
fox jumps oˇover
the lazy dover
the lazy dog"})
.await;
cx.simulate_shared_keystrokes(["u", "shift-p"]).await;
cx.assert_shared_state(indoc! {"
The quick brown
fox jumps ˇover
the lazy doover
the lazy dog"})
.await;
}
#[gpui::test]
async fn test_paste_visual(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
// copy in visual mode
cx.set_shared_state(indoc! {"
The quick brown
fox jˇumps over
the lazy dog"})
.await;
cx.simulate_shared_keystrokes(["v", "i", "w", "y"]).await;
cx.assert_shared_state(indoc! {"
The quick brown
fox ˇjumps over
the lazy dog"})
.await;
// paste in visual mode
cx.simulate_shared_keystrokes(["w", "v", "i", "w", "p"])
.await;
cx.assert_shared_state(indoc! {"
The quick brown
fox jumps jumpˇs
the lazy dog"})
.await;
cx.assert_shared_clipboard("over").await;
// paste in visual line mode
cx.simulate_shared_keystrokes(["up", "shift-v", "shift-p"])
.await;
cx.assert_shared_state(indoc! {"
ˇover
fox jumps jumps
the lazy dog"})
.await;
cx.assert_shared_clipboard("over").await;
// paste in visual block mode
cx.simulate_shared_keystrokes(["ctrl-v", "down", "down", "p"])
.await;
cx.assert_shared_state(indoc! {"
oveˇrver
overox jumps jumps
overhe lazy dog"})
.await;
// copy in visual line mode
cx.set_shared_state(indoc! {"
The quick brown
fox juˇmps over
the lazy dog"})
.await;
cx.simulate_shared_keystrokes(["shift-v", "d"]).await;
cx.assert_shared_state(indoc! {"
The quick brown
the laˇzy dog"})
.await;
// paste in visual mode
cx.simulate_shared_keystrokes(["v", "i", "w", "p"]).await;
cx.assert_shared_state(
&indoc! {"
The quick brown
the_
ˇfox jumps over
_dog"}
.replace("_", " "), // Hack for trailing whitespace
)
.await;
cx.assert_shared_clipboard("lazy").await;
cx.set_shared_state(indoc! {"
The quick brown
fox juˇmps over
the lazy dog"})
.await;
cx.simulate_shared_keystrokes(["shift-v", "d"]).await;
cx.assert_shared_state(indoc! {"
The quick brown
the laˇzy dog"})
.await;
// paste in visual line mode
cx.simulate_shared_keystrokes(["k", "shift-v", "p"]).await;
cx.assert_shared_state(indoc! {"
ˇfox jumps over
the lazy dog"})
.await;
cx.assert_shared_clipboard("The quick brown\n").await;
}
#[gpui::test]
async fn test_paste_visual_block(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
// copy in visual block mode
cx.set_shared_state(indoc! {"
The ˇquick brown
fox jumps over
the lazy dog"})
.await;
cx.simulate_shared_keystrokes(["ctrl-v", "2", "j", "y"])
.await;
cx.assert_shared_clipboard("q\nj\nl").await;
cx.simulate_shared_keystrokes(["p"]).await;
cx.assert_shared_state(indoc! {"
The qˇquick brown
fox jjumps over
the llazy dog"})
.await;
cx.simulate_shared_keystrokes(["v", "i", "w", "shift-p"])
.await;
cx.assert_shared_state(indoc! {"
The ˇq brown
fox jjjumps over
the lllazy dog"})
.await;
cx.simulate_shared_keystrokes(["v", "i", "w", "shift-p"])
.await;
cx.set_shared_state(indoc! {"
The ˇquick brown
fox jumps over
the lazy dog"})
.await;
cx.simulate_shared_keystrokes(["ctrl-v", "j", "y"]).await;
cx.assert_shared_clipboard("q\nj").await;
cx.simulate_shared_keystrokes(["l", "ctrl-v", "2", "j", "shift-p"])
.await;
cx.assert_shared_state(indoc! {"
The qˇqick brown
fox jjmps over
the lzy dog"})
.await;
cx.simulate_shared_keystrokes(["shift-v", "p"]).await;
cx.assert_shared_state(indoc! {"
ˇq
j
fox jjmps over
the lzy dog"})
.await;
}
#[gpui::test]
async fn test_paste_indent(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestContext::new_typescript(cx).await;
cx.set_state(
indoc! {"
class A {ˇ
}
"},
Mode::Normal,
);
cx.simulate_keystrokes(["o", "a", "(", ")", "{", "escape"]);
cx.assert_state(
indoc! {"
class A {
a()ˇ{}
}
"},
Mode::Normal,
);
// cursor goes to the first non-blank character in the line;
cx.simulate_keystrokes(["y", "y", "p"]);
cx.assert_state(
indoc! {"
class A {
a(){}
ˇa(){}
}
"},
Mode::Normal,
);
// indentation is preserved when pasting
cx.simulate_keystrokes(["u", "shift-v", "up", "y", "shift-p"]);
cx.assert_state(
indoc! {"
ˇclass A {
a(){}
class A {
a(){}
}
"},
Mode::Normal,
);
}
}

View file

@ -1,7 +1,9 @@
use std::cmp::Ordering;
use crate::Vim;
use editor::{display_map::ToDisplayPoint, scroll::scroll_amount::ScrollAmount, Editor};
use editor::{
display_map::ToDisplayPoint,
scroll::{scroll_amount::ScrollAmount, VERTICAL_SCROLL_MARGIN},
DisplayPoint, Editor,
};
use gpui::{actions, AppContext, ViewContext};
use language::Bias;
use workspace::Workspace;
@ -53,13 +55,9 @@ fn scroll(cx: &mut ViewContext<Workspace>, by: fn(c: Option<f32>) -> ScrollAmoun
fn scroll_editor(editor: &mut Editor, amount: &ScrollAmount, cx: &mut ViewContext<Editor>) {
let should_move_cursor = editor.newest_selection_on_screen(cx).is_eq();
editor.scroll_screen(amount, cx);
if should_move_cursor {
let selection_ordering = editor.newest_selection_on_screen(cx);
if selection_ordering.is_eq() {
return;
}
let visible_rows = if let Some(visible_rows) = editor.visible_line_count() {
visible_rows as u32
} else {
@ -69,21 +67,19 @@ fn scroll_editor(editor: &mut Editor, amount: &ScrollAmount, cx: &mut ViewContex
let top_anchor = editor.scroll_manager.anchor().anchor;
editor.change_selections(None, cx, |s| {
s.replace_cursors_with(|snapshot| {
let mut new_point = top_anchor.to_display_point(&snapshot);
s.move_heads_with(|map, head, goal| {
let top = top_anchor.to_display_point(map);
let min_row = top.row() + VERTICAL_SCROLL_MARGIN as u32;
let max_row = top.row() + visible_rows - VERTICAL_SCROLL_MARGIN as u32 - 1;
match selection_ordering {
Ordering::Less => {
new_point = snapshot.clip_point(new_point, Bias::Right);
}
Ordering::Greater => {
*new_point.row_mut() += visible_rows - 1;
new_point = snapshot.clip_point(new_point, Bias::Left);
}
Ordering::Equal => unreachable!(),
}
vec![new_point]
let new_head = if head.row() < min_row {
map.clip_point(DisplayPoint::new(min_row, head.column()), Bias::Left)
} else if head.row() > max_row {
map.clip_point(DisplayPoint::new(max_row, head.column()), Bias::Left)
} else {
head
};
(new_head, goal)
})
});
}

View file

@ -1,5 +1,5 @@
use gpui::{actions, impl_actions, AppContext, ViewContext};
use search::{buffer_search, BufferSearchBar, SearchOptions};
use search::{buffer_search, BufferSearchBar, SearchMode, SearchOptions};
use serde_derive::Deserialize;
use workspace::{searchable::Direction, Pane, Workspace};
@ -65,15 +65,13 @@ fn search(workspace: &mut Workspace, action: &Search, cx: &mut ViewContext<Works
cx.focus_self();
if query.is_empty() {
search_bar.set_search_options(
SearchOptions::CASE_SENSITIVE | SearchOptions::REGEX,
cx,
);
search_bar.set_search_options(SearchOptions::CASE_SENSITIVE, cx);
search_bar.activate_search_mode(SearchMode::Regex, cx);
}
vim.state.search = SearchState {
vim.workspace_state.search = SearchState {
direction,
count,
initial_query: query,
initial_query: query.clone(),
};
});
}
@ -83,7 +81,7 @@ fn search(workspace: &mut Workspace, action: &Search, cx: &mut ViewContext<Works
// hook into the existing to clear out any vim search state on cmd+f or edit -> find.
fn search_deploy(_: &mut Pane, _: &buffer_search::Deploy, cx: &mut ViewContext<Pane>) {
Vim::update(cx, |vim, _| vim.state.search = Default::default());
Vim::update(cx, |vim, _| vim.workspace_state.search = Default::default());
cx.propagate_action();
}
@ -93,8 +91,9 @@ fn search_submit(workspace: &mut Workspace, _: &SearchSubmit, cx: &mut ViewConte
pane.update(cx, |pane, cx| {
if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
search_bar.update(cx, |search_bar, cx| {
let state = &mut vim.state.search;
let state = &mut vim.workspace_state.search;
let mut count = state.count;
let direction = state.direction;
// in the case that the query has changed, the search bar
// will have selected the next match already.
@ -103,8 +102,8 @@ fn search_submit(workspace: &mut Workspace, _: &SearchSubmit, cx: &mut ViewConte
{
count = count.saturating_sub(1)
}
search_bar.select_match(state.direction, count, cx);
state.count = 1;
search_bar.select_match(direction, count, cx);
search_bar.focus_editor(&Default::default(), cx);
});
}

View file

@ -4,9 +4,9 @@ use language::Point;
use crate::{motion::Motion, utils::copy_selections_content, Mode, Vim};
pub fn substitute(vim: &mut Vim, count: Option<usize>, cx: &mut WindowContext) {
let line_mode = vim.state.mode == Mode::Visual { line: true };
vim.switch_mode(Mode::Insert, true, cx);
let line_mode = vim.state().mode == Mode::VisualLine;
vim.update_active_editor(cx, |editor, cx| {
editor.set_clip_at_line_ends(false, cx);
editor.transact(cx, |editor, cx| {
editor.change_selections(None, cx, |s| {
s.move_with(|map, selection| {
@ -15,7 +15,10 @@ pub fn substitute(vim: &mut Vim, count: Option<usize>, cx: &mut WindowContext) {
}
if line_mode {
Motion::CurrentLine.expand_selection(map, selection, None, false);
if let Some((point, _)) = Motion::FirstNonWhitespace.move_point(
if let Some((point, _)) = (Motion::FirstNonWhitespace {
display_lines: false,
})
.move_point(
map,
selection.start,
selection.goal,
@ -32,6 +35,7 @@ pub fn substitute(vim: &mut Vim, count: Option<usize>, cx: &mut WindowContext) {
editor.edit(edits, cx);
});
});
vim.switch_mode(Mode::Insert, true, cx);
}
#[cfg(test)]
@ -52,7 +56,7 @@ mod test {
cx.assert_editor_state("xˇbc\n");
// supports a selection
cx.set_state(indoc! {"a«bcˇ»\n"}, Mode::Visual { line: false });
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");

View file

@ -62,9 +62,9 @@ pub fn init(cx: &mut AppContext) {
}
fn object(object: Object, cx: &mut WindowContext) {
match Vim::read(cx).state.mode {
match Vim::read(cx).state().mode {
Mode::Normal => normal_object(object, cx),
Mode::Visual { .. } => visual_object(object, cx),
Mode::Visual | Mode::VisualLine | Mode::VisualBlock => visual_object(object, cx),
Mode::Insert => {
// Shouldn't execute a text object in insert mode. Ignoring
}
@ -72,6 +72,47 @@ fn object(object: Object, cx: &mut WindowContext) {
}
impl Object {
pub fn is_multiline(self) -> bool {
match self {
Object::Word { .. } | Object::Quotes | Object::BackQuotes | Object::DoubleQuotes => {
false
}
Object::Sentence
| Object::Parentheses
| Object::AngleBrackets
| Object::CurlyBrackets
| Object::SquareBrackets => true,
}
}
pub fn always_expands_both_ways(self) -> bool {
match self {
Object::Word { .. } | Object::Sentence => false,
Object::Quotes
| Object::BackQuotes
| Object::DoubleQuotes
| Object::Parentheses
| Object::SquareBrackets
| Object::CurlyBrackets
| Object::AngleBrackets => true,
}
}
pub fn target_visual_mode(self, current_mode: Mode) -> Mode {
match self {
Object::Word { .. } if current_mode == Mode::VisualLine => Mode::Visual,
Object::Word { .. } => current_mode,
Object::Sentence
| Object::Quotes
| Object::BackQuotes
| Object::DoubleQuotes
| Object::Parentheses
| Object::SquareBrackets
| Object::CurlyBrackets
| Object::AngleBrackets => Mode::Visual,
}
}
pub fn range(
self,
map: &DisplaySnapshot,
@ -87,13 +128,27 @@ impl Object {
}
}
Object::Sentence => sentence(map, relative_to, around),
Object::Quotes => surrounding_markers(map, relative_to, around, false, '\'', '\''),
Object::BackQuotes => surrounding_markers(map, relative_to, around, false, '`', '`'),
Object::DoubleQuotes => surrounding_markers(map, relative_to, around, false, '"', '"'),
Object::Parentheses => surrounding_markers(map, relative_to, around, true, '(', ')'),
Object::SquareBrackets => surrounding_markers(map, relative_to, around, true, '[', ']'),
Object::CurlyBrackets => surrounding_markers(map, relative_to, around, true, '{', '}'),
Object::AngleBrackets => surrounding_markers(map, relative_to, around, true, '<', '>'),
Object::Quotes => {
surrounding_markers(map, relative_to, around, self.is_multiline(), '\'', '\'')
}
Object::BackQuotes => {
surrounding_markers(map, relative_to, around, self.is_multiline(), '`', '`')
}
Object::DoubleQuotes => {
surrounding_markers(map, relative_to, around, self.is_multiline(), '"', '"')
}
Object::Parentheses => {
surrounding_markers(map, relative_to, around, self.is_multiline(), '(', ')')
}
Object::SquareBrackets => {
surrounding_markers(map, relative_to, around, self.is_multiline(), '[', ']')
}
Object::CurlyBrackets => {
surrounding_markers(map, relative_to, around, self.is_multiline(), '{', '}')
}
Object::AngleBrackets => {
surrounding_markers(map, relative_to, around, self.is_multiline(), '<', '>')
}
}
}

View file

@ -9,14 +9,16 @@ use crate::motion::Motion;
pub enum Mode {
Normal,
Insert,
Visual { line: bool },
Visual,
VisualLine,
VisualBlock,
}
impl Mode {
pub fn is_visual(&self) -> bool {
match self {
Mode::Normal | Mode::Insert => false,
Mode::Visual { .. } => true,
Mode::Visual | Mode::VisualLine | Mode::VisualBlock => true,
}
}
}
@ -39,15 +41,20 @@ pub enum Operator {
FindBackward { after: bool },
}
#[derive(Default)]
pub struct VimState {
#[derive(Default, Clone)]
pub struct EditorState {
pub mode: Mode,
pub last_mode: Mode,
pub operator_stack: Vec<Operator>,
pub search: SearchState,
}
#[derive(Default, Clone)]
pub struct WorkspaceState {
pub search: SearchState,
pub last_find: Option<Motion>,
}
#[derive(Clone)]
pub struct SearchState {
pub direction: Direction,
pub count: usize,
@ -64,7 +71,7 @@ impl Default for SearchState {
}
}
impl VimState {
impl EditorState {
pub fn cursor_shape(&self) -> CursorShape {
match self.mode {
Mode::Normal => {
@ -74,7 +81,7 @@ impl VimState {
CursorShape::Underscore
}
}
Mode::Visual { .. } => CursorShape::Block,
Mode::Visual | Mode::VisualLine | Mode::VisualBlock => CursorShape::Block,
Mode::Insert => CursorShape::Bar,
}
}
@ -87,9 +94,13 @@ impl VimState {
)
}
pub fn should_autoindent(&self) -> bool {
!(self.mode == Mode::Insert && self.last_mode == Mode::VisualBlock)
}
pub fn clip_at_line_ends(&self) -> bool {
match self.mode {
Mode::Insert | Mode::Visual { .. } => false,
Mode::Insert | Mode::Visual | Mode::VisualLine | Mode::VisualBlock => false,
Mode::Normal => true,
}
}
@ -101,7 +112,7 @@ impl VimState {
"vim_mode",
match self.mode {
Mode::Normal => "normal",
Mode::Visual { .. } => "visual",
Mode::Visual | Mode::VisualLine | Mode::VisualBlock => "visual",
Mode::Insert => "insert",
},
);

View file

@ -1,7 +1,6 @@
mod neovim_backed_binding_test_context;
mod neovim_backed_test_context;
mod neovim_connection;
mod vim_binding_test_context;
mod vim_test_context;
use std::sync::Arc;
@ -10,7 +9,6 @@ use command_palette::CommandPalette;
use editor::DisplayPoint;
pub use neovim_backed_binding_test_context::*;
pub use neovim_backed_test_context::*;
pub use vim_binding_test_context::*;
pub use vim_test_context::*;
use indoc::indoc;
@ -241,7 +239,7 @@ async fn test_status_indicator(
deterministic.run_until_parked();
assert_eq!(
cx.workspace(|_, cx| mode_indicator.read(cx).mode),
Some(Mode::Visual { line: false })
Some(Mode::Visual)
);
// hides if vim mode is disabled
@ -261,3 +259,244 @@ async fn test_status_indicator(
assert!(mode_indicator.read(cx).mode.is_some());
});
}
#[gpui::test]
async fn test_word_characters(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestContext::new_typescript(cx).await;
cx.set_state(
indoc! { "
class A {
#ˇgoop = 99;
$ˇgoop () { return this.#gˇoop };
};
console.log(new A().$gooˇp())
"},
Mode::Normal,
);
cx.simulate_keystrokes(["v", "i", "w"]);
cx.assert_state(
indoc! {"
class A {
«#goopˇ» = 99;
«$goopˇ» () { return this.«#goopˇ» };
};
console.log(new A().«$goopˇ»())
"},
Mode::Visual,
)
}
#[gpui::test]
async fn test_wrapped_lines(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
cx.set_shared_wrap(12).await;
// tests line wrap as follows:
// 1: twelve char
// twelve char
// 2: twelve char
cx.set_shared_state(indoc! { "
tˇwelve char twelve char
twelve char
"})
.await;
cx.simulate_shared_keystrokes(["j"]).await;
cx.assert_shared_state(indoc! { "
twelve char twelve char
tˇwelve char
"})
.await;
cx.simulate_shared_keystrokes(["k"]).await;
cx.assert_shared_state(indoc! { "
tˇwelve char twelve char
twelve char
"})
.await;
cx.simulate_shared_keystrokes(["g", "j"]).await;
cx.assert_shared_state(indoc! { "
twelve char tˇwelve char
twelve char
"})
.await;
cx.simulate_shared_keystrokes(["g", "j"]).await;
cx.assert_shared_state(indoc! { "
twelve char twelve char
tˇwelve char
"})
.await;
cx.simulate_shared_keystrokes(["g", "k"]).await;
cx.assert_shared_state(indoc! { "
twelve char tˇwelve char
twelve char
"})
.await;
cx.simulate_shared_keystrokes(["g", "^"]).await;
cx.assert_shared_state(indoc! { "
twelve char ˇtwelve char
twelve char
"})
.await;
cx.simulate_shared_keystrokes(["^"]).await;
cx.assert_shared_state(indoc! { "
ˇtwelve char twelve char
twelve char
"})
.await;
cx.simulate_shared_keystrokes(["g", "$"]).await;
cx.assert_shared_state(indoc! { "
twelve charˇ twelve char
twelve char
"})
.await;
cx.simulate_shared_keystrokes(["$"]).await;
cx.assert_shared_state(indoc! { "
twelve char twelve chaˇr
twelve char
"})
.await;
cx.set_shared_state(indoc! { "
tˇwelve char twelve char
twelve char
"})
.await;
cx.simulate_shared_keystrokes(["enter"]).await;
cx.assert_shared_state(indoc! { "
twelve char twelve char
ˇtwelve char
"})
.await;
cx.set_shared_state(indoc! { "
twelve char
tˇwelve char twelve char
twelve char
"})
.await;
cx.simulate_shared_keystrokes(["o", "o", "escape"]).await;
cx.assert_shared_state(indoc! { "
twelve char
twelve char twelve char
ˇo
twelve char
"})
.await;
cx.set_shared_state(indoc! { "
twelve char
tˇwelve char twelve char
twelve char
"})
.await;
cx.simulate_shared_keystrokes(["shift-a", "a", "escape"])
.await;
cx.assert_shared_state(indoc! { "
twelve char
twelve char twelve charˇa
twelve char
"})
.await;
cx.simulate_shared_keystrokes(["shift-i", "i", "escape"])
.await;
cx.assert_shared_state(indoc! { "
twelve char
ˇitwelve char twelve chara
twelve char
"})
.await;
cx.simulate_shared_keystrokes(["shift-d"]).await;
cx.assert_shared_state(indoc! { "
twelve char
ˇ
twelve char
"})
.await;
cx.set_shared_state(indoc! { "
twelve char
twelve char tˇwelve char
twelve char
"})
.await;
cx.simulate_shared_keystrokes(["shift-o", "o", "escape"])
.await;
cx.assert_shared_state(indoc! { "
twelve char
ˇo
twelve char twelve char
twelve char
"})
.await;
}
#[gpui::test]
async fn test_folds(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
cx.set_neovim_option("foldmethod=manual").await;
cx.set_shared_state(indoc! { "
fn boop() {
ˇbarp()
bazp()
}
"})
.await;
cx.simulate_shared_keystrokes(["shift-v", "j", "z", "f"])
.await;
// visual display is now:
// fn boop () {
// [FOLDED]
// }
// TODO: this should not be needed but currently zf does not
// return to normal mode.
cx.simulate_shared_keystrokes(["escape"]).await;
// skip over fold downward
cx.simulate_shared_keystrokes(["g", "g"]).await;
cx.assert_shared_state(indoc! { "
ˇfn boop() {
barp()
bazp()
}
"})
.await;
cx.simulate_shared_keystrokes(["j", "j"]).await;
cx.assert_shared_state(indoc! { "
fn boop() {
barp()
bazp()
ˇ}
"})
.await;
// skip over fold upward
cx.simulate_shared_keystrokes(["2", "k"]).await;
cx.assert_shared_state(indoc! { "
ˇfn boop() {
barp()
bazp()
}
"})
.await;
// yank the fold
cx.simulate_shared_keystrokes(["down", "y", "y"]).await;
cx.assert_shared_clipboard(" barp()\n bazp()\n").await;
// re-open
cx.simulate_shared_keystrokes(["z", "o"]).await;
cx.assert_shared_state(indoc! { "
fn boop() {
ˇ barp()
bazp()
}
"})
.await;
}

View file

@ -1,9 +1,13 @@
use indoc::indoc;
use settings::SettingsStore;
use std::ops::{Deref, DerefMut, Range};
use collections::{HashMap, HashSet};
use gpui::ContextHandle;
use language::OffsetRangeExt;
use language::{
language_settings::{AllLanguageSettings, SoftWrap},
OffsetRangeExt,
};
use util::test::{generate_marked_text, marked_text_offsets};
use super::{neovim_connection::NeovimConnection, NeovimBackedBindingTestContext, VimTestContext};
@ -116,7 +120,7 @@ impl<'a> NeovimBackedTestContext<'a> {
pub async fn set_shared_state(&mut self, marked_text: &str) -> ContextHandle {
let mode = if marked_text.contains("»") {
Mode::Visual { line: false }
Mode::Visual
} else {
Mode::Normal
};
@ -127,16 +131,46 @@ impl<'a> NeovimBackedTestContext<'a> {
context_handle
}
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("columns=12").await;
self.update(|cx| {
cx.update_global(|settings: &mut SettingsStore, cx| {
settings.update_user_settings::<AllLanguageSettings>(cx, |settings| {
settings.defaults.soft_wrap = Some(SoftWrap::PreferredLineLength);
settings.defaults.preferred_line_length = Some(columns);
});
})
})
}
pub async fn set_neovim_option(&mut self, option: &str) {
self.neovim.set_option(option).await;
}
pub async fn assert_shared_state(&mut self, marked_text: &str) {
let neovim = self.neovim_state().await;
if neovim != marked_text {
let initial_state = self
.last_set_state
.as_ref()
.unwrap_or(&"N/A".to_string())
.clone();
panic!(
indoc! {"Test is incorrect (currently expected != neovim state)
let editor = self.editor_state();
if neovim == marked_text && neovim == editor {
return;
}
let initial_state = self
.last_set_state
.as_ref()
.unwrap_or(&"N/A".to_string())
.clone();
let message = if neovim != marked_text {
"Test is incorrect (currently expected != neovim_state)"
} else {
"Editor does not match nvim behaviour"
};
panic!(
indoc! {"{}
# initial state:
{}
# keystrokes:
@ -147,20 +181,65 @@ impl<'a> NeovimBackedTestContext<'a> {
{}
# zed state:
{}"},
initial_state,
self.recent_keystrokes.join(" "),
marked_text,
neovim,
self.editor_state(),
)
message,
initial_state,
self.recent_keystrokes.join(" "),
marked_text,
neovim,
editor
)
}
pub async fn assert_shared_clipboard(&mut self, text: &str) {
let neovim = self.neovim.read_register('"').await;
let editor = self
.platform()
.read_from_clipboard()
.unwrap()
.text()
.clone();
if text == neovim && text == editor {
return;
}
self.assert_editor_state(marked_text)
let message = if neovim != text {
"Test is incorrect (currently expected != neovim)"
} else {
"Editor does not match nvim behaviour"
};
let initial_state = self
.last_set_state
.as_ref()
.unwrap_or(&"N/A".to_string())
.clone();
panic!(
indoc! {"{}
# initial state:
{}
# keystrokes:
{}
# currently expected:
{}
# neovim clipboard:
{}
# zed clipboard:
{}"},
message,
initial_state,
self.recent_keystrokes.join(" "),
text,
neovim,
editor
)
}
pub async fn neovim_state(&mut self) -> String {
generate_marked_text(
self.neovim.text().await.as_str(),
&vec![self.neovim_selection().await],
&self.neovim_selections().await[..],
true,
)
}
@ -169,9 +248,12 @@ impl<'a> NeovimBackedTestContext<'a> {
self.neovim.mode().await.unwrap()
}
async fn neovim_selection(&mut self) -> Range<usize> {
let neovim_selection = self.neovim.selection().await;
neovim_selection.to_offset(&self.buffer_snapshot())
async fn neovim_selections(&mut self) -> Vec<Range<usize>> {
let neovim_selections = self.neovim.selections().await;
neovim_selections
.into_iter()
.map(|selection| selection.to_offset(&self.buffer_snapshot()))
.collect()
}
pub async fn assert_state_matches(&mut self) {

View file

@ -1,5 +1,8 @@
#[cfg(feature = "neovim")]
use std::ops::{Deref, DerefMut};
use std::{
cmp,
ops::{Deref, DerefMut},
};
use std::{ops::Range, path::PathBuf};
#[cfg(feature = "neovim")]
@ -37,6 +40,8 @@ pub enum NeovimData {
Put { state: String },
Key(String),
Get { state: String, mode: Option<Mode> },
ReadRegister { name: char, value: String },
SetOption { value: String },
}
pub struct NeovimConnection {
@ -135,7 +140,7 @@ impl NeovimConnection {
#[cfg(feature = "neovim")]
pub async fn set_state(&mut self, marked_text: &str) {
let (text, selection) = parse_state(&marked_text);
let (text, selections) = parse_state(&marked_text);
let nvim_buffer = self
.nvim
@ -167,6 +172,11 @@ impl NeovimConnection {
.await
.expect("Could not get neovim window");
if selections.len() != 1 {
panic!("must have one selection");
}
let selection = &selections[0];
let cursor = selection.start;
nvim_window
.set_cursor((cursor.row as i64 + 1, cursor.column as i64))
@ -213,6 +223,59 @@ impl NeovimConnection {
);
}
#[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) {
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(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() {
if 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
@ -224,7 +287,7 @@ impl NeovimConnection {
}
#[cfg(feature = "neovim")]
pub async fn state(&mut self) -> (Option<Mode>, String, Range<Point>) {
pub async fn state(&mut self) -> (Option<Mode>, String, Vec<Range<Point>>) {
let nvim_buffer = self
.nvim
.get_current_buf()
@ -261,16 +324,51 @@ impl NeovimConnection {
let mode = match nvim_mode_text.as_ref() {
"i" => Some(Mode::Insert),
"n" => Some(Mode::Normal),
"v" => Some(Mode::Visual { line: false }),
"V" => Some(Mode::Visual { line: true }),
"v" => Some(Mode::Visual),
"V" => Some(Mode::VisualLine),
"\x16" => Some(Mode::VisualBlock),
_ => None,
};
let mut selections = Vec::new();
// Vim uses the index of the first and last character in the selection
// Zed uses the index of the positions between the characters, so we need
// to add one to the end in visual mode.
match mode {
Some(Mode::Visual { .. }) => {
Some(Mode::VisualBlock) if selection_row != cursor_row => {
// in zed we fake a block selecrtion by using multiple cursors (one per line)
// this code emulates that.
// to deal with casees where the selection is not perfectly rectangular we extract
// the content of the selection via the "a register to get the shape correctly.
self.nvim.input("\"aygv").await.unwrap();
let content = self.nvim.command_output("echo getreg('a')").await.unwrap();
let lines = content.split("\n").collect::<Vec<_>>();
let top = cmp::min(selection_row, cursor_row);
let left = cmp::min(selection_col, cursor_col);
for row in top..=cmp::max(selection_row, cursor_row) {
let content = if row - top >= lines.len() as u32 {
""
} else {
lines[(row - top) as usize]
};
let line_len = self
.read_position(format!("echo strlen(getline({}))", row + 1).as_str())
.await;
if left > line_len {
continue;
}
let start = Point::new(row, left);
let end = Point::new(row, left + content.len() as u32);
if cursor_col >= selection_col {
selections.push(start..end)
} else {
selections.push(end..start)
}
}
}
Some(Mode::Visual) | Some(Mode::VisualLine) | Some(Mode::VisualBlock) => {
if selection_col > cursor_col {
let selection_line_length =
self.read_position("echo strlen(getline(line('v')))").await;
@ -290,38 +388,37 @@ impl NeovimConnection {
cursor_row += 1;
}
}
selections.push(
Point::new(selection_row, selection_col)..Point::new(cursor_row, cursor_col),
)
}
Some(Mode::Insert) | Some(Mode::Normal) | None => {}
Some(Mode::Insert) | Some(Mode::Normal) | None => selections
.push(Point::new(selection_row, selection_col)..Point::new(cursor_row, cursor_col)),
}
let (start, end) = (
Point::new(selection_row, selection_col),
Point::new(cursor_row, cursor_col),
);
let state = NeovimData::Get {
mode,
state: encode_range(&text, start..end),
state: encode_ranges(&text, &selections),
};
if self.data.back() != Some(&state) {
self.data.push_back(state.clone());
}
(mode, text, start..end)
(mode, text, selections)
}
#[cfg(not(feature = "neovim"))]
pub async fn state(&mut self) -> (Option<Mode>, String, Range<Point>) {
pub async fn state(&mut self) -> (Option<Mode>, String, Vec<Range<Point>>) {
if let Some(NeovimData::Get { state: text, mode }) = self.data.front() {
let (text, range) = parse_state(text);
(*mode, text, range)
let (text, ranges) = parse_state(text);
(*mode, text, ranges)
} else {
panic!("operation does not match recorded script. re-record with --features=neovim");
}
}
pub async fn selection(&mut self) -> Range<Point> {
pub async fn selections(&mut self) -> Vec<Range<Point>> {
self.state().await.2
}
@ -421,51 +518,62 @@ impl Handler for NvimHandler {
}
}
fn parse_state(marked_text: &str) -> (String, Range<Point>) {
fn parse_state(marked_text: &str) -> (String, Vec<Range<Point>>) {
let (text, ranges) = util::test::marked_text_ranges(marked_text, true);
let byte_range = ranges[0].clone();
let mut point_range = Point::zero()..Point::zero();
let mut ix = 0;
let mut position = Point::zero();
for c in text.chars().chain(['\0']) {
if ix == byte_range.start {
point_range.start = position;
}
if ix == byte_range.end {
point_range.end = position;
}
let len_utf8 = c.len_utf8();
ix += len_utf8;
if c == '\n' {
position.row += 1;
position.column = 0;
} else {
position.column += len_utf8 as u32;
}
}
(text, point_range)
let point_ranges = ranges
.into_iter()
.map(|byte_range| {
let mut point_range = Point::zero()..Point::zero();
let mut ix = 0;
let mut position = Point::zero();
for c in text.chars().chain(['\0']) {
if ix == byte_range.start {
point_range.start = position;
}
if ix == byte_range.end {
point_range.end = position;
}
let len_utf8 = c.len_utf8();
ix += len_utf8;
if c == '\n' {
position.row += 1;
position.column = 0;
} else {
position.column += len_utf8 as u32;
}
}
point_range
})
.collect::<Vec<_>>();
(text, point_ranges)
}
#[cfg(feature = "neovim")]
fn encode_range(text: &str, range: Range<Point>) -> String {
let mut byte_range = 0..0;
let mut ix = 0;
let mut position = Point::zero();
for c in text.chars().chain(['\0']) {
if position == range.start {
byte_range.start = ix;
}
if position == range.end {
byte_range.end = ix;
}
let len_utf8 = c.len_utf8();
ix += len_utf8;
if c == '\n' {
position.row += 1;
position.column = 0;
} else {
position.column += len_utf8 as u32;
}
}
util::test::generate_marked_text(text, &[byte_range], true)
fn encode_ranges(text: &str, point_ranges: &Vec<Range<Point>>) -> String {
let byte_ranges = point_ranges
.into_iter()
.map(|range| {
let mut byte_range = 0..0;
let mut ix = 0;
let mut position = Point::zero();
for c in text.chars().chain(['\0']) {
if position == range.start {
byte_range.start = ix;
}
if position == range.end {
byte_range.end = ix;
}
let len_utf8 = c.len_utf8();
ix += len_utf8;
if c == '\n' {
position.row += 1;
position.column = 0;
} else {
position.column += len_utf8 as u32;
}
}
byte_range
})
.collect::<Vec<_>>();
util::test::generate_marked_text(text, &byte_ranges[..], true)
}

View file

@ -1,64 +0,0 @@
use std::ops::{Deref, DerefMut};
use crate::*;
use super::VimTestContext;
pub struct VimBindingTestContext<'a, const COUNT: usize> {
cx: VimTestContext<'a>,
keystrokes_under_test: [&'static str; COUNT],
mode_before: Mode,
mode_after: Mode,
}
impl<'a, const COUNT: usize> VimBindingTestContext<'a, COUNT> {
pub fn new(
keystrokes_under_test: [&'static str; COUNT],
mode_before: Mode,
mode_after: Mode,
cx: VimTestContext<'a>,
) -> Self {
Self {
cx,
keystrokes_under_test,
mode_before,
mode_after,
}
}
pub fn binding<const NEW_COUNT: usize>(
self,
keystrokes_under_test: [&'static str; NEW_COUNT],
) -> VimBindingTestContext<'a, NEW_COUNT> {
VimBindingTestContext {
keystrokes_under_test,
cx: self.cx,
mode_before: self.mode_before,
mode_after: self.mode_after,
}
}
pub fn assert(&mut self, initial_state: &str, state_after: &str) {
self.cx.assert_binding(
self.keystrokes_under_test,
initial_state,
self.mode_before,
state_after,
self.mode_after,
)
}
}
impl<'a, const COUNT: usize> Deref for VimBindingTestContext<'a, COUNT> {
type Target = VimTestContext<'a>;
fn deref(&self) -> &Self::Target {
&self.cx
}
}
impl<'a, const COUNT: usize> DerefMut for VimBindingTestContext<'a, COUNT> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.cx
}
}

View file

@ -8,16 +8,24 @@ use search::{BufferSearchBar, ProjectSearchBar};
use crate::{state::Operator, *};
use super::VimBindingTestContext;
pub struct VimTestContext<'a> {
cx: EditorLspTestContext<'a>,
}
impl<'a> VimTestContext<'a> {
pub async fn new(cx: &'a mut gpui::TestAppContext, enabled: bool) -> VimTestContext<'a> {
let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
let lsp = EditorLspTestContext::new_rust(Default::default(), cx).await;
Self::new_with_lsp(lsp, enabled)
}
pub async fn new_typescript(cx: &'a mut gpui::TestAppContext) -> VimTestContext<'a> {
Self::new_with_lsp(
EditorLspTestContext::new_typescript(Default::default(), cx).await,
true,
)
}
pub fn new_with_lsp(mut cx: EditorLspTestContext<'a>, enabled: bool) -> VimTestContext<'a> {
cx.update(|cx| {
search::init(cx);
crate::init(cx);
@ -76,12 +84,12 @@ impl<'a> VimTestContext<'a> {
}
pub fn mode(&mut self) -> Mode {
self.cx.read(|cx| cx.global::<Vim>().state.mode)
self.cx.read(|cx| cx.global::<Vim>().state().mode)
}
pub fn active_operator(&mut self) -> Option<Operator> {
self.cx
.read(|cx| cx.global::<Vim>().state.operator_stack.last().copied())
.read(|cx| cx.global::<Vim>().state().operator_stack.last().copied())
}
pub fn set_state(&mut self, text: &str, mode: Mode) -> ContextHandle {
@ -92,6 +100,7 @@ impl<'a> VimTestContext<'a> {
vim.switch_mode(mode, true, cx);
})
});
self.cx.foreground().run_until_parked();
context_handle
}
@ -115,14 +124,6 @@ impl<'a> VimTestContext<'a> {
assert_eq!(self.mode(), mode_after, "{}", self.assertion_context());
assert_eq!(self.active_operator(), None, "{}", self.assertion_context());
}
pub fn binding<const COUNT: usize>(
mut self,
keystrokes: [&'static str; COUNT],
) -> VimBindingTestContext<'a, COUNT> {
let mode = self.mode();
VimBindingTestContext::new(keystrokes, mode, mode, self)
}
}
impl<'a> Deref for VimTestContext<'a> {

View file

@ -1,5 +1,6 @@
use editor::{ClipboardSelection, Editor};
use gpui::{AppContext, ClipboardItem};
use language::Point;
pub fn copy_selections_content(editor: &mut Editor, linewise: bool, cx: &mut AppContext) {
let selections = editor.selections.all_adjusted(cx);
@ -7,13 +8,35 @@ pub fn copy_selections_content(editor: &mut Editor, linewise: bool, cx: &mut App
let mut text = String::new();
let mut clipboard_selections = Vec::with_capacity(selections.len());
{
let mut is_first = true;
for selection in selections.iter() {
let initial_len = text.len();
let start = selection.start;
let mut start = selection.start;
let end = selection.end;
if is_first {
is_first = false;
} else {
text.push_str("\n");
}
let initial_len = text.len();
// if the file does not end with \n, and our line-mode selection ends on
// that line, we will have expanded the start of the selection to ensure it
// contains a newline (so that delete works as expected). We undo that change
// here.
let is_last_line = linewise
&& end.row == buffer.max_buffer_row()
&& buffer.max_point().column > 0
&& start == Point::new(start.row, buffer.line_len(start.row));
if is_last_line {
start = Point::new(buffer.max_buffer_row(), 0);
}
for chunk in buffer.text_for_range(start..end) {
text.push_str(chunk);
}
if is_last_line {
text.push_str("\n");
}
clipboard_selections.push(ClipboardSelection {
len: text.len() - initial_len,
is_entire_line: linewise,

View file

@ -12,21 +12,21 @@ mod utils;
mod visual;
use anyhow::Result;
use collections::CommandPaletteFilter;
use collections::{CommandPaletteFilter, HashMap};
use editor::{movement, Editor, EditorMode, Event};
use gpui::{
actions, impl_actions, keymap_matcher::KeymapContext, keymap_matcher::MatchResult, AppContext,
Subscription, ViewContext, ViewHandle, WeakViewHandle, WindowContext,
};
use language::CursorShape;
use language::{CursorShape, Selection, SelectionGoal};
pub use mode_indicator::ModeIndicator;
use motion::Motion;
use normal::normal_replace;
use serde::Deserialize;
use settings::{Setting, SettingsStore};
use state::{Mode, Operator, VimState};
use state::{EditorState, Mode, Operator, WorkspaceState};
use std::sync::Arc;
use visual::visual_replace;
use visual::{visual_block_motion, visual_replace};
use workspace::{self, Workspace};
struct VimModeSetting(bool);
@ -127,7 +127,9 @@ pub struct Vim {
active_editor: Option<WeakViewHandle<Editor>>,
editor_subscription: Option<Subscription>,
enabled: bool,
state: VimState,
editor_states: HashMap<usize, EditorState>,
workspace_state: WorkspaceState,
default_state: EditorState,
}
impl Vim {
@ -143,13 +145,13 @@ impl Vim {
}
fn set_active_editor(&mut self, editor: ViewHandle<Editor>, cx: &mut WindowContext) {
self.active_editor = Some(editor.downgrade());
self.active_editor = Some(editor.clone().downgrade());
self.editor_subscription = Some(cx.subscribe(&editor, |editor, event, cx| match event {
Event::SelectionsChanged { local: true } => {
let editor = editor.read(cx);
if editor.leader_replica_id().is_none() {
let newest_empty = editor.selections.newest::<usize>(cx).is_empty();
local_selections_changed(newest_empty, cx);
let newest = editor.selections.newest::<usize>(cx);
local_selections_changed(newest, cx);
}
}
Event::InputIgnored { text } => {
@ -163,8 +165,11 @@ impl Vim {
let editor_mode = editor.mode();
let newest_selection_empty = editor.selections.newest::<usize>(cx).is_empty();
if editor_mode == EditorMode::Full && !newest_selection_empty {
self.switch_mode(Mode::Visual { line: false }, true, cx);
if editor_mode == EditorMode::Full
&& !newest_selection_empty
&& self.state().mode == Mode::Normal
{
self.switch_mode(Mode::Visual, true, cx);
}
}
@ -181,9 +186,14 @@ impl Vim {
}
fn switch_mode(&mut self, mode: Mode, leave_selections: bool, cx: &mut WindowContext) {
let last_mode = self.state.mode;
self.state.mode = mode;
self.state.operator_stack.clear();
let state = self.state();
let last_mode = state.mode;
let prior_mode = state.last_mode;
self.update_state(|state| {
state.last_mode = last_mode;
state.mode = mode;
state.operator_stack.clear();
});
cx.emit_global(VimEvent::ModeChanged { mode });
@ -196,11 +206,33 @@ impl Vim {
// Adjust selections
self.update_active_editor(cx, |editor, cx| {
if last_mode != Mode::VisualBlock && last_mode.is_visual() && mode == Mode::VisualBlock
{
visual_block_motion(true, editor, cx, |_, point, goal| Some((point, goal)))
}
editor.change_selections(None, cx, |s| {
// we cheat with visual block mode and use multiple cursors.
// the cost of this cheat is we need to convert back to a single
// cursor whenever vim would.
if last_mode == Mode::VisualBlock
&& (mode != Mode::VisualBlock && mode != Mode::Insert)
{
let tail = s.oldest_anchor().tail();
let head = s.newest_anchor().head();
s.select_anchor_ranges(vec![tail..head]);
} else if last_mode == Mode::Insert
&& prior_mode == Mode::VisualBlock
&& mode != Mode::VisualBlock
{
let pos = s.first_anchor().head();
s.select_anchor_ranges(vec![pos..pos])
}
s.move_with(|map, selection| {
if last_mode.is_visual() && !mode.is_visual() {
let mut point = selection.head();
if !selection.reversed {
if !selection.reversed && !selection.is_empty() {
point = movement::left(map, selection.head());
}
selection.collapse_to(point, selection.goal)
@ -215,7 +247,7 @@ impl Vim {
}
fn push_operator(&mut self, operator: Operator, cx: &mut WindowContext) {
self.state.operator_stack.push(operator);
self.update_state(|state| state.operator_stack.push(operator));
self.sync_vim_settings(cx);
}
@ -228,9 +260,13 @@ impl Vim {
}
}
fn maybe_pop_operator(&mut self) -> Option<Operator> {
self.update_state(|state| state.operator_stack.pop())
}
fn pop_operator(&mut self, cx: &mut WindowContext) -> Operator {
let popped_operator = self.state.operator_stack.pop()
.expect("Operator popped when no operator was on the stack. This likely means there is an invalid keymap config");
let popped_operator = self.update_state( |state| state.operator_stack.pop()
) .expect("Operator popped when no operator was on the stack. This likely means there is an invalid keymap config");
self.sync_vim_settings(cx);
popped_operator
}
@ -244,12 +280,12 @@ impl Vim {
}
fn clear_operator(&mut self, cx: &mut WindowContext) {
self.state.operator_stack.clear();
self.update_state(|state| state.operator_stack.clear());
self.sync_vim_settings(cx);
}
fn active_operator(&self) -> Option<Operator> {
self.state.operator_stack.last().copied()
self.state().operator_stack.last().copied()
}
fn active_editor_input_ignored(text: Arc<str>, cx: &mut WindowContext) {
@ -260,17 +296,21 @@ impl Vim {
match Vim::read(cx).active_operator() {
Some(Operator::FindForward { before }) => {
let find = Motion::FindForward { before, text };
Vim::update(cx, |vim, _| vim.state.last_find = Some(find.clone()));
Vim::update(cx, |vim, _| {
vim.workspace_state.last_find = Some(find.clone())
});
motion::motion(find, cx)
}
Some(Operator::FindBackward { after }) => {
let find = Motion::FindBackward { after, text };
Vim::update(cx, |vim, _| vim.state.last_find = Some(find.clone()));
Vim::update(cx, |vim, _| {
vim.workspace_state.last_find = Some(find.clone())
});
motion::motion(find, cx)
}
Some(Operator::Replace) => match Vim::read(cx).state.mode {
Some(Operator::Replace) => match Vim::read(cx).state().mode {
Mode::Normal => normal_replace(text, cx),
Mode::Visual { .. } => visual_replace(text, cx),
Mode::Visual | Mode::VisualLine | Mode::VisualBlock => visual_replace(text, cx),
_ => Vim::update(cx, |vim, cx| vim.clear_operator(cx)),
},
_ => {}
@ -280,7 +320,6 @@ impl Vim {
fn set_enabled(&mut self, enabled: bool, cx: &mut AppContext) {
if self.enabled != enabled {
self.enabled = enabled;
self.state = Default::default();
cx.update_default_global::<CommandPaletteFilter, _, _>(|filter, _| {
if self.enabled {
@ -307,8 +346,29 @@ impl Vim {
}
}
pub fn state(&self) -> &EditorState {
if let Some(active_editor) = self.active_editor.as_ref() {
if let Some(state) = self.editor_states.get(&active_editor.id()) {
return state;
}
}
&self.default_state
}
pub fn update_state<T>(&mut self, func: impl FnOnce(&mut EditorState) -> T) -> T {
let mut state = self.state().clone();
let ret = func(&mut state);
if let Some(active_editor) = self.active_editor.as_ref() {
self.editor_states.insert(active_editor.id(), state);
}
ret
}
fn sync_vim_settings(&self, cx: &mut WindowContext) {
let state = &self.state;
let state = self.state();
let cursor_shape = state.cursor_shape();
self.update_active_editor(cx, |editor, cx| {
@ -317,7 +377,8 @@ impl Vim {
editor.set_clip_at_line_ends(state.clip_at_line_ends(), cx);
editor.set_collapse_matches(true);
editor.set_input_enabled(!state.vim_controlled());
editor.selections.line_mode = matches!(state.mode, Mode::Visual { line: true });
editor.set_autoindent(state.should_autoindent());
editor.selections.line_mode = matches!(state.mode, Mode::VisualLine);
let context_layer = state.keymap_context_layer();
editor.set_keymap_context_layer::<Self>(context_layer, cx);
} else {
@ -333,6 +394,7 @@ impl Vim {
editor.set_cursor_shape(CursorShape::Bar, cx);
editor.set_clip_at_line_ends(false, cx);
editor.set_input_enabled(true);
editor.set_autoindent(true);
editor.selections.line_mode = false;
// we set the VimEnabled context on all editors so that we
@ -365,10 +427,14 @@ impl Setting for VimModeSetting {
}
}
fn local_selections_changed(newest_empty: bool, cx: &mut WindowContext) {
fn local_selections_changed(newest: Selection<usize>, cx: &mut WindowContext) {
Vim::update(cx, |vim, cx| {
if vim.enabled && vim.state.mode == Mode::Normal && !newest_empty {
vim.switch_mode(Mode::Visual { line: false }, false, cx)
if vim.enabled && vim.state().mode == Mode::Normal && !newest.is_empty() {
if matches!(newest.goal, SelectionGoal::ColumnRange { .. }) {
vim.switch_mode(Mode::VisualBlock, false, cx);
} else {
vim.switch_mode(Mode::Visual, false, cx)
}
}
})
}

File diff suppressed because it is too large Load diff

View file

@ -1,15 +1,15 @@
{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}}
{"Key":"shift-v"}
{"Get":{"state":"The «qˇ»uick brown\nfox jumps over\nthe lazy dog","mode":{"Visual":{"line":true}}}}
{"Get":{"state":"The «qˇ»uick brown\nfox jumps over\nthe lazy dog","mode":"VisualLine"}}
{"Key":"x"}
{"Get":{"state":"fox ˇjumps over\nthe lazy dog","mode":"Normal"}}
{"Put":{"state":"a\nˇ\nb"}}
{"Key":"shift-v"}
{"Get":{"state":"a\n«\nˇ»b","mode":{"Visual":{"line":true}}}}
{"Get":{"state":"a\n«\nˇ»b","mode":"VisualLine"}}
{"Key":"x"}
{"Get":{"state":"a\nˇb","mode":"Normal"}}
{"Put":{"state":"a\nb\nˇ"}}
{"Key":"shift-v"}
{"Get":{"state":"a\nb\nˇ","mode":{"Visual":{"line":true}}}}
{"Get":{"state":"a\nb\nˇ","mode":"VisualLine"}}
{"Key":"x"}
{"Get":{"state":"a\nˇb","mode":"Normal"}}

View file

@ -1,20 +1,20 @@
{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}}
{"Key":"v"}
{"Get":{"state":"The «qˇ»uick brown\nfox jumps over\nthe lazy dog","mode":{"Visual":{"line":false}}}}
{"Get":{"state":"The «qˇ»uick brown\nfox jumps over\nthe lazy dog","mode":"Visual"}}
{"Key":"w"}
{"Key":"j"}
{"Get":{"state":"The «quick brown\nfox jumps oˇ»ver\nthe lazy dog","mode":{"Visual":{"line":false}}}}
{"Get":{"state":"The «quick brown\nfox jumps oˇ»ver\nthe lazy dog","mode":"Visual"}}
{"Key":"escape"}
{"Get":{"state":"The quick brown\nfox jumps ˇover\nthe lazy dog","mode":"Normal"}}
{"Key":"v"}
{"Key":"k"}
{"Key":"b"}
{"Get":{"state":"The «ˇquick brown\nfox jumps o»ver\nthe lazy dog","mode":{"Visual":{"line":false}}}}
{"Get":{"state":"The «ˇquick brown\nfox jumps o»ver\nthe lazy dog","mode":"Visual"}}
{"Put":{"state":"a\nˇ\nb\n"}}
{"Key":"v"}
{"Get":{"state":"a\n«\nˇ»b\n","mode":{"Visual":{"line":false}}}}
{"Get":{"state":"a\n«\nˇ»b\n","mode":"Visual"}}
{"Key":"v"}
{"Get":{"state":"a\nˇ\nb\n","mode":"Normal"}}
{"Put":{"state":"a\nb\nˇ"}}
{"Key":"v"}
{"Get":{"state":"a\nb\nˇ","mode":{"Visual":{"line":false}}}}
{"Get":{"state":"a\nb\nˇ","mode":"Visual"}}

View file

@ -0,0 +1,23 @@
{"SetOption":{"value":"foldmethod=manual"}}
{"Put":{"state":"fn boop() {\n ˇbarp()\n bazp()\n}\n"}}
{"Key":"shift-v"}
{"Key":"j"}
{"Key":"z"}
{"Key":"f"}
{"Key":"escape"}
{"Key":"g"}
{"Key":"g"}
{"Get":{"state":"ˇfn boop() {\n barp()\n bazp()\n}\n","mode":"Normal"}}
{"Key":"j"}
{"Key":"j"}
{"Get":{"state":"fn boop() {\n barp()\n bazp()\nˇ}\n","mode":"Normal"}}
{"Key":"2"}
{"Key":"k"}
{"Get":{"state":"ˇfn boop() {\n barp()\n bazp()\n}\n","mode":"Normal"}}
{"Key":"down"}
{"Key":"y"}
{"Key":"y"}
{"ReadRegister":{"name":"\"","value":" barp()\n bazp()\n"}}
{"Key":"z"}
{"Key":"o"}
{"Get":{"state":"fn boop() {\nˇ barp()\n bazp()\n}\n","mode":"Normal"}}

View file

@ -2,9 +2,9 @@
{"Key":"v"}
{"Key":"i"}
{"Key":"{"}
{"Get":{"state":"func empty(a string) bool {\n« if a == \"\" {\n return true\n }\n return false\nˇ»}","mode":{"Visual":{"line":false}}}}
{"Get":{"state":"func empty(a string) bool {\n« if a == \"\" {\n return true\n }\n return false\nˇ»}","mode":"Visual"}}
{"Put":{"state":"func empty(a string) bool {\n if a == \"\" {\n ˇreturn true\n }\n return false\n}"}}
{"Key":"v"}
{"Key":"i"}
{"Key":"{"}
{"Get":{"state":"func empty(a string) bool {\n if a == \"\" {\n« return true\nˇ» }\n return false\n}","mode":{"Visual":{"line":false}}}}
{"Get":{"state":"func empty(a string) bool {\n if a == \"\" {\n« return true\nˇ» }\n return false\n}","mode":"Visual"}}

View file

@ -0,0 +1,3 @@
{"Put":{"state":"ˇone\n two\nthree"}}
{"Key":"enter"}
{"Get":{"state":"one\n ˇtwo\nthree","mode":"Normal"}}

View file

@ -1,13 +0,0 @@
{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog"}}
{"Key":"d"}
{"Key":"d"}
{"Get":{"state":"The quick brown\nthe laˇzy dog","mode":"Normal"}}
{"Key":"p"}
{"Get":{"state":"The quick brown\nthe lazy dog\nˇfox jumps over","mode":"Normal"}}
{"Put":{"state":"The quick brown\nfox ˇjumps over\nthe lazy dog"}}
{"Key":"v"}
{"Key":"w"}
{"Key":"y"}
{"Put":{"state":"The quick brown\nfox jumps oveˇr\nthe lazy dog"}}
{"Key":"p"}
{"Get":{"state":"The quick brown\nfox jumps overjumps ˇo\nthe lazy dog","mode":"Normal"}}

View file

@ -0,0 +1,31 @@
{"Put":{"state":"The quick brown\nfox ˇjumps over\nthe lazy dog"}}
{"Key":"v"}
{"Key":"w"}
{"Key":"y"}
{"ReadRegister":{"name":"\"","value":"jumps o"}}
{"Put":{"state":"The quick brown\nfox jumps oveˇr\nthe lazy dog"}}
{"Key":"p"}
{"Get":{"state":"The quick brown\nfox jumps overjumps ˇo\nthe lazy dog","mode":"Normal"}}
{"Put":{"state":"The quick brown\nfox jumps oveˇr\nthe lazy dog"}}
{"Key":"shift-p"}
{"Get":{"state":"The quick brown\nfox jumps ovejumps ˇor\nthe lazy dog","mode":"Normal"}}
{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog"}}
{"Key":"d"}
{"Key":"d"}
{"ReadRegister":{"name":"\"","value":"fox jumps over\n"}}
{"Get":{"state":"The quick brown\nthe laˇzy dog","mode":"Normal"}}
{"Key":"p"}
{"Get":{"state":"The quick brown\nthe lazy dog\nˇfox jumps over","mode":"Normal"}}
{"Key":"k"}
{"Key":"shift-p"}
{"Get":{"state":"The quick brown\nˇfox jumps over\nthe lazy dog\nfox jumps over","mode":"Normal"}}
{"Put":{"state":"The quick brown\nfox jumps ˇover\nthe lazy dog"}}
{"Key":"v"}
{"Key":"j"}
{"Key":"y"}
{"ReadRegister":{"name":"\"","value":"over\nthe lazy do"}}
{"Key":"p"}
{"Get":{"state":"The quick brown\nfox jumps oˇover\nthe lazy dover\nthe lazy dog","mode":"Normal"}}
{"Key":"u"}
{"Key":"shift-p"}
{"Get":{"state":"The quick brown\nfox jumps ˇover\nthe lazy doover\nthe lazy dog","mode":"Normal"}}

View file

@ -0,0 +1,42 @@
{"Put":{"state":"The quick brown\nfox jˇumps over\nthe lazy dog"}}
{"Key":"v"}
{"Key":"i"}
{"Key":"w"}
{"Key":"y"}
{"Get":{"state":"The quick brown\nfox ˇjumps over\nthe lazy dog","mode":"Normal"}}
{"Key":"w"}
{"Key":"v"}
{"Key":"i"}
{"Key":"w"}
{"Key":"p"}
{"Get":{"state":"The quick brown\nfox jumps jumpˇs\nthe lazy dog","mode":"Normal"}}
{"ReadRegister":{"name":"\"","value":"over"}}
{"Key":"up"}
{"Key":"shift-v"}
{"Key":"shift-p"}
{"Get":{"state":"ˇover\nfox jumps jumps\nthe lazy dog","mode":"Normal"}}
{"ReadRegister":{"name":"\"","value":"over"}}
{"Key":"ctrl-v"}
{"Key":"down"}
{"Key":"down"}
{"Key":"p"}
{"Get":{"state":"oveˇrver\noverox jumps jumps\noverhe lazy dog","mode":"Normal"}}
{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog"}}
{"Key":"shift-v"}
{"Key":"d"}
{"Get":{"state":"The quick brown\nthe laˇzy dog","mode":"Normal"}}
{"Key":"v"}
{"Key":"i"}
{"Key":"w"}
{"Key":"p"}
{"Get":{"state":"The quick brown\nthe \nˇfox jumps over\n dog","mode":"Normal"}}
{"ReadRegister":{"name":"\"","value":"lazy"}}
{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog"}}
{"Key":"shift-v"}
{"Key":"d"}
{"Get":{"state":"The quick brown\nthe laˇzy dog","mode":"Normal"}}
{"Key":"k"}
{"Key":"shift-v"}
{"Key":"p"}
{"Get":{"state":"ˇfox jumps over\nthe lazy dog","mode":"Normal"}}
{"ReadRegister":{"name":"\"","value":"The quick brown\n"}}

View file

@ -0,0 +1,31 @@
{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}}
{"Key":"ctrl-v"}
{"Key":"2"}
{"Key":"j"}
{"Key":"y"}
{"ReadRegister":{"name":"\"","value":"q\nj\nl"}}
{"Key":"p"}
{"Get":{"state":"The qˇquick brown\nfox jjumps over\nthe llazy dog","mode":"Normal"}}
{"Key":"v"}
{"Key":"i"}
{"Key":"w"}
{"Key":"shift-p"}
{"Get":{"state":"The ˇq brown\nfox jjjumps over\nthe lllazy dog","mode":"Normal"}}
{"Key":"v"}
{"Key":"i"}
{"Key":"w"}
{"Key":"shift-p"}
{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}}
{"Key":"ctrl-v"}
{"Key":"j"}
{"Key":"y"}
{"ReadRegister":{"name":"\"","value":"q\nj"}}
{"Key":"l"}
{"Key":"ctrl-v"}
{"Key":"2"}
{"Key":"j"}
{"Key":"shift-p"}
{"Get":{"state":"The qˇqick brown\nfox jjmps over\nthe lzy dog","mode":"Normal"}}
{"Key":"shift-v"}
{"Key":"p"}
{"Get":{"state":"ˇq\nj\nfox jjmps over\nthe lzy dog","mode":"Normal"}}

View file

@ -0,0 +1,18 @@
{"Put":{"state":"ˇThe quick brown\nfox jumps over\nthe lazy dog\n"}}
{"Key":"ctrl-v"}
{"Key":"9"}
{"Key":"down"}
{"Get":{"state":"«Tˇ»he quick brown\n«fˇ»ox jumps over\n«tˇ»he lazy dog\nˇ","mode":"VisualBlock"}}
{"Key":"shift-i"}
{"Key":"k"}
{"Key":"escape"}
{"Get":{"state":"ˇkThe quick brown\nkfox jumps over\nkthe lazy dog\nk","mode":"Normal"}}
{"Put":{"state":"ˇThe quick brown\nfox jumps over\nthe lazy dog\n"}}
{"Key":"ctrl-v"}
{"Key":"9"}
{"Key":"down"}
{"Get":{"state":"«Tˇ»he quick brown\n«fˇ»ox jumps over\n«tˇ»he lazy dog\nˇ","mode":"VisualBlock"}}
{"Key":"c"}
{"Key":"k"}
{"Key":"escape"}
{"Get":{"state":"ˇkhe quick brown\nkox jumps over\nkhe lazy dog\nk","mode":"Normal"}}

View file

@ -0,0 +1,38 @@
{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}}
{"Key":"ctrl-v"}
{"Get":{"state":"The «qˇ»uick brown\nfox jumps over\nthe lazy dog","mode":"VisualBlock"}}
{"Key":"2"}
{"Key":"down"}
{"Get":{"state":"The «qˇ»uick brown\nfox «jˇ»umps over\nthe «lˇ»azy dog","mode":"VisualBlock"}}
{"Key":"e"}
{"Get":{"state":"The «quicˇ»k brown\nfox «jumpˇ»s over\nthe «lazyˇ» dog","mode":"VisualBlock"}}
{"Key":"^"}
{"Get":{"state":"«ˇThe q»uick brown\n«ˇfox j»umps over\n«ˇthe l»azy dog","mode":"VisualBlock"}}
{"Key":"$"}
{"Get":{"state":"The «quick brownˇ»\nfox «jumps overˇ»\nthe «lazy dogˇ»","mode":"VisualBlock"}}
{"Key":"shift-f"}
{"Key":" "}
{"Get":{"state":"The «quickˇ» brown\nfox «jumpsˇ» over\nthe «lazy ˇ»dog","mode":"VisualBlock"}}
{"Key":"v"}
{"Get":{"state":"The «quick brown\nfox jumps over\nthe lazy ˇ»dog","mode":"Visual"}}
{"Key":"ctrl-v"}
{"Get":{"state":"The «quickˇ» brown\nfox «jumpsˇ» over\nthe «lazy ˇ»dog","mode":"VisualBlock"}}
{"Put":{"state":"The ˇquick\nbrown\nfox\njumps over the\n\nlazy dog\n"}}
{"Key":"ctrl-v"}
{"Key":"down"}
{"Key":"down"}
{"Get":{"state":"The«ˇ q»uick\nbro«ˇwn»\nfoxˇ\njumps over the\n\nlazy dog\n","mode":"VisualBlock"}}
{"Key":"down"}
{"Get":{"state":"The «qˇ»uick\nbrow«nˇ»\nfox\njump«sˇ» over the\n\nlazy dog\n","mode":"VisualBlock"}}
{"Key":"left"}
{"Get":{"state":"The«ˇ q»uick\nbro«ˇwn»\nfoxˇ\njum«ˇps» over the\n\nlazy dog\n","mode":"VisualBlock"}}
{"Key":"s"}
{"Key":"o"}
{"Key":"escape"}
{"Get":{"state":"Theˇouick\nbroo\nfoxo\njumo over the\n\nlazy dog\n","mode":"Normal"}}
{"Put":{"state":"Theˇ quick brown\n\nfox jumps over\nthe lazy dog\n"}}
{"Key":"l"}
{"Key":"ctrl-v"}
{"Key":"j"}
{"Key":"j"}
{"Get":{"state":"The «qˇ»uick brown\n\nfox «jˇ»umps over\nthe lazy dog\n","mode":"VisualBlock"}}

View file

@ -1,7 +1,7 @@
{"Put":{"state":"The quick ˇbrown"}}
{"Key":"v"}
{"Key":"w"}
{"Get":{"state":"The quick «brownˇ»","mode":{"Visual":{"line":false}}}}
{"Get":{"state":"The quick «brownˇ»","mode":"Visual"}}
{"Put":{"state":"The quick ˇbrown"}}
{"Key":"v"}
{"Key":"w"}

View file

@ -4,14 +4,11 @@
{"Get":{"state":"fox juˇmps over\nthe lazy dog","mode":"Normal"}}
{"Key":"p"}
{"Get":{"state":"fox jumps over\nˇThe quick brown\nthe lazy dog","mode":"Normal"}}
{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog"}}
{"Key":"shift-v"}
{"Key":"x"}
{"Get":{"state":"The quick brown\nthe laˇzy dog","mode":"Normal"}}
{"Put":{"state":"The quick brown\nfox jumps over\nthe laˇzy dog"}}
{"Key":"shift-v"}
{"Key":"x"}
{"Get":{"state":"The quick brown\nfox juˇmps over","mode":"Normal"}}
{"ReadRegister":{"name":"\"","value":"the lazy dog\n"}}
{"Put":{"state":"The quˇick brown\nfox jumps over\nthe lazy dog"}}
{"Key":"shift-v"}
{"Key":"j"}
@ -19,16 +16,6 @@
{"Get":{"state":"the laˇzy dog","mode":"Normal"}}
{"Key":"p"}
{"Get":{"state":"the lazy dog\nˇThe quick brown\nfox jumps over","mode":"Normal"}}
{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog"}}
{"Key":"shift-v"}
{"Key":"j"}
{"Key":"x"}
{"Get":{"state":"The quˇick brown","mode":"Normal"}}
{"Put":{"state":"The quick brown\nfox jumps over\nthe laˇzy dog"}}
{"Key":"shift-v"}
{"Key":"j"}
{"Key":"x"}
{"Get":{"state":"The quick brown\nfox juˇmps over","mode":"Normal"}}
{"Put":{"state":"The ˇlong line\nshould not\ncrash\n"}}
{"Key":"shift-v"}
{"Key":"$"}

View file

@ -0,0 +1,19 @@
{"Put":{"state":"hello (in [parˇens] o)"}}
{"Key":"ctrl-v"}
{"Key":"l"}
{"Key":"a"}
{"Key":"]"}
{"Get":{"state":"hello (in «[parens]ˇ» o)","mode":"Visual"}}
{"Key":"i"}
{"Key":"("}
{"Get":{"state":"hello («in [parens] oˇ»)","mode":"Visual"}}
{"Put":{"state":"hello in a wˇord again."}}
{"Key":"ctrl-v"}
{"Key":"l"}
{"Key":"i"}
{"Key":"w"}
{"Get":{"state":"hello in a w«ordˇ» again.","mode":"VisualBlock"}}
{"Key":"o"}
{"Key":"a"}
{"Key":"s"}
{"Get":{"state":"«ˇhello in a word» again.","mode":"VisualBlock"}}

View file

@ -0,0 +1,26 @@
{"Put":{"state":"The quick brown\nfox jˇumps over\nthe lazy dog"}}
{"Key":"v"}
{"Key":"i"}
{"Key":"w"}
{"Key":"y"}
{"Get":{"state":"The quick brown\nfox ˇjumps over\nthe lazy dog","mode":"Normal"}}
{"Key":"p"}
{"Get":{"state":"The quick brown\nfox jjumpˇsumps over\nthe lazy dog","mode":"Normal"}}
{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog"}}
{"Key":"shift-v"}
{"Key":"d"}
{"Get":{"state":"The quick brown\nthe laˇzy dog","mode":"Normal"}}
{"Key":"v"}
{"Key":"i"}
{"Key":"w"}
{"Key":"p"}
{"Get":{"state":"The quick brown\nthe \nˇfox jumps over\n dog","mode":"Normal"}}
{"ReadRegister":{"name":"\"","value":"lazy"}}
{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog"}}
{"Key":"shift-v"}
{"Key":"d"}
{"Get":{"state":"The quick brown\nthe laˇzy dog","mode":"Normal"}}
{"Key":"k"}
{"Key":"shift-v"}
{"Key":"p"}
{"Get":{"state":"ˇfox jumps over\nthe lazy dog","mode":"Normal"}}

View file

@ -1,236 +1,236 @@
{"Put":{"state":"The quick ˇbrown\nfox"}}
{"Key":"v"}
{"Get":{"state":"The quick «bˇ»rown\nfox","mode":{"Visual":{"line":false}}}}
{"Get":{"state":"The quick «bˇ»rown\nfox","mode":"Visual"}}
{"Key":"i"}
{"Key":"w"}
{"Get":{"state":"The quick «brownˇ»\nfox","mode":{"Visual":{"line":false}}}}
{"Get":{"state":"The quick «brownˇ»\nfox","mode":"Visual"}}
{"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":"v"}
{"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":{"Visual":{"line":false}}}}
{"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":"Visual"}}
{"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":"v"}
{"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":{"Visual":{"line":false}}}}
{"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":"Visual"}}
{"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":"v"}
{"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":{"Visual":{"line":false}}}}
{"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":"Visual"}}
{"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":"v"}
{"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":{"Visual":{"line":false}}}}
{"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":"Visual"}}
{"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":"v"}
{"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":{"Visual":{"line":false}}}}
{"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":"Visual"}}
{"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":"v"}
{"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":{"Visual":{"line":false}}}}
{"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":"Visual"}}
{"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":"v"}
{"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":{"Visual":{"line":false}}}}
{"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":"Visual"}}
{"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":"v"}
{"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":{"Visual":{"line":false}}}}
{"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":"Visual"}}
{"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":"v"}
{"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":{"Visual":{"line":false}}}}
{"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":"Visual"}}
{"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":"v"}
{"Key":"i"}
{"Key":"w"}
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n«\nˇ»The-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n«\nˇ»The-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}}
{"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":"v"}
{"Key":"i"}
{"Key":"w"}
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\n«Theˇ»-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\n«Theˇ»-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}}
{"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":"v"}
{"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":{"Visual":{"line":false}}}}
{"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":"Visual"}}
{"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":"v"}
{"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":{"Visual":{"line":false}}}}
{"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":"Visual"}}
{"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":"v"}
{"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":{"Visual":{"line":false}}}}
{"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":"Visual"}}
{"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":"v"}
{"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":{"Visual":{"line":false}}}}
{"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":"Visual"}}
{"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":"v"}
{"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":{"Visual":{"line":false}}}}
{"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":"Visual"}}
{"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":"v"}
{"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":{"Visual":{"line":false}}}}
{"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":"Visual"}}
{"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":"v"}
{"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":{"Visual":{"line":false}}}}
{"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":"Visual"}}
{"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":"v"}
{"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":{"Visual":{"line":false}}}}
{"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":"Visual"}}
{"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":"v"}
{"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":{"Visual":{"line":false}}}}
{"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":"Visual"}}
{"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":"v"}
{"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":{"Visual":{"line":false}}}}
{"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":"Visual"}}
{"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":"v"}
{"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":{"Visual":{"line":false}}}}
{"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":"Visual"}}
{"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":"v"}
{"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":{"Visual":{"line":false}}}}
{"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":"Visual"}}
{"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":"v"}
{"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":{"Visual":{"line":false}}}}
{"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":"Visual"}}
{"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":"v"}
{"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":{"Visual":{"line":false}}}}
{"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":"Visual"}}
{"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":"v"}
{"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":{"Visual":{"line":false}}}}
{"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":"Visual"}}
{"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":"v"}
{"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":{"Visual":{"line":false}}}}
{"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":"Visual"}}
{"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":"v"}
{"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":{"Visual":{"line":false}}}}
{"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":"Visual"}}
{"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":"v"}
{"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":{"Visual":{"line":false}}}}
{"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":"Visual"}}
{"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":"v"}
{"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":{"Visual":{"line":false}}}}
{"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":"Visual"}}
{"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":"v"}
{"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":{"Visual":{"line":false}}}}
{"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":"Visual"}}
{"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":"v"}
{"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":{"Visual":{"line":false}}}}
{"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":"Visual"}}
{"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":"v"}
{"Key":"i"}
{"Key":"shift-w"}
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n«\nˇ»The-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n«\nˇ»The-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}}
{"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":"v"}
{"Key":"i"}
{"Key":"shift-w"}
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\n«The-quickˇ» brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\n«The-quickˇ» brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}}
{"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":"v"}
{"Key":"i"}
{"Key":"shift-w"}
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\n«The-quickˇ» brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\n«The-quickˇ» brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}}
{"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":"v"}
{"Key":"i"}
{"Key":"shift-w"}
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\n«The-quickˇ» brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\n«The-quickˇ» brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}}
{"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":"v"}
{"Key":"i"}
{"Key":"shift-w"}
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\n«The-quickˇ» brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}}
{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\n«The-quickˇ» brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}}
{"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":"v"}
{"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":{"Visual":{"line":false}}}}
{"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":"Visual"}}
{"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":"v"}
{"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":{"Visual":{"line":false}}}}
{"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":"Visual"}}
{"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":"v"}
{"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":{"Visual":{"line":false}}}}
{"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":"Visual"}}
{"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":"v"}
{"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":{"Visual":{"line":false}}}}
{"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":"Visual"}}
{"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":"v"}
{"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":{"Visual":{"line":false}}}}
{"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":"Visual"}}
{"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":"v"}
{"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":{"Visual":{"line":false}}}}
{"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":"Visual"}}
{"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":"v"}
{"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":{"Visual":{"line":false}}}}
{"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":"Visual"}}
{"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":"v"}
{"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":{"Visual":{"line":false}}}}
{"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":"Visual"}}
{"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":"v"}
{"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":{"Visual":{"line":false}}}}
{"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":"Visual"}}

View file

@ -0,0 +1,29 @@
{"Put":{"state":"The quick ˇbrown"}}
{"Key":"v"}
{"Key":"w"}
{"Key":"y"}
{"Get":{"state":"The quick ˇbrown","mode":"Normal"}}
{"ReadRegister":{"name":"\"","value":"brown"}}
{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}}
{"Key":"v"}
{"Key":"w"}
{"Key":"j"}
{"Key":"y"}
{"Get":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog","mode":"Normal"}}
{"ReadRegister":{"name":"\"","value":"quick brown\nfox jumps o"}}
{"Put":{"state":"The quick brown\nfox jumps over\nthe ˇlazy dog"}}
{"Key":"v"}
{"Key":"w"}
{"Key":"j"}
{"Key":"y"}
{"Get":{"state":"The quick brown\nfox jumps over\nthe ˇlazy dog","mode":"Normal"}}
{"ReadRegister":{"name":"\"","value":"lazy d"}}
{"Key":"shift-v"}
{"Key":"y"}
{"ReadRegister":{"name":"\"","value":"the lazy dog\n"}}
{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}}
{"Key":"v"}
{"Key":"b"}
{"Key":"k"}
{"Key":"y"}
{"Get":{"state":"ˇThe quick brown\nfox jumps over\nthe lazy dog","mode":"Normal"}}

View file

@ -0,0 +1,50 @@
{"SetOption":{"value":"wrap"}}
{"SetOption":{"value":"columns=12"}}
{"Put":{"state":"tˇwelve char twelve char\ntwelve char\n"}}
{"Key":"j"}
{"Get":{"state":"twelve char twelve char\ntˇwelve char\n","mode":"Normal"}}
{"Key":"k"}
{"Get":{"state":"tˇwelve char twelve char\ntwelve char\n","mode":"Normal"}}
{"Key":"g"}
{"Key":"j"}
{"Get":{"state":"twelve char tˇwelve char\ntwelve char\n","mode":"Normal"}}
{"Key":"g"}
{"Key":"j"}
{"Get":{"state":"twelve char twelve char\ntˇwelve char\n","mode":"Normal"}}
{"Key":"g"}
{"Key":"k"}
{"Get":{"state":"twelve char tˇwelve char\ntwelve char\n","mode":"Normal"}}
{"Key":"g"}
{"Key":"^"}
{"Get":{"state":"twelve char ˇtwelve char\ntwelve char\n","mode":"Normal"}}
{"Key":"^"}
{"Get":{"state":"ˇtwelve char twelve char\ntwelve char\n","mode":"Normal"}}
{"Key":"g"}
{"Key":"$"}
{"Get":{"state":"twelve charˇ twelve char\ntwelve char\n","mode":"Normal"}}
{"Key":"$"}
{"Get":{"state":"twelve char twelve chaˇr\ntwelve char\n","mode":"Normal"}}
{"Put":{"state":"tˇwelve char twelve char\ntwelve char\n"}}
{"Key":"enter"}
{"Get":{"state":"twelve char twelve char\nˇtwelve char\n","mode":"Normal"}}
{"Put":{"state":"twelve char\ntˇwelve char twelve char\ntwelve char\n"}}
{"Key":"o"}
{"Key":"o"}
{"Key":"escape"}
{"Get":{"state":"twelve char\ntwelve char twelve char\nˇo\ntwelve char\n","mode":"Normal"}}
{"Put":{"state":"twelve char\ntˇwelve char twelve char\ntwelve char\n"}}
{"Key":"shift-a"}
{"Key":"a"}
{"Key":"escape"}
{"Get":{"state":"twelve char\ntwelve char twelve charˇa\ntwelve char\n","mode":"Normal"}}
{"Key":"shift-i"}
{"Key":"i"}
{"Key":"escape"}
{"Get":{"state":"twelve char\nˇitwelve char twelve chara\ntwelve char\n","mode":"Normal"}}
{"Key":"shift-d"}
{"Get":{"state":"twelve char\nˇ\ntwelve char\n","mode":"Normal"}}
{"Put":{"state":"twelve char\ntwelve char tˇwelve char\ntwelve char\n"}}
{"Key":"shift-o"}
{"Key":"o"}
{"Key":"escape"}
{"Get":{"state":"twelve char\nˇo\ntwelve char twelve char\ntwelve char\n","mode":"Normal"}}