From d3650594c386e2b96958a0fb552e5ad322a6df30 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 28 Aug 2023 11:47:37 -0600 Subject: [PATCH] Fix find_{,preceding}boundary to work on buffer text Before this change the bounday could mistakenly have happened on a soft line wrap. Also fixes interaction with inlays better. --- crates/editor/src/movement.rs | 174 ++++++------------ crates/vim/src/motion.rs | 26 ++- crates/vim/src/normal.rs | 2 +- crates/vim/src/normal/change.rs | 20 +- crates/vim/src/object.rs | 30 ++- crates/vim/src/test.rs | 18 ++ .../src/test/neovim_backed_test_context.rs | 7 +- crates/vim/test_data/test_end_of_word.json | 32 ++++ .../test_data/test_visual_word_object.json | 6 +- crates/vim/test_data/test_wrapped_lines.json | 5 + 10 files changed, 174 insertions(+), 146 deletions(-) create mode 100644 crates/vim/test_data/test_end_of_word.json diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index def6340e38..915da7b23f 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -1,8 +1,14 @@ use super::{Bias, DisplayPoint, DisplaySnapshot, SelectionGoal, ToDisplayPoint}; -use crate::{char_kind, CharKind, ToPoint}; +use crate::{char_kind, CharKind, ToOffset, ToPoint}; use language::Point; use std::ops::Range; +#[derive(Debug, PartialEq)] +pub enum FindRange { + SingleLine, + MultiLine, +} + pub fn left(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint { if point.column() > 0 { *point.column_mut() -= 1; @@ -179,7 +185,7 @@ pub fn previous_word_start(map: &DisplaySnapshot, point: DisplayPoint) -> Displa let raw_point = point.to_point(map); let language = map.buffer_snapshot.language_at(raw_point); - find_preceding_boundary(map, point, |left, right| { + find_preceding_boundary(map, point, FindRange::MultiLine, |left, right| { (char_kind(language, left) != char_kind(language, right) && !right.is_whitespace()) || left == '\n' }) @@ -188,7 +194,7 @@ pub fn previous_word_start(map: &DisplaySnapshot, point: DisplayPoint) -> Displa pub fn previous_subword_start(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint { let raw_point = point.to_point(map); let language = map.buffer_snapshot.language_at(raw_point); - find_preceding_boundary(map, point, |left, right| { + find_preceding_boundary(map, point, FindRange::MultiLine, |left, right| { let is_word_start = char_kind(language, left) != char_kind(language, right) && !right.is_whitespace(); let is_subword_start = @@ -200,7 +206,7 @@ pub fn previous_subword_start(map: &DisplaySnapshot, point: DisplayPoint) -> Dis pub fn next_word_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint { let raw_point = point.to_point(map); let language = map.buffer_snapshot.language_at(raw_point); - find_boundary(map, point, |left, right| { + find_boundary(map, point, FindRange::MultiLine, |left, right| { (char_kind(language, left) != char_kind(language, right) && !left.is_whitespace()) || right == '\n' }) @@ -209,7 +215,7 @@ pub fn next_word_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint pub fn next_subword_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint { let raw_point = point.to_point(map); let language = map.buffer_snapshot.language_at(raw_point); - find_boundary(map, point, |left, right| { + find_boundary(map, point, FindRange::MultiLine, |left, right| { let is_word_end = (char_kind(language, left) != char_kind(language, right)) && !left.is_whitespace(); let is_subword_end = @@ -272,79 +278,34 @@ pub fn end_of_paragraph( map.max_point() } -/// Scans for a boundary preceding the given start point `from` until a boundary is found, indicated by the -/// given predicate returning true. The predicate is called with the character to the left and right -/// of the candidate boundary location, and will be called with `\n` characters indicating the start -/// or end of a line. +/// Scans for a boundary preceding the given start point `from` until a boundary is found, +/// indicated by the given predicate returning true. +/// The predicate is called with the character to the left and right of the candidate boundary location. +/// If FindRange::SingleLine is specified and no boundary is found before the start of the current line, the start of the current line will be returned. pub fn find_preceding_boundary( map: &DisplaySnapshot, from: DisplayPoint, + find_range: FindRange, mut is_boundary: impl FnMut(char, char) -> bool, ) -> DisplayPoint { - let mut start_column = 0; - let mut soft_wrap_row = from.row() + 1; + let mut prev_ch = None; + let mut offset = from.to_point(map).to_offset(&map.buffer_snapshot); - let mut prev = None; - for (ch, point) in map.reverse_chars_at(from) { - // Recompute soft_wrap_indent if the row has changed - if point.row() != soft_wrap_row { - soft_wrap_row = point.row(); - - if point.row() == 0 { - start_column = 0; - } else if let Some(indent) = map.soft_wrap_indent(point.row() - 1) { - start_column = indent; - } - } - - // If the current point is in the soft_wrap, skip comparing it - if point.column() < start_column { - continue; - } - - if let Some((prev_ch, prev_point)) = prev { - if is_boundary(ch, prev_ch) { - return map.clip_point(prev_point, Bias::Left); - } - } - - prev = Some((ch, point)); - } - map.clip_point(DisplayPoint::zero(), Bias::Left) -} - -/// Scans for a boundary preceding the given start point `from` until a boundary is found, indicated by the -/// given predicate returning true. The predicate is called with the character to the left and right -/// of the candidate boundary location, and will be called with `\n` characters indicating the start -/// or end of a line. If no boundary is found, the start of the line is returned. -pub fn find_preceding_boundary_in_line( - map: &DisplaySnapshot, - from: DisplayPoint, - mut is_boundary: impl FnMut(char, char) -> bool, -) -> DisplayPoint { - let mut start_column = 0; - if from.row() > 0 { - if let Some(indent) = map.soft_wrap_indent(from.row() - 1) { - start_column = indent; - } - } - - let mut prev = None; - for (ch, point) in map.reverse_chars_at(from) { - if let Some((prev_ch, prev_point)) = prev { - if is_boundary(ch, prev_ch) { - return map.clip_point(prev_point, Bias::Left); - } - } - - if ch == '\n' || point.column() < start_column { + for ch in map.buffer_snapshot.reversed_chars_at(offset) { + if find_range == FindRange::SingleLine && ch == '\n' { break; } + if let Some(prev_ch) = prev_ch { + if is_boundary(ch, prev_ch) { + break; + } + } - prev = Some((ch, point)); + offset -= ch.len_utf8(); + prev_ch = Some(ch); } - map.clip_point(prev.map(|(_, point)| point).unwrap_or(from), Bias::Left) + map.clip_point(offset.to_display_point(map), Bias::Left) } /// Scans for a boundary following the given start point until a boundary is found, indicated by the @@ -354,47 +315,26 @@ pub fn find_preceding_boundary_in_line( pub fn find_boundary( map: &DisplaySnapshot, from: DisplayPoint, + find_range: FindRange, mut is_boundary: impl FnMut(char, char) -> bool, ) -> DisplayPoint { + let mut offset = from.to_offset(&map, Bias::Right); let mut prev_ch = None; - for (ch, point) in map.chars_at(from) { - if let Some(prev_ch) = prev_ch { - if is_boundary(prev_ch, ch) { - return map.clip_point(point, Bias::Right); - } - } - prev_ch = Some(ch); - } - map.clip_point(map.max_point(), Bias::Right) -} - -/// Scans for a boundary following the given start point until a boundary is found, indicated by the -/// given predicate returning true. The predicate is called with the character to the left and right -/// of the candidate boundary location, and will be called with `\n` characters indicating the start -/// or end of a line. If no boundary is found, the end of the line is returned -pub fn find_boundary_in_line( - map: &DisplaySnapshot, - from: DisplayPoint, - mut is_boundary: impl FnMut(char, char) -> bool, -) -> DisplayPoint { - let mut prev = None; - for (ch, point) in map.chars_at(from) { - if let Some((prev_ch, _)) = prev { - if is_boundary(prev_ch, ch) { - return map.clip_point(point, Bias::Right); - } - } - - prev = Some((ch, point)); - - if ch == '\n' { + for ch in map.buffer_snapshot.chars_at(offset) { + if find_range == FindRange::SingleLine && ch == '\n' { break; } - } + if let Some(prev_ch) = prev_ch { + if is_boundary(prev_ch, ch) { + break; + } + } - // Return the last position checked so that we give a point right before the newline or eof. - map.clip_point(prev.map(|(_, point)| point).unwrap_or(from), Bias::Right) + offset += ch.len_utf8(); + prev_ch = Some(ch); + } + map.clip_point(offset.to_display_point(map), Bias::Right) } pub fn is_inside_word(map: &DisplaySnapshot, point: DisplayPoint) -> bool { @@ -533,7 +473,12 @@ mod tests { ) { let (snapshot, display_points) = marked_display_snapshot(marked_text, cx); assert_eq!( - find_preceding_boundary(&snapshot, display_points[1], is_boundary), + find_preceding_boundary( + &snapshot, + display_points[1], + FindRange::MultiLine, + is_boundary + ), display_points[0] ); } @@ -612,21 +557,15 @@ mod tests { find_preceding_boundary( &snapshot, buffer_snapshot.len().to_display_point(&snapshot), - |left, _| left == 'a', + FindRange::MultiLine, + |left, _| left == 'e', ), - 0.to_display_point(&snapshot), + snapshot + .buffer_snapshot + .offset_to_point(5) + .to_display_point(&snapshot), "Should not stop at inlays when looking for boundaries" ); - - assert_eq!( - find_preceding_boundary_in_line( - &snapshot, - buffer_snapshot.len().to_display_point(&snapshot), - |left, _| left == 'a', - ), - 0.to_display_point(&snapshot), - "Should not stop at inlays when looking for boundaries in line" - ); } #[gpui::test] @@ -699,7 +638,12 @@ mod tests { ) { let (snapshot, display_points) = marked_display_snapshot(marked_text, cx); assert_eq!( - find_boundary(&snapshot, display_points[0], is_boundary), + find_boundary( + &snapshot, + display_points[0], + FindRange::MultiLine, + is_boundary + ), display_points[1] ); } diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index 0d3fb700ef..6f28430796 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -3,7 +3,8 @@ use std::{cmp, sync::Arc}; use editor::{ char_kind, display_map::{DisplaySnapshot, FoldPoint, ToDisplayPoint}, - movement, Bias, CharKind, DisplayPoint, ToOffset, + movement::{self, FindRange}, + Bias, CharKind, DisplayPoint, ToOffset, }; use gpui::{actions, impl_actions, AppContext, WindowContext}; use language::{Point, Selection, SelectionGoal}; @@ -592,7 +593,7 @@ pub(crate) fn next_word_start( let language = map.buffer_snapshot.language_at(point.to_point(map)); for _ in 0..times { let mut crossed_newline = false; - point = movement::find_boundary(map, point, |left, right| { + point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| { let left_kind = char_kind(language, left).coerce_punctuation(ignore_punctuation); let right_kind = char_kind(language, right).coerce_punctuation(ignore_punctuation); let at_newline = right == '\n'; @@ -616,8 +617,14 @@ fn next_word_end( ) -> DisplayPoint { let language = map.buffer_snapshot.language_at(point.to_point(map)); for _ in 0..times { - *point.column_mut() += 1; - point = movement::find_boundary(map, point, |left, right| { + if point.column() < map.line_len(point.row()) { + *point.column_mut() += 1; + } else if point.row() < map.max_buffer_row() { + *point.row_mut() += 1; + *point.column_mut() = 0; + } + // *point.column_mut() += 1; + point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| { let left_kind = char_kind(language, left).coerce_punctuation(ignore_punctuation); let right_kind = char_kind(language, right).coerce_punctuation(ignore_punctuation); @@ -649,12 +656,13 @@ fn previous_word_start( for _ in 0..times { // This works even though find_preceding_boundary is called for every character in the line containing // cursor because the newline is checked only once. - point = movement::find_preceding_boundary(map, point, |left, right| { - let left_kind = char_kind(language, left).coerce_punctuation(ignore_punctuation); - let right_kind = char_kind(language, right).coerce_punctuation(ignore_punctuation); + point = + movement::find_preceding_boundary(map, point, FindRange::MultiLine, |left, right| { + let left_kind = char_kind(language, left).coerce_punctuation(ignore_punctuation); + let right_kind = char_kind(language, right).coerce_punctuation(ignore_punctuation); - (left_kind != right_kind && !right.is_whitespace()) || left == '\n' - }); + (left_kind != right_kind && !right.is_whitespace()) || left == '\n' + }); } point } diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index a73c518809..c8e623e4c1 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -445,7 +445,7 @@ mod test { } #[gpui::test] - async fn test_e(cx: &mut gpui::TestAppContext) { + async fn test_end_of_word(cx: &mut gpui::TestAppContext) { let mut cx = NeovimBackedTestContext::new(cx).await.binding(["e"]); cx.assert_all(indoc! {" Thˇe quicˇkˇ-browˇn diff --git a/crates/vim/src/normal/change.rs b/crates/vim/src/normal/change.rs index 5591de89c6..6e64b050d1 100644 --- a/crates/vim/src/normal/change.rs +++ b/crates/vim/src/normal/change.rs @@ -1,7 +1,10 @@ use crate::{motion::Motion, object::Object, state::Mode, utils::copy_selections_content, Vim}; use editor::{ - char_kind, display_map::DisplaySnapshot, movement, scroll::autoscroll::Autoscroll, CharKind, - DisplayPoint, + char_kind, + display_map::DisplaySnapshot, + movement::{self, FindRange}, + scroll::autoscroll::Autoscroll, + CharKind, DisplayPoint, }; use gpui::WindowContext; use language::Selection; @@ -96,12 +99,15 @@ fn expand_changed_word_selection( .unwrap_or_default(); if in_word { - selection.end = movement::find_boundary(map, selection.end, |left, right| { - let left_kind = char_kind(language, left).coerce_punctuation(ignore_punctuation); - let right_kind = char_kind(language, right).coerce_punctuation(ignore_punctuation); + selection.end = + movement::find_boundary(map, selection.end, FindRange::MultiLine, |left, right| { + let left_kind = + char_kind(language, left).coerce_punctuation(ignore_punctuation); + let right_kind = + char_kind(language, right).coerce_punctuation(ignore_punctuation); - left_kind != right_kind && left_kind != CharKind::Whitespace - }); + left_kind != right_kind && left_kind != CharKind::Whitespace + }); true } else { Motion::NextWordStart { ignore_punctuation } diff --git a/crates/vim/src/object.rs b/crates/vim/src/object.rs index dd922e7af6..94906a1e80 100644 --- a/crates/vim/src/object.rs +++ b/crates/vim/src/object.rs @@ -1,6 +1,11 @@ use std::ops::Range; -use editor::{char_kind, display_map::DisplaySnapshot, movement, Bias, CharKind, DisplayPoint}; +use editor::{ + char_kind, + display_map::DisplaySnapshot, + movement::{self, FindRange}, + Bias, CharKind, DisplayPoint, +}; use gpui::{actions, impl_actions, AppContext, WindowContext}; use language::Selection; use serde::Deserialize; @@ -178,15 +183,16 @@ fn in_word( ) -> Option> { // Use motion::right so that we consider the character under the cursor when looking for the start let language = map.buffer_snapshot.language_at(relative_to.to_point(map)); - let start = movement::find_preceding_boundary_in_line( + let start = movement::find_preceding_boundary( map, right(map, relative_to, 1), + movement::FindRange::SingleLine, |left, right| { char_kind(language, left).coerce_punctuation(ignore_punctuation) != char_kind(language, right).coerce_punctuation(ignore_punctuation) }, ); - let end = movement::find_boundary_in_line(map, relative_to, |left, right| { + let end = movement::find_boundary(map, relative_to, FindRange::SingleLine, |left, right| { char_kind(language, left).coerce_punctuation(ignore_punctuation) != char_kind(language, right).coerce_punctuation(ignore_punctuation) }); @@ -241,9 +247,10 @@ fn around_next_word( ) -> Option> { let language = map.buffer_snapshot.language_at(relative_to.to_point(map)); // Get the start of the word - let start = movement::find_preceding_boundary_in_line( + let start = movement::find_preceding_boundary( map, right(map, relative_to, 1), + FindRange::SingleLine, |left, right| { char_kind(language, left).coerce_punctuation(ignore_punctuation) != char_kind(language, right).coerce_punctuation(ignore_punctuation) @@ -251,7 +258,7 @@ fn around_next_word( ); let mut word_found = false; - let end = movement::find_boundary(map, relative_to, |left, right| { + let end = movement::find_boundary(map, relative_to, FindRange::MultiLine, |left, right| { let left_kind = char_kind(language, left).coerce_punctuation(ignore_punctuation); let right_kind = char_kind(language, right).coerce_punctuation(ignore_punctuation); @@ -566,11 +573,18 @@ mod test { async fn test_visual_word_object(cx: &mut gpui::TestAppContext) { let mut cx = NeovimBackedTestContext::new(cx).await; - cx.set_shared_state("The quick ˇbrown\nfox").await; + /* + cx.set_shared_state("The quick ˇbrown\nfox").await; + cx.simulate_shared_keystrokes(["v"]).await; + cx.assert_shared_state("The quick «bˇ»rown\nfox").await; + cx.simulate_shared_keystrokes(["i", "w"]).await; + cx.assert_shared_state("The quick «brownˇ»\nfox").await; + */ + cx.set_shared_state("The quick brown\nˇ\nfox").await; cx.simulate_shared_keystrokes(["v"]).await; - cx.assert_shared_state("The quick «bˇ»rown\nfox").await; + cx.assert_shared_state("The quick brown\n«\nˇ»fox").await; cx.simulate_shared_keystrokes(["i", "w"]).await; - cx.assert_shared_state("The quick «brownˇ»\nfox").await; + cx.assert_shared_state("The quick brown\n«\nˇ»fox").await; cx.assert_binding_matches_all(["v", "i", "w"], WORD_LOCATIONS) .await; diff --git a/crates/vim/src/test.rs b/crates/vim/src/test.rs index 88fa375851..c6a212d77f 100644 --- a/crates/vim/src/test.rs +++ b/crates/vim/src/test.rs @@ -431,6 +431,24 @@ async fn test_wrapped_lines(cx: &mut gpui::TestAppContext) { twelve char "}) .await; + + // line wraps as: + // fourteen ch + // ar + // fourteen ch + // ar + cx.set_shared_state(indoc! { " + fourteen chaˇr + fourteen char + "}) + .await; + + cx.simulate_shared_keystrokes(["d", "i", "w"]).await; + cx.assert_shared_state(indoc! {" + fourteenˇ• + fourteen char + "}) + .await; } #[gpui::test] diff --git a/crates/vim/src/test/neovim_backed_test_context.rs b/crates/vim/src/test/neovim_backed_test_context.rs index d04b1b7768..b433a6bfc0 100644 --- a/crates/vim/src/test/neovim_backed_test_context.rs +++ b/crates/vim/src/test/neovim_backed_test_context.rs @@ -153,6 +153,7 @@ impl<'a> NeovimBackedTestContext<'a> { } pub async fn assert_shared_state(&mut self, marked_text: &str) { + let marked_text = marked_text.replace("•", " "); let neovim = self.neovim_state().await; let editor = self.editor_state(); if neovim == marked_text && neovim == editor { @@ -184,9 +185,9 @@ impl<'a> NeovimBackedTestContext<'a> { message, initial_state, self.recent_keystrokes.join(" "), - marked_text, - neovim, - editor + marked_text.replace(" \n", "•\n"), + neovim.replace(" \n", "•\n"), + editor.replace(" \n", "•\n") ) } diff --git a/crates/vim/test_data/test_end_of_word.json b/crates/vim/test_data/test_end_of_word.json new file mode 100644 index 0000000000..06f80dc245 --- /dev/null +++ b/crates/vim/test_data/test_end_of_word.json @@ -0,0 +1,32 @@ +{"Put":{"state":"Thˇe quick-brown\n\n\nfox_jumps over\nthe"}} +{"Key":"e"} +{"Get":{"state":"The quicˇk-brown\n\n\nfox_jumps over\nthe","mode":"Normal"}} +{"Key":"e"} +{"Get":{"state":"The quickˇ-brown\n\n\nfox_jumps over\nthe","mode":"Normal"}} +{"Key":"e"} +{"Get":{"state":"The quick-browˇn\n\n\nfox_jumps over\nthe","mode":"Normal"}} +{"Key":"e"} +{"Get":{"state":"The quick-brown\n\n\nfox_jumpˇs over\nthe","mode":"Normal"}} +{"Key":"e"} +{"Get":{"state":"The quick-brown\n\n\nfox_jumps oveˇr\nthe","mode":"Normal"}} +{"Key":"e"} +{"Get":{"state":"The quick-brown\n\n\nfox_jumps over\nthˇe","mode":"Normal"}} +{"Key":"e"} +{"Get":{"state":"The quick-brown\n\n\nfox_jumps over\nthˇe","mode":"Normal"}} +{"Put":{"state":"Thˇe quick-brown\n\n\nfox_jumps over\nthe"}} +{"Key":"shift-e"} +{"Get":{"state":"The quick-browˇn\n\n\nfox_jumps over\nthe","mode":"Normal"}} +{"Put":{"state":"The quicˇk-brown\n\n\nfox_jumps over\nthe"}} +{"Key":"shift-e"} +{"Get":{"state":"The quick-browˇn\n\n\nfox_jumps over\nthe","mode":"Normal"}} +{"Put":{"state":"The quickˇ-brown\n\n\nfox_jumps over\nthe"}} +{"Key":"shift-e"} +{"Get":{"state":"The quick-browˇn\n\n\nfox_jumps over\nthe","mode":"Normal"}} +{"Key":"shift-e"} +{"Get":{"state":"The quick-brown\n\n\nfox_jumpˇs over\nthe","mode":"Normal"}} +{"Key":"shift-e"} +{"Get":{"state":"The quick-brown\n\n\nfox_jumps oveˇr\nthe","mode":"Normal"}} +{"Key":"shift-e"} +{"Get":{"state":"The quick-brown\n\n\nfox_jumps over\nthˇe","mode":"Normal"}} +{"Key":"shift-e"} +{"Get":{"state":"The quick-brown\n\n\nfox_jumps over\nthˇe","mode":"Normal"}} diff --git a/crates/vim/test_data/test_visual_word_object.json b/crates/vim/test_data/test_visual_word_object.json index 0041baf969..5e1a9839e9 100644 --- a/crates/vim/test_data/test_visual_word_object.json +++ b/crates/vim/test_data/test_visual_word_object.json @@ -1,9 +1,9 @@ -{"Put":{"state":"The quick ˇbrown\nfox"}} +{"Put":{"state":"The quick brown\nˇ\nfox"}} {"Key":"v"} -{"Get":{"state":"The quick «bˇ»rown\nfox","mode":"Visual"}} +{"Get":{"state":"The quick brown\n«\nˇ»fox","mode":"Visual"}} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick «brownˇ»\nfox","mode":"Visual"}} +{"Get":{"state":"The quick brown\n«\nˇ»fox","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"} diff --git a/crates/vim/test_data/test_wrapped_lines.json b/crates/vim/test_data/test_wrapped_lines.json index 1ebbd4f205..1fbfc935d9 100644 --- a/crates/vim/test_data/test_wrapped_lines.json +++ b/crates/vim/test_data/test_wrapped_lines.json @@ -48,3 +48,8 @@ {"Key":"o"} {"Key":"escape"} {"Get":{"state":"twelve char\nˇo\ntwelve char twelve char\ntwelve char\n","mode":"Normal"}} +{"Put":{"state":"fourteen chaˇr\nfourteen char\n"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"w"} +{"Get":{"state":"fourteenˇ \nfourteen char\n","mode":"Normal"}}