Detailed changes
@@ -38,6 +38,8 @@
"^": "vim::FirstNonWhitespace",
"shift-g": "vim::EndOfDocument",
"w": "vim::NextWordStart",
+ "{": "vim::StartOfParagraph",
+ "}": "vim::EndOfParagraph",
"shift-w": [
"vim::NextWordStart",
{
@@ -5123,7 +5123,7 @@ impl Editor {
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_with(|map, selection| {
selection.collapse_to(
- movement::start_of_paragraph(map, selection.head()),
+ movement::start_of_paragraph(map, selection.head(), 1),
SelectionGoal::None,
)
});
@@ -5143,7 +5143,7 @@ impl Editor {
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_with(|map, selection| {
selection.collapse_to(
- movement::end_of_paragraph(map, selection.head()),
+ movement::end_of_paragraph(map, selection.head(), 1),
SelectionGoal::None,
)
});
@@ -5162,7 +5162,10 @@ impl Editor {
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_heads_with(|map, head, _| {
- (movement::start_of_paragraph(map, head), SelectionGoal::None)
+ (
+ movement::start_of_paragraph(map, head, 1),
+ SelectionGoal::None,
+ )
});
})
}
@@ -5179,7 +5182,10 @@ impl Editor {
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_heads_with(|map, head, _| {
- (movement::end_of_paragraph(map, head), SelectionGoal::None)
+ (
+ movement::end_of_paragraph(map, head, 1),
+ SelectionGoal::None,
+ )
});
})
}
@@ -193,7 +193,11 @@ pub fn next_subword_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPo
})
}
-pub fn start_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
+pub fn start_of_paragraph(
+ map: &DisplaySnapshot,
+ display_point: DisplayPoint,
+ mut count: usize,
+) -> DisplayPoint {
let point = display_point.to_point(map);
if point.row == 0 {
return map.max_point();
@@ -203,7 +207,11 @@ pub fn start_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) ->
for row in (0..point.row + 1).rev() {
let blank = map.buffer_snapshot.is_line_blank(row);
if found_non_blank_line && blank {
- return Point::new(row, 0).to_display_point(map);
+ if count <= 1 {
+ return Point::new(row, 0).to_display_point(map);
+ }
+ count -= 1;
+ found_non_blank_line = false;
}
found_non_blank_line |= !blank;
@@ -212,7 +220,11 @@ pub fn start_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) ->
DisplayPoint::zero()
}
-pub fn end_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
+pub fn end_of_paragraph(
+ map: &DisplaySnapshot,
+ display_point: DisplayPoint,
+ mut count: usize,
+) -> DisplayPoint {
let point = display_point.to_point(map);
if point.row == map.max_buffer_row() {
return DisplayPoint::zero();
@@ -222,7 +234,11 @@ pub fn end_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) -> D
for row in point.row..map.max_buffer_row() + 1 {
let blank = map.buffer_snapshot.is_line_blank(row);
if found_non_blank_line && blank {
- return Point::new(row, 0).to_display_point(map);
+ if count <= 1 {
+ return Point::new(row, 0).to_display_point(map);
+ }
+ count -= 1;
+ found_non_blank_line = false;
}
found_non_blank_line |= !blank;
@@ -210,6 +210,10 @@ impl<'a> EditorTestContext<'a> {
self.assert_selections(expected_selections, marked_text.to_string())
}
+ pub fn editor_state(&mut self) -> String {
+ generate_marked_text(self.buffer_text().as_str(), &self.editor_selections(), true)
+ }
+
#[track_caller]
pub fn assert_editor_background_highlights<Tag: 'static>(&mut self, marked_text: &str) {
let expected_ranges = self.ranges(marked_text);
@@ -248,14 +252,8 @@ impl<'a> EditorTestContext<'a> {
self.assert_selections(expected_selections, expected_marked_text)
}
- #[track_caller]
- fn assert_selections(
- &mut self,
- expected_selections: Vec<Range<usize>>,
- expected_marked_text: String,
- ) {
- let actual_selections = self
- .editor
+ fn editor_selections(&self) -> Vec<Range<usize>> {
+ self.editor
.read_with(self.cx, |editor, cx| editor.selections.all::<usize>(cx))
.into_iter()
.map(|s| {
@@ -265,12 +263,22 @@ impl<'a> EditorTestContext<'a> {
s.start..s.end
}
})
- .collect::<Vec<_>>();
+ .collect::<Vec<_>>()
+ }
+
+ #[track_caller]
+ fn assert_selections(
+ &mut self,
+ expected_selections: Vec<Range<usize>>,
+ expected_marked_text: String,
+ ) {
+ let actual_selections = self.editor_selections();
let actual_marked_text =
generate_marked_text(&self.buffer_text(), &actual_selections, true);
if expected_selections != actual_selections {
panic!(
indoc! {"
+
{}Editor has unexpected selections.
Expected selections:
@@ -31,6 +31,8 @@ pub enum Motion {
CurrentLine,
StartOfLine,
EndOfLine,
+ StartOfParagraph,
+ EndOfParagraph,
StartOfDocument,
EndOfDocument,
Matching,
@@ -72,6 +74,8 @@ actions!(
StartOfLine,
EndOfLine,
CurrentLine,
+ StartOfParagraph,
+ EndOfParagraph,
StartOfDocument,
EndOfDocument,
Matching,
@@ -92,6 +96,12 @@ pub fn init(cx: &mut AppContext) {
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)
+ });
+ cx.add_action(|_: &mut Workspace, _: &EndOfParagraph, cx: _| {
+ motion(Motion::EndOfParagraph, cx)
+ });
cx.add_action(|_: &mut Workspace, _: &StartOfDocument, cx: _| {
motion(Motion::StartOfDocument, cx)
});
@@ -142,7 +152,8 @@ impl Motion {
pub fn linewise(&self) -> bool {
use Motion::*;
match self {
- Down | Up | StartOfDocument | EndOfDocument | CurrentLine | NextLineStart => true,
+ Down | Up | StartOfDocument | EndOfDocument | CurrentLine | NextLineStart
+ | StartOfParagraph | EndOfParagraph => true,
EndOfLine
| NextWordEnd { .. }
| Matching
@@ -172,6 +183,8 @@ impl Motion {
| Backspace
| Right
| StartOfLine
+ | StartOfParagraph
+ | EndOfParagraph
| NextWordStart { .. }
| PreviousWordStart { .. }
| FirstNonWhitespace
@@ -197,6 +210,8 @@ impl Motion {
| Backspace
| Right
| StartOfLine
+ | StartOfParagraph
+ | EndOfParagraph
| NextWordStart { .. }
| PreviousWordStart { .. }
| FirstNonWhitespace
@@ -235,6 +250,14 @@ impl Motion {
FirstNonWhitespace => (first_non_whitespace(map, point), SelectionGoal::None),
StartOfLine => (start_of_line(map, point), SelectionGoal::None),
EndOfLine => (end_of_line(map, point), SelectionGoal::None),
+ StartOfParagraph => (
+ movement::start_of_paragraph(map, point, times),
+ SelectionGoal::None,
+ ),
+ EndOfParagraph => (
+ map.clip_at_line_end(movement::end_of_paragraph(map, point, times)),
+ SelectionGoal::None,
+ ),
CurrentLine => (end_of_line(map, point), SelectionGoal::None),
StartOfDocument => (start_of_document(map, point, times), SelectionGoal::None),
EndOfDocument => (
@@ -502,10 +525,13 @@ fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint
if line_end == point {
line_end = map.max_point().to_point(map);
}
- line_end.column = line_end.column.saturating_sub(1);
let line_range = map.prev_line_boundary(point).0..line_end;
- let ranges = map.buffer_snapshot.bracket_ranges(line_range.clone());
+ let visible_line_range =
+ line_range.start..Point::new(line_range.end.row, line_range.end.column.saturating_sub(1));
+ let ranges = map
+ .buffer_snapshot
+ .bracket_ranges(visible_line_range.clone());
if let Some(ranges) = ranges {
let line_range = line_range.start.to_offset(&map.buffer_snapshot)
..line_range.end.to_offset(&map.buffer_snapshot);
@@ -590,3 +616,131 @@ fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) ->
let new_row = (point.row() + times as u32).min(map.max_buffer_row());
map.clip_point(DisplayPoint::new(new_row, 0), Bias::Left)
}
+
+#[cfg(test)]
+
+mod test {
+
+ use crate::test::NeovimBackedTestContext;
+ use indoc::indoc;
+
+ #[gpui::test]
+ async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) {
+ let mut cx = NeovimBackedTestContext::new(cx).await;
+
+ let initial_state = indoc! {r"ˇabc
+ def
+
+ paragraph
+ the second
+
+
+
+ third and
+ final"};
+
+ // goes down once
+ cx.set_shared_state(initial_state).await;
+ cx.simulate_shared_keystrokes(["}"]).await;
+ cx.assert_shared_state(indoc! {r"abc
+ def
+ ˇ
+ paragraph
+ the second
+
+
+
+ third and
+ final"})
+ .await;
+
+ // goes up once
+ cx.simulate_shared_keystrokes(["{"]).await;
+ cx.assert_shared_state(initial_state).await;
+
+ // goes down twice
+ cx.simulate_shared_keystrokes(["2", "}"]).await;
+ cx.assert_shared_state(indoc! {r"abc
+ def
+
+ paragraph
+ the second
+ ˇ
+
+
+ third and
+ final"})
+ .await;
+
+ // goes down over multiple blanks
+ cx.simulate_shared_keystrokes(["}"]).await;
+ cx.assert_shared_state(indoc! {r"abc
+ def
+
+ paragraph
+ the second
+
+
+
+ third and
+ finaˇl"})
+ .await;
+
+ // goes up twice
+ cx.simulate_shared_keystrokes(["2", "{"]).await;
+ cx.assert_shared_state(indoc! {r"abc
+ def
+ ˇ
+ paragraph
+ the second
+
+
+
+ third and
+ final"})
+ .await
+ }
+
+ #[gpui::test]
+ async fn test_matching(cx: &mut gpui::TestAppContext) {
+ let mut cx = NeovimBackedTestContext::new(cx).await;
+
+ cx.set_shared_state(indoc! {r"func ˇ(a string) {
+ do(something(with<Types>.and_arrays[0, 2]))
+ }"})
+ .await;
+ cx.simulate_shared_keystrokes(["%"]).await;
+ cx.assert_shared_state(indoc! {r"func (a stringˇ) {
+ do(something(with<Types>.and_arrays[0, 2]))
+ }"})
+ .await;
+
+ // test it works on the last character of the line
+ cx.set_shared_state(indoc! {r"func (a string) ˇ{
+ do(something(with<Types>.and_arrays[0, 2]))
+ }"})
+ .await;
+ cx.simulate_shared_keystrokes(["%"]).await;
+ cx.assert_shared_state(indoc! {r"func (a string) {
+ do(something(with<Types>.and_arrays[0, 2]))
+ ˇ}"})
+ .await;
+
+ // test it works on immediate nesting
+ cx.set_shared_state("ˇ{()}").await;
+ cx.simulate_shared_keystrokes(["%"]).await;
+ cx.assert_shared_state("{()ˇ}").await;
+ cx.simulate_shared_keystrokes(["%"]).await;
+ cx.assert_shared_state("ˇ{()}").await;
+
+ // test it works on immediate nesting inside braces
+ cx.set_shared_state("{\n ˇ{()}\n}").await;
+ cx.simulate_shared_keystrokes(["%"]).await;
+ cx.assert_shared_state("{\n {()ˇ}\n}").await;
+
+ // test it jumps to the next paren on a line
+ cx.set_shared_state("func ˇboop() {\n}").await;
+ cx.simulate_shared_keystrokes(["%"]).await;
+ cx.assert_shared_state("func boop(ˇ) {\n}").await;
+ }
+}
@@ -1,29 +1,51 @@
+use editor::scroll::autoscroll::Autoscroll;
use gpui::ViewContext;
-use language::Point;
+use language::{Bias, Point};
use workspace::Workspace;
-use crate::{motion::Motion, normal::ChangeCase, Vim};
+use crate::{normal::ChangeCase, state::Mode, Vim};
pub fn change_case(_: &mut Workspace, _: &ChangeCase, cx: &mut ViewContext<Workspace>) {
Vim::update(cx, |vim, cx| {
- let count = vim.pop_number_operator(cx);
+ let count = vim.pop_number_operator(cx).unwrap_or(1) as u32;
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| {
- if selection.start == selection.end {
- Motion::Right.expand_selection(map, selection, count, true);
+ let mut ranges = Vec::new();
+ let mut cursor_positions = Vec::new();
+ let snapshot = editor.buffer().read(cx).snapshot(cx);
+ for selection in editor.selections.all::<Point>(cx) {
+ match vim.state.mode {
+ Mode::Visual { line: true } => {
+ 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 } => {
+ ranges.push(selection.start..selection.end);
+ cursor_positions.push(selection.start..selection.start);
+ }
+ Mode::Insert | Mode::Normal => {
+ let start = selection.start;
+ let mut end = start;
+ for _ in 0..count {
+ end = snapshot.clip_point(end + Point::new(0, 1), Bias::Right);
}
- })
- });
- let selections = editor.selections.all::<Point>(cx);
- for selection in selections.into_iter().rev() {
+ ranges.push(start..end);
+
+ if end.column == snapshot.line_len(end.row) {
+ end = snapshot.clip_point(end - Point::new(0, 1), Bias::Left);
+ }
+ cursor_positions.push(end..end)
+ }
+ }
+ }
+ editor.transact(cx, |editor, cx| {
+ for range in ranges.into_iter().rev() {
let snapshot = editor.buffer().read(cx).snapshot(cx);
editor.buffer().update(cx, |buffer, cx| {
- let range = selection.start..selection.end;
let text = snapshot
- .text_for_range(selection.start..selection.end)
+ .text_for_range(range.start..range.end)
.flat_map(|s| s.chars())
.flat_map(|c| {
if c.is_lowercase() {
@@ -37,28 +59,46 @@ pub fn change_case(_: &mut Workspace, _: &ChangeCase, cx: &mut ViewContext<Works
buffer.edit([(range, text)], None, cx)
})
}
+ editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
+ s.select_ranges(cursor_positions)
+ })
});
- editor.set_clip_at_line_ends(true, cx);
});
+ vim.switch_mode(Mode::Normal, true, cx)
})
}
-
#[cfg(test)]
mod test {
- use crate::{state::Mode, test::VimTestContext};
- use indoc::indoc;
+ use crate::{state::Mode, test::NeovimBackedTestContext};
#[gpui::test]
async fn test_change_case(cx: &mut gpui::TestAppContext) {
- let mut cx = VimTestContext::new(cx, true).await;
- cx.set_state(indoc! {"ˇabC\n"}, Mode::Normal);
- cx.simulate_keystrokes(["~"]);
- cx.assert_editor_state("AˇbC\n");
- cx.simulate_keystrokes(["2", "~"]);
- cx.assert_editor_state("ABcˇ\n");
+ let mut cx = NeovimBackedTestContext::new(cx).await;
+ cx.set_shared_state("ˇabC\n").await;
+ cx.simulate_shared_keystrokes(["~"]).await;
+ cx.assert_shared_state("AˇbC\n").await;
+ cx.simulate_shared_keystrokes(["2", "~"]).await;
+ cx.assert_shared_state("ABˇc\n").await;
+
+ // works in visual mode
+ cx.set_shared_state("a😀C«dÉ1*fˇ»\n").await;
+ cx.simulate_shared_keystrokes(["~"]).await;
+ cx.assert_shared_state("a😀CˇDé1*F\n").await;
+
+ // works with multibyte characters
+ cx.simulate_shared_keystrokes(["~"]).await;
+ cx.set_shared_state("aˇC😀é1*F\n").await;
+ cx.simulate_shared_keystrokes(["4", "~"]).await;
+ cx.assert_shared_state("ac😀É1ˇ*F\n").await;
+
+ // works with line selections
+ cx.set_shared_state("abˇC\n").await;
+ cx.simulate_shared_keystrokes(["shift-v", "~"]).await;
+ cx.assert_shared_state("ˇABc\n").await;
- cx.set_state(indoc! {"a😀C«dÉ1*fˇ»\n"}, Mode::Normal);
- cx.simulate_keystrokes(["~"]);
- cx.assert_editor_state("a😀CDé1*Fˇ\n");
+ // works with multiple cursors (zed only)
+ cx.set_state("aˇßcdˇe\n", Mode::Normal);
+ cx.simulate_keystroke("~");
+ cx.assert_state("aSSˇcdˇE\n", Mode::Normal);
}
}
@@ -1,9 +1,10 @@
-use std::ops::{Deref, DerefMut};
+use indoc::indoc;
+use std::ops::{Deref, DerefMut, Range};
use collections::{HashMap, HashSet};
use gpui::ContextHandle;
use language::OffsetRangeExt;
-use util::test::marked_text_offsets;
+use util::test::{generate_marked_text, marked_text_offsets};
use super::{neovim_connection::NeovimConnection, NeovimBackedBindingTestContext, VimTestContext};
use crate::state::Mode;
@@ -112,6 +113,43 @@ impl<'a> NeovimBackedTestContext<'a> {
context_handle
}
+ pub async fn assert_shared_state(&mut self, marked_text: &str) {
+ let neovim = self.neovim_state().await;
+ if neovim != marked_text {
+ panic!(
+ indoc! {"Test is incorrect (currently expected != neovim state)
+
+ # currently expected:
+ {}
+ # neovim state:
+ {}
+ # zed state:
+ {}"},
+ marked_text,
+ neovim,
+ self.editor_state(),
+ )
+ }
+ self.assert_editor_state(marked_text)
+ }
+
+ pub async fn neovim_state(&mut self) -> String {
+ generate_marked_text(
+ self.neovim.text().await.as_str(),
+ &vec![self.neovim_selection().await],
+ true,
+ )
+ }
+
+ async fn neovim_selection(&mut self) -> Range<usize> {
+ let mut neovim_selection = self.neovim.selection().await;
+ // Zed selections adjust themselves to make the end point visually make sense
+ if neovim_selection.start > neovim_selection.end {
+ neovim_selection.start.column += 1;
+ }
+ neovim_selection.to_offset(&self.buffer_snapshot())
+ }
+
pub async fn assert_state_matches(&mut self) {
assert_eq!(
self.neovim.text().await,
@@ -120,13 +158,8 @@ impl<'a> NeovimBackedTestContext<'a> {
self.assertion_context()
);
- let mut neovim_selection = self.neovim.selection().await;
- // Zed selections adjust themselves to make the end point visually make sense
- if neovim_selection.start > neovim_selection.end {
- neovim_selection.start.column += 1;
- }
- let neovim_selection = neovim_selection.to_offset(&self.buffer_snapshot());
- self.assert_editor_selections(vec![neovim_selection]);
+ let selections = vec![self.neovim_selection().await];
+ self.assert_editor_selections(selections);
if let Some(neovim_mode) = self.neovim.mode().await {
assert_eq!(neovim_mode, self.mode(), "{}", self.assertion_context(),);
@@ -167,15 +167,25 @@ impl NeovimConnection {
.await
.expect("Could not get neovim window");
- if !selection.is_empty() {
- panic!("Setting neovim state with non empty selection not yet supported");
- }
let cursor = selection.start;
nvim_window
.set_cursor((cursor.row as i64 + 1, cursor.column as i64))
.await
.expect("Could not set nvim cursor position");
+ if !selection.is_empty() {
+ self.nvim
+ .input("v")
+ .await
+ .expect("could not enter visual mode");
+
+ let cursor = selection.end;
+ nvim_window
+ .set_cursor((cursor.row as i64 + 1, cursor.column as i64))
+ .await
+ .expect("Could not set nvim cursor position");
+ }
+
if let Some(NeovimData::Get { mode, state }) = self.data.back() {
if *mode == Some(Mode::Normal) && *state == marked_text {
return;
@@ -0,0 +1,18 @@
+{"Put":{"state":"ˇabC\n"}}
+{"Key":"~"}
+{"Get":{"state":"AˇbC\n","mode":"Normal"}}
+{"Key":"2"}
+{"Key":"~"}
+{"Get":{"state":"ABˇc\n","mode":"Normal"}}
+{"Put":{"state":"a😀C«dÉ1*fˇ»\n"}}
+{"Key":"~"}
+{"Get":{"state":"a😀CˇDé1*F\n","mode":"Normal"}}
+{"Key":"~"}
+{"Put":{"state":"aˇC😀é1*F\n"}}
+{"Key":"4"}
+{"Key":"~"}
+{"Get":{"state":"ac😀É1ˇ*F\n","mode":"Normal"}}
+{"Put":{"state":"abˇC\n"}}
+{"Key":"shift-v"}
+{"Key":"~"}
+{"Get":{"state":"ˇABc\n","mode":"Normal"}}
@@ -0,0 +1,17 @@
+{"Put":{"state":"func ˇ(a string) {\n do(something(with<Types>.and_arrays[0, 2]))\n}"}}
+{"Key":"%"}
+{"Get":{"state":"func (a stringˇ) {\n do(something(with<Types>.and_arrays[0, 2]))\n}","mode":"Normal"}}
+{"Put":{"state":"func (a string) ˇ{\ndo(something(with<Types>.and_arrays[0, 2]))\n}"}}
+{"Key":"%"}
+{"Get":{"state":"func (a string) {\ndo(something(with<Types>.and_arrays[0, 2]))\nˇ}","mode":"Normal"}}
+{"Put":{"state":"ˇ{()}"}}
+{"Key":"%"}
+{"Get":{"state":"{()ˇ}","mode":"Normal"}}
+{"Key":"%"}
+{"Get":{"state":"ˇ{()}","mode":"Normal"}}
+{"Put":{"state":"{\n ˇ{()}\n}"}}
+{"Key":"%"}
+{"Get":{"state":"{\n {()ˇ}\n}","mode":"Normal"}}
+{"Put":{"state":"func ˇboop() {\n}"}}
+{"Key":"%"}
+{"Get":{"state":"func boop(ˇ) {\n}","mode":"Normal"}}
@@ -0,0 +1,13 @@
+{"Put":{"state":"ˇabc\ndef\n\nparagraph\nthe second\n\n\n\nthird and\nfinal"}}
+{"Key":"}"}
+{"Get":{"state":"abc\ndef\nˇ\nparagraph\nthe second\n\n\n\nthird and\nfinal","mode":"Normal"}}
+{"Key":"{"}
+{"Get":{"state":"ˇabc\ndef\n\nparagraph\nthe second\n\n\n\nthird and\nfinal","mode":"Normal"}}
+{"Key":"2"}
+{"Key":"}"}
+{"Get":{"state":"abc\ndef\n\nparagraph\nthe second\nˇ\n\n\nthird and\nfinal","mode":"Normal"}}
+{"Key":"}"}
+{"Get":{"state":"abc\ndef\n\nparagraph\nthe second\n\n\n\nthird and\nfinaˇl","mode":"Normal"}}
+{"Key":"2"}
+{"Key":"{"}
+{"Get":{"state":"abc\ndef\nˇ\nparagraph\nthe second\n\n\n\nthird and\nfinal","mode":"Normal"}}