Merge pull request #1685 from zed-industries/vim-text-objects

Vim Text Objects and Numeric Repitions
This commit is contained in:
Kay Simmons authored and GitHub committed 2022-10-11 16:36:19 -07:00
commit 394517dbc6
79 files changed
+3861 -2462

No files matched your search

+89 -10
View File
@@ -9,11 +9,10 @@
}
],
"h": "vim::Left",
"backspace": "vim::Left",
"backspace": "vim::Backspace",
"j": "vim::Down",
"k": "vim::Up",
"l": "vim::Right",
"0": "vim::StartOfLine",
"$": "vim::EndOfLine",
"shift-g": "vim::EndOfDocument",
"w": "vim::NextWordStart",
@@ -38,7 +37,60 @@
}
],
"%": "vim::Matching",
"escape": "editor::Cancel"
"escape": "editor::Cancel",
"i": [
"vim::PushOperator",
{
"Object": {
"around": false
}
}
],
"a": [
"vim::PushOperator",
{
"Object": {
"around": true
}
}
],
"0": "vim::StartOfLine", // When no number operator present, use start of line motion
"1": [
"vim::Number",
1
],
"2": [
"vim::Number",
2
],
"3": [
"vim::Number",
3
],
"4": [
"vim::Number",
4
],
"5": [
"vim::Number",
5
],
"6": [
"vim::Number",
6
],
"7": [
"vim::Number",
7
],
"8": [
"vim::Number",
8
],
"9": [
"vim::Number",
9
]
}
},
{
@@ -98,6 +150,15 @@
]
}
},
{
"context": "Editor && vim_operator == n",
"bindings": {
"0": [
"vim::Number",
0
]
}
},
{
"context": "Editor && vim_operator == g",
"bindings": {
@@ -112,13 +173,6 @@
{
"context": "Editor && vim_operator == c",
"bindings": {
"w": "vim::ChangeWord",
"shift-w": [
"vim::ChangeWord",
{
"ignorePunctuation": true
}
],
"c": "vim::CurrentLine"
}
},
@@ -134,9 +188,34 @@
"y": "vim::CurrentLine"
}
},
{
"context": "Editor && VimObject",
"bindings": {
"w": "vim::Word",
"shift-w": [
"vim::Word",
{
"ignorePunctuation": true
}
],
"s": "vim::Sentence",
"'": "vim::Quotes",
"`": "vim::BackQuotes",
"\"": "vim::DoubleQuotes",
"(": "vim::Parentheses",
")": "vim::Parentheses",
"[": "vim::SquareBrackets",
"]": "vim::SquareBrackets",
"{": "vim::CurlyBrackets",
"}": "vim::CurlyBrackets",
"<": "vim::AngleBrackets",
">": "vim::AngleBrackets"
}
},
{
"context": "Editor && vim_mode == visual",
"bindings": {
"u": "editor::Undo",
"c": "vim::VisualChange",
"d": "vim::VisualDelete",
"x": "vim::VisualDelete",
+2
View File
@@ -5523,6 +5523,7 @@ async fn test_random_collaboration(
cx.font_cache(),
cx.leak_detector(),
next_entity_id,
cx.function_name.clone(),
);
let host = server.create_client(&mut host_cx, "host").await;
let host_project = host_cx.update(|cx| {
@@ -5764,6 +5765,7 @@ async fn test_random_collaboration(
cx.font_cache(),
cx.leak_detector(),
next_entity_id,
cx.function_name.clone(),
);
deterministic.start_waiting();
+71 -14
View File
@@ -330,34 +330,91 @@ impl DisplaySnapshot {
DisplayPoint(self.blocks_snapshot.max_point())
}
/// Returns text chunks starting at the given display row until the end of the file
pub fn text_chunks(&self, display_row: u32) -> impl Iterator<Item = &str> {
self.blocks_snapshot
.chunks(display_row..self.max_point().row() + 1, false, None)
.map(|h| h.text)
}
// Returns text chunks starting at the end of the given display row in reverse until the start of the file
pub fn reverse_text_chunks(&self, display_row: u32) -> impl Iterator<Item = &str> {
(0..=display_row).into_iter().rev().flat_map(|row| {
self.blocks_snapshot
.chunks(row..row + 1, false, None)
.map(|h| h.text)
.collect::<Vec<_>>()
.into_iter()
.rev()
})
}
pub fn chunks(&self, display_rows: Range<u32>, language_aware: bool) -> DisplayChunks<'_> {
self.blocks_snapshot
.chunks(display_rows, language_aware, Some(&self.text_highlights))
}
pub fn chars_at(&self, point: DisplayPoint) -> impl Iterator<Item = char> + '_ {
let mut column = 0;
let mut chars = self.text_chunks(point.row()).flat_map(str::chars);
while column < point.column() {
if let Some(c) = chars.next() {
column += c.len_utf8() as u32;
} else {
break;
}
}
chars
pub fn chars_at(
&self,
mut point: DisplayPoint,
) -> impl Iterator<Item = (char, DisplayPoint)> + '_ {
point = DisplayPoint(self.blocks_snapshot.clip_point(point.0, Bias::Left));
self.text_chunks(point.row())
.flat_map(str::chars)
.skip_while({
let mut column = 0;
move |char| {
let at_point = column >= point.column();
column += char.len_utf8() as u32;
!at_point
}
})
.map(move |ch| {
let result = (ch, point);
if ch == '\n' {
*point.row_mut() += 1;
*point.column_mut() = 0;
} else {
*point.column_mut() += ch.len_utf8() as u32;
}
result
})
}
pub fn reverse_chars_at(
&self,
mut point: DisplayPoint,
) -> impl Iterator<Item = (char, DisplayPoint)> + '_ {
point = DisplayPoint(self.blocks_snapshot.clip_point(point.0, Bias::Left));
self.reverse_text_chunks(point.row())
.flat_map(|chunk| chunk.chars().rev())
.skip_while({
let mut column = self.line_len(point.row());
if self.max_point().row() > point.row() {
column += 1;
}
move |char| {
let at_point = column <= point.column();
column = column.saturating_sub(char.len_utf8() as u32);
!at_point
}
})
.map(move |ch| {
if ch == '\n' {
*point.row_mut() -= 1;
*point.column_mut() = self.line_len(point.row());
} else {
*point.column_mut() = point.column().saturating_sub(ch.len_utf8() as u32);
}
(ch, point)
})
}
pub fn column_to_chars(&self, display_row: u32, target: u32) -> u32 {
let mut count = 0;
let mut column = 0;
for c in self.chars_at(DisplayPoint::new(display_row, 0)) {
for (c, _) in self.chars_at(DisplayPoint::new(display_row, 0)) {
if column >= target {
break;
}
@@ -370,7 +427,7 @@ impl DisplaySnapshot {
pub fn column_from_chars(&self, display_row: u32, char_count: u32) -> u32 {
let mut column = 0;
for (count, c) in self.chars_at(DisplayPoint::new(display_row, 0)).enumerate() {
for (count, (c, _)) in self.chars_at(DisplayPoint::new(display_row, 0)).enumerate() {
if c == '\n' || count >= char_count as usize {
break;
}
@@ -454,7 +511,7 @@ impl DisplaySnapshot {
pub fn line_indent(&self, display_row: u32) -> (u32, bool) {
let mut indent = 0;
let mut is_blank = true;
for c in self.chars_at(DisplayPoint::new(display_row, 0)) {
for (c, _) in self.chars_at(DisplayPoint::new(display_row, 0)) {
if c == ' ' {
indent += 1;
} else {
+2 -2
View File
@@ -4095,7 +4095,7 @@ impl Editor {
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
s.move_cursors_with(|map, head, _| {
(
movement::line_beginning(map, head, true),
movement::indented_line_beginning(map, head, true),
SelectionGoal::None,
)
});
@@ -4110,7 +4110,7 @@ impl Editor {
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
s.move_heads_with(|map, head, _| {
(
movement::line_beginning(map, head, action.stop_at_soft_wraps),
movement::indented_line_beginning(map, head, action.stop_at_soft_wraps),
SelectionGoal::None,
)
});
+8 -6
View File
@@ -1,20 +1,22 @@
use std::{cell::RefCell, rc::Rc, time::Instant};
use futures::StreamExt;
use indoc::indoc;
use unindent::Unindent;
use super::*;
use crate::test::{
assert_text_with_selections, build_editor, select_ranges, EditorLspTestContext,
EditorTestContext,
assert_text_with_selections, build_editor, editor_lsp_test_context::EditorLspTestContext,
editor_test_context::EditorTestContext, select_ranges,
};
use futures::StreamExt;
use gpui::{
geometry::rect::RectF,
platform::{WindowBounds, WindowOptions},
};
use indoc::indoc;
use language::{FakeLspAdapter, LanguageConfig, LanguageRegistry};
use project::FakeFs;
use settings::EditorSettings;
use std::{cell::RefCell, rc::Rc, time::Instant};
use text::Point;
use unindent::Unindent;
use util::{
assert_set_eq,
test::{marked_text_ranges, marked_text_ranges_by, sample_text, TextRangeMarker},
+1 -1
View File
@@ -751,7 +751,7 @@ impl EditorElement {
.snapshot
.chars_at(cursor_position)
.next()
.and_then(|character| {
.and_then(|(character, _)| {
let font_id =
cursor_row_layout.font_for_index(cursor_column)?;
let text = character.to_string();
@@ -32,8 +32,9 @@ pub fn refresh_matching_bracket_highlights(editor: &mut Editor, cx: &mut ViewCon
#[cfg(test)]
mod tests {
use crate::test::editor_lsp_test_context::EditorLspTestContext;
use super::*;
use crate::test::EditorLspTestContext;
use indoc::indoc;
use language::{BracketPair, Language, LanguageConfig};
+2 -2
View File
@@ -427,13 +427,13 @@ impl DiagnosticPopover {
#[cfg(test)]
mod tests {
use futures::StreamExt;
use indoc::indoc;
use language::{Diagnostic, DiagnosticSet};
use project::HoverBlock;
use smol::stream::StreamExt;
use crate::test::EditorLspTestContext;
use crate::test::editor_lsp_test_context::EditorLspTestContext;
use super::*;
+1 -1
View File
@@ -400,7 +400,7 @@ mod tests {
use indoc::indoc;
use lsp::request::{GotoDefinition, GotoTypeDefinition};
use crate::test::EditorLspTestContext;
use crate::test::editor_lsp_test_context::EditorLspTestContext;
use super::*;
+2 -1
View File
@@ -70,8 +70,9 @@ pub fn deploy_context_menu(
#[cfg(test)]
mod tests {
use crate::test::editor_lsp_test_context::EditorLspTestContext;
use super::*;
use crate::test::EditorLspTestContext;
use indoc::indoc;
#[gpui::test]
+110 -47
View File
@@ -101,6 +101,22 @@ pub fn line_beginning(
map: &DisplaySnapshot,
display_point: DisplayPoint,
stop_at_soft_boundaries: bool,
) -> DisplayPoint {
let point = display_point.to_point(map);
let soft_line_start = map.clip_point(DisplayPoint::new(display_point.row(), 0), Bias::Right);
let line_start = map.prev_line_boundary(point).1;
if stop_at_soft_boundaries && display_point != soft_line_start {
soft_line_start
} else {
line_start
}
}
pub fn indented_line_beginning(
map: &DisplaySnapshot,
display_point: DisplayPoint,
stop_at_soft_boundaries: bool,
) -> DisplayPoint {
let point = display_point.to_point(map);
let soft_line_start = map.clip_point(DisplayPoint::new(display_point.row(), 0), Bias::Right);
@@ -167,54 +183,79 @@ pub fn next_subword_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPo
})
}
/// Scans for a boundary from the start of each line preceding the given end 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 the predicate returns true multiple times
/// on a line, the *rightmost* boundary is returned.
/// 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.
pub fn find_preceding_boundary(
map: &DisplaySnapshot,
end: DisplayPoint,
from: DisplayPoint,
mut is_boundary: impl FnMut(char, char) -> bool,
) -> DisplayPoint {
let mut point = end;
loop {
*point.column_mut() = 0;
if point.row() > 0 {
if let Some(indent) = map.soft_wrap_indent(point.row() - 1) {
*point.column_mut() = indent;
let mut start_column = 0;
let mut soft_wrap_row = from.row() + 1;
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;
}
}
let mut boundary = None;
let mut prev_ch = if point.is_zero() { None } else { Some('\n') };
for ch in map.chars_at(point) {
if point >= end {
break;
}
if let Some(prev_ch) = prev_ch {
if is_boundary(prev_ch, ch) {
boundary = Some(point);
}
}
if ch == '\n' {
break;
}
prev_ch = Some(ch);
*point.column_mut() += ch.len_utf8() as u32;
// If the current point is in the soft_wrap, skip comparing it
if point.column() < start_column {
continue;
}
if let Some(boundary) = boundary {
return boundary;
} else if point.row() == 0 {
return DisplayPoint::zero();
} else {
*point.row_mut() -= 1;
if let Some((prev_ch, prev_point)) = prev {
if is_boundary(ch, prev_ch) {
return prev_point;
}
}
prev = Some((ch, point));
}
DisplayPoint::zero()
}
/// 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 prev_point;
}
}
if ch == '\n' || point.column() < start_column {
break;
}
prev = Some((ch, point));
}
prev.map(|(_, point)| point).unwrap_or(from)
}
/// Scans for a boundary following the given start point until a boundary is found, indicated by the
@@ -223,26 +264,48 @@ pub fn find_preceding_boundary(
/// or end of a line.
pub fn find_boundary(
map: &DisplaySnapshot,
mut point: DisplayPoint,
from: DisplayPoint,
mut is_boundary: impl FnMut(char, char) -> bool,
) -> DisplayPoint {
let mut prev_ch = None;
for ch in map.chars_at(point) {
for (ch, point) in map.chars_at(from) {
if let Some(prev_ch) = prev_ch {
if is_boundary(prev_ch, ch) {
break;
return map.clip_point(point, Bias::Right);
}
}
if ch == '\n' {
*point.row_mut() += 1;
*point.column_mut() = 0;
} else {
*point.column_mut() += ch.len_utf8() as u32;
}
prev_ch = Some(ch);
}
map.clip_point(point, Bias::Right)
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' {
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)
}
pub fn is_inside_word(map: &DisplaySnapshot, point: DisplayPoint) -> bool {
+8 -449
View File
@@ -1,28 +1,14 @@
pub mod editor_lsp_test_context;
pub mod editor_test_context;
use crate::{
display_map::{DisplayMap, DisplaySnapshot, ToDisplayPoint},
multi_buffer::ToPointUtf16,
AnchorRangeExt, Autoscroll, DisplayPoint, Editor, EditorMode, MultiBuffer, ToPoint,
DisplayPoint, Editor, EditorMode, MultiBuffer,
};
use anyhow::Result;
use futures::{Future, StreamExt};
use gpui::{
json, keymap::Keystroke, AppContext, ModelContext, ModelHandle, ViewContext, ViewHandle,
};
use indoc::indoc;
use language::{point_to_lsp, Buffer, BufferSnapshot, FakeLspAdapter, Language, LanguageConfig};
use lsp::{notification, request};
use project::Project;
use settings::Settings;
use std::{
any::TypeId,
ops::{Deref, DerefMut, Range},
sync::Arc,
};
use util::{
assert_set_eq, set_eq,
test::{generate_marked_text, marked_text_offsets, marked_text_ranges},
};
use workspace::{pane, AppState, Workspace, WorkspaceHandle};
use gpui::{ModelHandle, ViewContext};
use util::test::{marked_text_offsets, marked_text_ranges};
#[cfg(test)]
#[ctor::ctor]
@@ -80,430 +66,3 @@ pub(crate) fn build_editor(
) -> Editor {
Editor::new(EditorMode::Full, buffer, None, None, cx)
}
pub struct EditorTestContext<'a> {
pub cx: &'a mut gpui::TestAppContext,
pub window_id: usize,
pub editor: ViewHandle<Editor>,
}
impl<'a> EditorTestContext<'a> {
pub fn new(cx: &'a mut gpui::TestAppContext) -> EditorTestContext<'a> {
let (window_id, editor) = cx.update(|cx| {
cx.set_global(Settings::test(cx));
crate::init(cx);
let (window_id, editor) = cx.add_window(Default::default(), |cx| {
build_editor(MultiBuffer::build_simple("", cx), cx)
});
editor.update(cx, |_, cx| cx.focus_self());
(window_id, editor)
});
Self {
cx,
window_id,
editor,
}
}
pub fn condition(
&self,
predicate: impl FnMut(&Editor, &AppContext) -> bool,
) -> impl Future<Output = ()> {
self.editor.condition(self.cx, predicate)
}
pub fn editor<F, T>(&self, read: F) -> T
where
F: FnOnce(&Editor, &AppContext) -> T,
{
self.editor.read_with(self.cx, read)
}
pub fn update_editor<F, T>(&mut self, update: F) -> T
where
F: FnOnce(&mut Editor, &mut ViewContext<Editor>) -> T,
{
self.editor.update(self.cx, update)
}
pub fn multibuffer<F, T>(&self, read: F) -> T
where
F: FnOnce(&MultiBuffer, &AppContext) -> T,
{
self.editor(|editor, cx| read(editor.buffer().read(cx), cx))
}
pub fn update_multibuffer<F, T>(&mut self, update: F) -> T
where
F: FnOnce(&mut MultiBuffer, &mut ModelContext<MultiBuffer>) -> T,
{
self.update_editor(|editor, cx| editor.buffer().update(cx, update))
}
pub fn buffer_text(&self) -> String {
self.multibuffer(|buffer, cx| buffer.snapshot(cx).text())
}
pub fn buffer<F, T>(&self, read: F) -> T
where
F: FnOnce(&Buffer, &AppContext) -> T,
{
self.multibuffer(|multibuffer, cx| {
let buffer = multibuffer.as_singleton().unwrap().read(cx);
read(buffer, cx)
})
}
pub fn update_buffer<F, T>(&mut self, update: F) -> T
where
F: FnOnce(&mut Buffer, &mut ModelContext<Buffer>) -> T,
{
self.update_multibuffer(|multibuffer, cx| {
let buffer = multibuffer.as_singleton().unwrap();
buffer.update(cx, update)
})
}
pub fn buffer_snapshot(&self) -> BufferSnapshot {
self.buffer(|buffer, _| buffer.snapshot())
}
pub fn simulate_keystroke(&mut self, keystroke_text: &str) {
let keystroke = Keystroke::parse(keystroke_text).unwrap();
self.cx.dispatch_keystroke(self.window_id, keystroke, false);
}
pub fn simulate_keystrokes<const COUNT: usize>(&mut self, keystroke_texts: [&str; COUNT]) {
for keystroke_text in keystroke_texts.into_iter() {
self.simulate_keystroke(keystroke_text);
}
}
pub fn ranges(&self, marked_text: &str) -> Vec<Range<usize>> {
let (unmarked_text, ranges) = marked_text_ranges(marked_text, false);
assert_eq!(self.buffer_text(), unmarked_text);
ranges
}
pub fn display_point(&mut self, marked_text: &str) -> DisplayPoint {
let ranges = self.ranges(marked_text);
let snapshot = self
.editor
.update(self.cx, |editor, cx| editor.snapshot(cx));
ranges[0].start.to_display_point(&snapshot)
}
// Returns anchors for the current buffer using `«` and `»`
pub fn text_anchor_range(&self, marked_text: &str) -> Range<language::Anchor> {
let ranges = self.ranges(marked_text);
let snapshot = self.buffer_snapshot();
snapshot.anchor_before(ranges[0].start)..snapshot.anchor_after(ranges[0].end)
}
/// Change the editor's text and selections using a string containing
/// embedded range markers that represent the ranges and directions of
/// each selection.
///
/// See the `util::test::marked_text_ranges` function for more information.
pub fn set_state(&mut self, marked_text: &str) {
let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true);
self.editor.update(self.cx, |editor, cx| {
editor.set_text(unmarked_text, cx);
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
s.select_ranges(selection_ranges)
})
})
}
/// Make an assertion about the editor's text and the ranges and directions
/// of its selections using a string containing embedded range markers.
///
/// See the `util::test::marked_text_ranges` function for more information.
pub fn assert_editor_state(&mut self, marked_text: &str) {
let (unmarked_text, expected_selections) = marked_text_ranges(marked_text, true);
let buffer_text = self.buffer_text();
assert_eq!(
buffer_text, unmarked_text,
"Unmarked text doesn't match buffer text"
);
self.assert_selections(expected_selections, marked_text.to_string())
}
pub fn assert_editor_background_highlights<Tag: 'static>(&mut self, marked_text: &str) {
let expected_ranges = self.ranges(marked_text);
let actual_ranges: Vec<Range<usize>> = self.update_editor(|editor, cx| {
let snapshot = editor.snapshot(cx);
editor
.background_highlights
.get(&TypeId::of::<Tag>())
.map(|h| h.1.clone())
.unwrap_or_default()
.into_iter()
.map(|range| range.to_offset(&snapshot.buffer_snapshot))
.collect()
});
assert_set_eq!(actual_ranges, expected_ranges);
}
pub fn assert_editor_text_highlights<Tag: ?Sized + 'static>(&mut self, marked_text: &str) {
let expected_ranges = self.ranges(marked_text);
let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
let actual_ranges: Vec<Range<usize>> = snapshot
.highlight_ranges::<Tag>()
.map(|ranges| ranges.as_ref().clone().1)
.unwrap_or_default()
.into_iter()
.map(|range| range.to_offset(&snapshot.buffer_snapshot))
.collect();
assert_set_eq!(actual_ranges, expected_ranges);
}
pub fn assert_editor_selections(&mut self, expected_selections: Vec<Range<usize>>) {
let expected_marked_text =
generate_marked_text(&self.buffer_text(), &expected_selections, true);
self.assert_selections(expected_selections, expected_marked_text)
}
fn assert_selections(
&mut self,
expected_selections: Vec<Range<usize>>,
expected_marked_text: String,
) {
let actual_selections = self
.editor
.read_with(self.cx, |editor, cx| editor.selections.all::<usize>(cx))
.into_iter()
.map(|s| {
if s.reversed {
s.end..s.start
} else {
s.start..s.end
}
})
.collect::<Vec<_>>();
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:
{}
Actual selections:
{}
"},
expected_marked_text, actual_marked_text,
);
}
}
}
impl<'a> Deref for EditorTestContext<'a> {
type Target = gpui::TestAppContext;
fn deref(&self) -> &Self::Target {
self.cx
}
}
impl<'a> DerefMut for EditorTestContext<'a> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.cx
}
}
pub struct EditorLspTestContext<'a> {
pub cx: EditorTestContext<'a>,
pub lsp: lsp::FakeLanguageServer,
pub workspace: ViewHandle<Workspace>,
pub buffer_lsp_url: lsp::Url,
}
impl<'a> EditorLspTestContext<'a> {
pub async fn new(
mut language: Language,
capabilities: lsp::ServerCapabilities,
cx: &'a mut gpui::TestAppContext,
) -> EditorLspTestContext<'a> {
use json::json;
cx.update(|cx| {
crate::init(cx);
pane::init(cx);
});
let params = cx.update(AppState::test);
let file_name = format!(
"file.{}",
language
.path_suffixes()
.first()
.unwrap_or(&"txt".to_string())
);
let mut fake_servers = language
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
capabilities,
..Default::default()
}))
.await;
let project = Project::test(params.fs.clone(), [], cx).await;
project.update(cx, |project, _| project.languages().add(Arc::new(language)));
params
.fs
.as_fake()
.insert_tree("/root", json!({ "dir": { file_name: "" }}))
.await;
let (window_id, workspace) =
cx.add_window(|cx| Workspace::new(project.clone(), |_, _| unimplemented!(), cx));
project
.update(cx, |project, cx| {
project.find_or_create_local_worktree("/root", true, cx)
})
.await
.unwrap();
cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
.await;
let file = cx.read(|cx| workspace.file_project_paths(cx)[0].clone());
let item = workspace
.update(cx, |workspace, cx| workspace.open_path(file, true, cx))
.await
.expect("Could not open test file");
let editor = cx.update(|cx| {
item.act_as::<Editor>(cx)
.expect("Opened test file wasn't an editor")
});
editor.update(cx, |_, cx| cx.focus_self());
let lsp = fake_servers.next().await.unwrap();
Self {
cx: EditorTestContext {
cx,
window_id,
editor,
},
lsp,
workspace,
buffer_lsp_url: lsp::Url::from_file_path("/root/dir/file.rs").unwrap(),
}
}
pub async fn new_rust(
capabilities: lsp::ServerCapabilities,
cx: &'a mut gpui::TestAppContext,
) -> EditorLspTestContext<'a> {
let language = Language::new(
LanguageConfig {
name: "Rust".into(),
path_suffixes: vec!["rs".to_string()],
..Default::default()
},
Some(tree_sitter_rust::language()),
);
Self::new(language, capabilities, cx).await
}
// Constructs lsp range using a marked string with '[', ']' range delimiters
pub fn lsp_range(&mut self, marked_text: &str) -> lsp::Range {
let ranges = self.ranges(marked_text);
self.to_lsp_range(ranges[0].clone())
}
pub fn to_lsp_range(&mut self, range: Range<usize>) -> lsp::Range {
let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
let start_point = range.start.to_point(&snapshot.buffer_snapshot);
let end_point = range.end.to_point(&snapshot.buffer_snapshot);
self.editor(|editor, cx| {
let buffer = editor.buffer().read(cx);
let start = point_to_lsp(
buffer
.point_to_buffer_offset(start_point, cx)
.unwrap()
.1
.to_point_utf16(&buffer.read(cx)),
);
let end = point_to_lsp(
buffer
.point_to_buffer_offset(end_point, cx)
.unwrap()
.1
.to_point_utf16(&buffer.read(cx)),
);
lsp::Range { start, end }
})
}
pub fn to_lsp(&mut self, offset: usize) -> lsp::Position {
let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
let point = offset.to_point(&snapshot.buffer_snapshot);
self.editor(|editor, cx| {
let buffer = editor.buffer().read(cx);
point_to_lsp(
buffer
.point_to_buffer_offset(point, cx)
.unwrap()
.1
.to_point_utf16(&buffer.read(cx)),
)
})
}
pub fn update_workspace<F, T>(&mut self, update: F) -> T
where
F: FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> T,
{
self.workspace.update(self.cx.cx, update)
}
pub fn handle_request<T, F, Fut>(
&self,
mut handler: F,
) -> futures::channel::mpsc::UnboundedReceiver<()>
where
T: 'static + request::Request,
T::Params: 'static + Send,
F: 'static + Send + FnMut(lsp::Url, T::Params, gpui::AsyncAppContext) -> Fut,
Fut: 'static + Send + Future<Output = Result<T::Result>>,
{
let url = self.buffer_lsp_url.clone();
self.lsp.handle_request::<T, _, _>(move |params, cx| {
let url = url.clone();
handler(url, params, cx)
})
}
pub fn notify<T: notification::Notification>(&self, params: T::Params) {
self.lsp.notify::<T>(params);
}
}
impl<'a> Deref for EditorLspTestContext<'a> {
type Target = EditorTestContext<'a>;
fn deref(&self) -> &Self::Target {
&self.cx
}
}
impl<'a> DerefMut for EditorLspTestContext<'a> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.cx
}
}
@@ -0,0 +1,208 @@
use std::{
ops::{Deref, DerefMut, Range},
sync::Arc,
};
use anyhow::Result;
use futures::Future;
use gpui::{json, ViewContext, ViewHandle};
use language::{point_to_lsp, FakeLspAdapter, Language, LanguageConfig};
use lsp::{notification, request};
use project::Project;
use smol::stream::StreamExt;
use workspace::{pane, AppState, Workspace, WorkspaceHandle};
use crate::{multi_buffer::ToPointUtf16, Editor, ToPoint};
use super::editor_test_context::EditorTestContext;
pub struct EditorLspTestContext<'a> {
pub cx: EditorTestContext<'a>,
pub lsp: lsp::FakeLanguageServer,
pub workspace: ViewHandle<Workspace>,
pub buffer_lsp_url: lsp::Url,
}
impl<'a> EditorLspTestContext<'a> {
pub async fn new(
mut language: Language,
capabilities: lsp::ServerCapabilities,
cx: &'a mut gpui::TestAppContext,
) -> EditorLspTestContext<'a> {
use json::json;
cx.update(|cx| {
crate::init(cx);
pane::init(cx);
});
let params = cx.update(AppState::test);
let file_name = format!(
"file.{}",
language
.path_suffixes()
.first()
.unwrap_or(&"txt".to_string())
);
let mut fake_servers = language
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
capabilities,
..Default::default()
}))
.await;
let project = Project::test(params.fs.clone(), [], cx).await;
project.update(cx, |project, _| project.languages().add(Arc::new(language)));
params
.fs
.as_fake()
.insert_tree("/root", json!({ "dir": { file_name: "" }}))
.await;
let (window_id, workspace) =
cx.add_window(|cx| Workspace::new(project.clone(), |_, _| unimplemented!(), cx));
project
.update(cx, |project, cx| {
project.find_or_create_local_worktree("/root", true, cx)
})
.await
.unwrap();
cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
.await;
let file = cx.read(|cx| workspace.file_project_paths(cx)[0].clone());
let item = workspace
.update(cx, |workspace, cx| workspace.open_path(file, true, cx))
.await
.expect("Could not open test file");
let editor = cx.update(|cx| {
item.act_as::<Editor>(cx)
.expect("Opened test file wasn't an editor")
});
editor.update(cx, |_, cx| cx.focus_self());
let lsp = fake_servers.next().await.unwrap();
Self {
cx: EditorTestContext {
cx,
window_id,
editor,
},
lsp,
workspace,
buffer_lsp_url: lsp::Url::from_file_path("/root/dir/file.rs").unwrap(),
}
}
pub async fn new_rust(
capabilities: lsp::ServerCapabilities,
cx: &'a mut gpui::TestAppContext,
) -> EditorLspTestContext<'a> {
let language = Language::new(
LanguageConfig {
name: "Rust".into(),
path_suffixes: vec!["rs".to_string()],
..Default::default()
},
Some(tree_sitter_rust::language()),
);
Self::new(language, capabilities, cx).await
}
// Constructs lsp range using a marked string with '[', ']' range delimiters
pub fn lsp_range(&mut self, marked_text: &str) -> lsp::Range {
let ranges = self.ranges(marked_text);
self.to_lsp_range(ranges[0].clone())
}
pub fn to_lsp_range(&mut self, range: Range<usize>) -> lsp::Range {
let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
let start_point = range.start.to_point(&snapshot.buffer_snapshot);
let end_point = range.end.to_point(&snapshot.buffer_snapshot);
self.editor(|editor, cx| {
let buffer = editor.buffer().read(cx);
let start = point_to_lsp(
buffer
.point_to_buffer_offset(start_point, cx)
.unwrap()
.1
.to_point_utf16(&buffer.read(cx)),
);
let end = point_to_lsp(
buffer
.point_to_buffer_offset(end_point, cx)
.unwrap()
.1
.to_point_utf16(&buffer.read(cx)),
);
lsp::Range { start, end }
})
}
pub fn to_lsp(&mut self, offset: usize) -> lsp::Position {
let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
let point = offset.to_point(&snapshot.buffer_snapshot);
self.editor(|editor, cx| {
let buffer = editor.buffer().read(cx);
point_to_lsp(
buffer
.point_to_buffer_offset(point, cx)
.unwrap()
.1
.to_point_utf16(&buffer.read(cx)),
)
})
}
pub fn update_workspace<F, T>(&mut self, update: F) -> T
where
F: FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> T,
{
self.workspace.update(self.cx.cx, update)
}
pub fn handle_request<T, F, Fut>(
&self,
mut handler: F,
) -> futures::channel::mpsc::UnboundedReceiver<()>
where
T: 'static + request::Request,
T::Params: 'static + Send,
F: 'static + Send + FnMut(lsp::Url, T::Params, gpui::AsyncAppContext) -> Fut,
Fut: 'static + Send + Future<Output = Result<T::Result>>,
{
let url = self.buffer_lsp_url.clone();
self.lsp.handle_request::<T, _, _>(move |params, cx| {
let url = url.clone();
handler(url, params, cx)
})
}
pub fn notify<T: notification::Notification>(&self, params: T::Params) {
self.lsp.notify::<T>(params);
}
}
impl<'a> Deref for EditorLspTestContext<'a> {
type Target = EditorTestContext<'a>;
fn deref(&self) -> &Self::Target {
&self.cx
}
}
impl<'a> DerefMut for EditorLspTestContext<'a> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.cx
}
}
@@ -0,0 +1,273 @@
use std::{
any::TypeId,
ops::{Deref, DerefMut, Range},
};
use futures::Future;
use indoc::indoc;
use crate::{
display_map::ToDisplayPoint, AnchorRangeExt, Autoscroll, DisplayPoint, Editor, MultiBuffer,
};
use gpui::{keymap::Keystroke, AppContext, ContextHandle, ModelContext, ViewContext, ViewHandle};
use language::{Buffer, BufferSnapshot};
use settings::Settings;
use util::{
assert_set_eq,
test::{generate_marked_text, marked_text_ranges},
};
use super::build_editor;
pub struct EditorTestContext<'a> {
pub cx: &'a mut gpui::TestAppContext,
pub window_id: usize,
pub editor: ViewHandle<Editor>,
}
impl<'a> EditorTestContext<'a> {
pub fn new(cx: &'a mut gpui::TestAppContext) -> EditorTestContext<'a> {
let (window_id, editor) = cx.update(|cx| {
cx.set_global(Settings::test(cx));
crate::init(cx);
let (window_id, editor) = cx.add_window(Default::default(), |cx| {
build_editor(MultiBuffer::build_simple("", cx), cx)
});
editor.update(cx, |_, cx| cx.focus_self());
(window_id, editor)
});
Self {
cx,
window_id,
editor,
}
}
pub fn condition(
&self,
predicate: impl FnMut(&Editor, &AppContext) -> bool,
) -> impl Future<Output = ()> {
self.editor.condition(self.cx, predicate)
}
pub fn editor<F, T>(&self, read: F) -> T
where
F: FnOnce(&Editor, &AppContext) -> T,
{
self.editor.read_with(self.cx, read)
}
pub fn update_editor<F, T>(&mut self, update: F) -> T
where
F: FnOnce(&mut Editor, &mut ViewContext<Editor>) -> T,
{
self.editor.update(self.cx, update)
}
pub fn multibuffer<F, T>(&self, read: F) -> T
where
F: FnOnce(&MultiBuffer, &AppContext) -> T,
{
self.editor(|editor, cx| read(editor.buffer().read(cx), cx))
}
pub fn update_multibuffer<F, T>(&mut self, update: F) -> T
where
F: FnOnce(&mut MultiBuffer, &mut ModelContext<MultiBuffer>) -> T,
{
self.update_editor(|editor, cx| editor.buffer().update(cx, update))
}
pub fn buffer_text(&self) -> String {
self.multibuffer(|buffer, cx| buffer.snapshot(cx).text())
}
pub fn buffer<F, T>(&self, read: F) -> T
where
F: FnOnce(&Buffer, &AppContext) -> T,
{
self.multibuffer(|multibuffer, cx| {
let buffer = multibuffer.as_singleton().unwrap().read(cx);
read(buffer, cx)
})
}
pub fn update_buffer<F, T>(&mut self, update: F) -> T
where
F: FnOnce(&mut Buffer, &mut ModelContext<Buffer>) -> T,
{
self.update_multibuffer(|multibuffer, cx| {
let buffer = multibuffer.as_singleton().unwrap();
buffer.update(cx, update)
})
}
pub fn buffer_snapshot(&self) -> BufferSnapshot {
self.buffer(|buffer, _| buffer.snapshot())
}
pub fn simulate_keystroke(&mut self, keystroke_text: &str) -> ContextHandle {
let keystroke_under_test_handle =
self.add_assertion_context(format!("Simulated Keystroke: {:?}", keystroke_text));
let keystroke = Keystroke::parse(keystroke_text).unwrap();
self.cx.dispatch_keystroke(self.window_id, keystroke, false);
keystroke_under_test_handle
}
pub fn simulate_keystrokes<const COUNT: usize>(
&mut self,
keystroke_texts: [&str; COUNT],
) -> ContextHandle {
let keystrokes_under_test_handle =
self.add_assertion_context(format!("Simulated Keystrokes: {:?}", keystroke_texts));
for keystroke_text in keystroke_texts.into_iter() {
self.simulate_keystroke(keystroke_text);
}
keystrokes_under_test_handle
}
pub fn ranges(&self, marked_text: &str) -> Vec<Range<usize>> {
let (unmarked_text, ranges) = marked_text_ranges(marked_text, false);
assert_eq!(self.buffer_text(), unmarked_text);
ranges
}
pub fn display_point(&mut self, marked_text: &str) -> DisplayPoint {
let ranges = self.ranges(marked_text);
let snapshot = self
.editor
.update(self.cx, |editor, cx| editor.snapshot(cx));
ranges[0].start.to_display_point(&snapshot)
}
// Returns anchors for the current buffer using `«` and `»`
pub fn text_anchor_range(&self, marked_text: &str) -> Range<language::Anchor> {
let ranges = self.ranges(marked_text);
let snapshot = self.buffer_snapshot();
snapshot.anchor_before(ranges[0].start)..snapshot.anchor_after(ranges[0].end)
}
/// Change the editor's text and selections using a string containing
/// embedded range markers that represent the ranges and directions of
/// each selection.
///
/// See the `util::test::marked_text_ranges` function for more information.
pub fn set_state(&mut self, marked_text: &str) -> ContextHandle {
let _state_context = self.add_assertion_context(format!(
"Editor State: \"{}\"",
marked_text.escape_debug().to_string()
));
let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true);
self.editor.update(self.cx, |editor, cx| {
editor.set_text(unmarked_text, cx);
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
s.select_ranges(selection_ranges)
})
});
_state_context
}
/// Make an assertion about the editor's text and the ranges and directions
/// of its selections using a string containing embedded range markers.
///
/// See the `util::test::marked_text_ranges` function for more information.
pub fn assert_editor_state(&mut self, marked_text: &str) {
let (unmarked_text, expected_selections) = marked_text_ranges(marked_text, true);
let buffer_text = self.buffer_text();
assert_eq!(
buffer_text, unmarked_text,
"Unmarked text doesn't match buffer text"
);
self.assert_selections(expected_selections, marked_text.to_string())
}
pub fn assert_editor_background_highlights<Tag: 'static>(&mut self, marked_text: &str) {
let expected_ranges = self.ranges(marked_text);
let actual_ranges: Vec<Range<usize>> = self.update_editor(|editor, cx| {
let snapshot = editor.snapshot(cx);
editor
.background_highlights
.get(&TypeId::of::<Tag>())
.map(|h| h.1.clone())
.unwrap_or_default()
.into_iter()
.map(|range| range.to_offset(&snapshot.buffer_snapshot))
.collect()
});
assert_set_eq!(actual_ranges, expected_ranges);
}
pub fn assert_editor_text_highlights<Tag: ?Sized + 'static>(&mut self, marked_text: &str) {
let expected_ranges = self.ranges(marked_text);
let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
let actual_ranges: Vec<Range<usize>> = snapshot
.highlight_ranges::<Tag>()
.map(|ranges| ranges.as_ref().clone().1)
.unwrap_or_default()
.into_iter()
.map(|range| range.to_offset(&snapshot.buffer_snapshot))
.collect();
assert_set_eq!(actual_ranges, expected_ranges);
}
pub fn assert_editor_selections(&mut self, expected_selections: Vec<Range<usize>>) {
let expected_marked_text =
generate_marked_text(&self.buffer_text(), &expected_selections, true);
self.assert_selections(expected_selections, expected_marked_text)
}
fn assert_selections(
&mut self,
expected_selections: Vec<Range<usize>>,
expected_marked_text: String,
) {
let actual_selections = self
.editor
.read_with(self.cx, |editor, cx| editor.selections.all::<usize>(cx))
.into_iter()
.map(|s| {
if s.reversed {
s.end..s.start
} else {
s.start..s.end
}
})
.collect::<Vec<_>>();
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:
{}
Actual selections:
{}
"},
self.assertion_context(),
expected_marked_text,
actual_marked_text,
);
}
}
}
impl<'a> Deref for EditorTestContext<'a> {
type Target = gpui::TestAppContext;
fn deref(&self) -> &Self::Target {
self.cx
}
}
impl<'a> DerefMut for EditorTestContext<'a> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.cx
}
}
+1
View File
@@ -25,6 +25,7 @@ env_logger = { version = "0.9", optional = true }
etagere = "0.2"
futures = "0.3"
image = "0.23"
itertools = "0.10"
lazy_static = "1.4.0"
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
num_cpus = "1.13"
+28 -603
View File
@@ -1,28 +1,8 @@
pub mod action;
mod callback_collection;
#[cfg(any(test, feature = "test-support"))]
pub mod test_app_context;
use crate::{
elements::ElementBox,
executor::{self, Task},
geometry::rect::RectF,
keymap::{self, Binding, Keystroke},
platform::{self, KeyDownEvent, Platform, PromptLevel, WindowOptions},
presenter::Presenter,
util::post_inc,
Appearance, AssetCache, AssetSource, ClipboardItem, FontCache, InputHandler, MouseButton,
MouseRegionId, PathPromptOptions, TextLayoutCache,
};
pub use action::*;
use anyhow::{anyhow, Context, Result};
use callback_collection::CallbackCollection;
use collections::{btree_map, hash_map::Entry, BTreeMap, HashMap, HashSet, VecDeque};
use keymap::MatchResult;
use lazy_static::lazy_static;
use parking_lot::Mutex;
use platform::Event;
use postage::oneshot;
use smallvec::SmallVec;
use smol::prelude::*;
use std::{
any::{type_name, Any, TypeId},
cell::RefCell,
@@ -38,7 +18,32 @@ use std::{
time::Duration,
};
use self::callback_collection::Mapping;
use anyhow::{anyhow, Context, Result};
use lazy_static::lazy_static;
use parking_lot::Mutex;
use postage::oneshot;
use smallvec::SmallVec;
use smol::prelude::*;
pub use action::*;
use callback_collection::{CallbackCollection, Mapping};
use collections::{btree_map, hash_map::Entry, BTreeMap, HashMap, HashSet, VecDeque};
use keymap::MatchResult;
use platform::Event;
#[cfg(any(test, feature = "test-support"))]
pub use test_app_context::{ContextHandle, TestAppContext};
use crate::{
elements::ElementBox,
executor::{self, Task},
geometry::rect::RectF,
keymap::{self, Binding, Keystroke},
platform::{self, KeyDownEvent, Platform, PromptLevel, WindowOptions},
presenter::Presenter,
util::post_inc,
Appearance, AssetCache, AssetSource, ClipboardItem, FontCache, InputHandler, MouseButton,
MouseRegionId, PathPromptOptions, TextLayoutCache,
};
pub trait Entity: 'static {
type Event;
@@ -177,13 +182,6 @@ pub struct App(Rc<RefCell<MutableAppContext>>);
#[derive(Clone)]
pub struct AsyncAppContext(Rc<RefCell<MutableAppContext>>);
#[cfg(any(test, feature = "test-support"))]
pub struct TestAppContext {
cx: Rc<RefCell<MutableAppContext>>,
foreground_platform: Rc<platform::test::ForegroundPlatform>,
condition_duration: Option<Duration>,
}
pub struct WindowInputHandler {
app: Rc<RefCell<MutableAppContext>>,
window_id: usize,
@@ -427,327 +425,6 @@ impl InputHandler for WindowInputHandler {
}
}
#[cfg(any(test, feature = "test-support"))]
impl TestAppContext {
pub fn new(
foreground_platform: Rc<platform::test::ForegroundPlatform>,
platform: Arc<dyn Platform>,
foreground: Rc<executor::Foreground>,
background: Arc<executor::Background>,
font_cache: Arc<FontCache>,
leak_detector: Arc<Mutex<LeakDetector>>,
first_entity_id: usize,
) -> Self {
let mut cx = MutableAppContext::new(
foreground,
background,
platform,
foreground_platform.clone(),
font_cache,
RefCounts {
#[cfg(any(test, feature = "test-support"))]
leak_detector,
..Default::default()
},
(),
);
cx.next_entity_id = first_entity_id;
let cx = TestAppContext {
cx: Rc::new(RefCell::new(cx)),
foreground_platform,
condition_duration: None,
};
cx.cx.borrow_mut().weak_self = Some(Rc::downgrade(&cx.cx));
cx
}
pub fn dispatch_action<A: Action>(&self, window_id: usize, action: A) {
let mut cx = self.cx.borrow_mut();
if let Some(view_id) = cx.focused_view_id(window_id) {
cx.handle_dispatch_action_from_effect(window_id, Some(view_id), &action);
}
}
pub fn dispatch_global_action<A: Action>(&self, action: A) {
self.cx.borrow_mut().dispatch_global_action(action);
}
pub fn dispatch_keystroke(&mut self, window_id: usize, keystroke: Keystroke, is_held: bool) {
let handled = self.cx.borrow_mut().update(|cx| {
let presenter = cx
.presenters_and_platform_windows
.get(&window_id)
.unwrap()
.0
.clone();
if cx.dispatch_keystroke(window_id, &keystroke) {
return true;
}
if presenter.borrow_mut().dispatch_event(
Event::KeyDown(KeyDownEvent {
keystroke: keystroke.clone(),
is_held,
}),
false,
cx,
) {
return true;
}
false
});
if !handled && !keystroke.cmd && !keystroke.ctrl {
WindowInputHandler {
app: self.cx.clone(),
window_id,
}
.replace_text_in_range(None, &keystroke.key)
}
}
pub fn add_model<T, F>(&mut self, build_model: F) -> ModelHandle<T>
where
T: Entity,
F: FnOnce(&mut ModelContext<T>) -> T,
{
self.cx.borrow_mut().add_model(build_model)
}
pub fn add_window<T, F>(&mut self, build_root_view: F) -> (usize, ViewHandle<T>)
where
T: View,
F: FnOnce(&mut ViewContext<T>) -> T,
{
let (window_id, view) = self
.cx
.borrow_mut()
.add_window(Default::default(), build_root_view);
self.simulate_window_activation(Some(window_id));
(window_id, view)
}
pub fn add_view<T, F>(
&mut self,
parent_handle: impl Into<AnyViewHandle>,
build_view: F,
) -> ViewHandle<T>
where
T: View,
F: FnOnce(&mut ViewContext<T>) -> T,
{
self.cx.borrow_mut().add_view(parent_handle, build_view)
}
pub fn window_ids(&self) -> Vec<usize> {
self.cx.borrow().window_ids().collect()
}
pub fn root_view<T: View>(&self, window_id: usize) -> Option<ViewHandle<T>> {
self.cx.borrow().root_view(window_id)
}
pub fn read<T, F: FnOnce(&AppContext) -> T>(&self, callback: F) -> T {
callback(self.cx.borrow().as_ref())
}
pub fn update<T, F: FnOnce(&mut MutableAppContext) -> T>(&mut self, callback: F) -> T {
let mut state = self.cx.borrow_mut();
// Don't increment pending flushes in order for effects to be flushed before the callback
// completes, which is helpful in tests.
let result = callback(&mut *state);
// Flush effects after the callback just in case there are any. This can happen in edge
// cases such as the closure dropping handles.
state.flush_effects();
result
}
pub fn render<F, V, T>(&mut self, handle: &ViewHandle<V>, f: F) -> T
where
F: FnOnce(&mut V, &mut RenderContext<V>) -> T,
V: View,
{
handle.update(&mut *self.cx.borrow_mut(), |view, cx| {
let mut render_cx = RenderContext {
app: cx,
window_id: handle.window_id(),
view_id: handle.id(),
view_type: PhantomData,
titlebar_height: 0.,
hovered_region_ids: Default::default(),
clicked_region_ids: None,
refreshing: false,
appearance: Appearance::Light,
};
f(view, &mut render_cx)
})
}
pub fn to_async(&self) -> AsyncAppContext {
AsyncAppContext(self.cx.clone())
}
pub fn font_cache(&self) -> Arc<FontCache> {
self.cx.borrow().cx.font_cache.clone()
}
pub fn foreground_platform(&self) -> Rc<platform::test::ForegroundPlatform> {
self.foreground_platform.clone()
}
pub fn platform(&self) -> Arc<dyn platform::Platform> {
self.cx.borrow().cx.platform.clone()
}
pub fn foreground(&self) -> Rc<executor::Foreground> {
self.cx.borrow().foreground().clone()
}
pub fn background(&self) -> Arc<executor::Background> {
self.cx.borrow().background().clone()
}
pub fn spawn<F, Fut, T>(&self, f: F) -> Task<T>
where
F: FnOnce(AsyncAppContext) -> Fut,
Fut: 'static + Future<Output = T>,
T: 'static,
{
let foreground = self.foreground();
let future = f(self.to_async());
let cx = self.to_async();
foreground.spawn(async move {
let result = future.await;
cx.0.borrow_mut().flush_effects();
result
})
}
pub fn simulate_new_path_selection(&self, result: impl FnOnce(PathBuf) -> Option<PathBuf>) {
self.foreground_platform.simulate_new_path_selection(result);
}
pub fn did_prompt_for_new_path(&self) -> bool {
self.foreground_platform.as_ref().did_prompt_for_new_path()
}
pub fn simulate_prompt_answer(&self, window_id: usize, answer: usize) {
use postage::prelude::Sink as _;
let mut done_tx = self
.window_mut(window_id)
.pending_prompts
.borrow_mut()
.pop_front()
.expect("prompt was not called");
let _ = done_tx.try_send(answer);
}
pub fn has_pending_prompt(&self, window_id: usize) -> bool {
let window = self.window_mut(window_id);
let prompts = window.pending_prompts.borrow_mut();
!prompts.is_empty()
}
pub fn current_window_title(&self, window_id: usize) -> Option<String> {
self.window_mut(window_id).title.clone()
}
pub fn simulate_window_close(&self, window_id: usize) -> bool {
let handler = self.window_mut(window_id).should_close_handler.take();
if let Some(mut handler) = handler {
let should_close = handler();
self.window_mut(window_id).should_close_handler = Some(handler);
should_close
} else {
false
}
}
pub fn simulate_window_activation(&self, to_activate: Option<usize>) {
let mut handlers = BTreeMap::new();
{
let mut cx = self.cx.borrow_mut();
for (window_id, (_, window)) in &mut cx.presenters_and_platform_windows {
let window = window
.as_any_mut()
.downcast_mut::<platform::test::Window>()
.unwrap();
handlers.insert(
*window_id,
mem::take(&mut window.active_status_change_handlers),
);
}
};
let mut handlers = handlers.into_iter().collect::<Vec<_>>();
handlers.sort_unstable_by_key(|(window_id, _)| Some(*window_id) == to_activate);
for (window_id, mut window_handlers) in handlers {
for window_handler in &mut window_handlers {
window_handler(Some(window_id) == to_activate);
}
self.window_mut(window_id)
.active_status_change_handlers
.extend(window_handlers);
}
}
pub fn is_window_edited(&self, window_id: usize) -> bool {
self.window_mut(window_id).edited
}
pub fn leak_detector(&self) -> Arc<Mutex<LeakDetector>> {
self.cx.borrow().leak_detector()
}
pub fn assert_dropped(&self, handle: impl WeakHandle) {
self.cx
.borrow()
.leak_detector()
.lock()
.assert_dropped(handle.id())
}
fn window_mut(&self, window_id: usize) -> std::cell::RefMut<platform::test::Window> {
std::cell::RefMut::map(self.cx.borrow_mut(), |state| {
let (_, window) = state
.presenters_and_platform_windows
.get_mut(&window_id)
.unwrap();
let test_window = window
.as_any_mut()
.downcast_mut::<platform::test::Window>()
.unwrap();
test_window
})
}
pub fn set_condition_duration(&mut self, duration: Option<Duration>) {
self.condition_duration = duration;
}
pub fn condition_duration(&self) -> Duration {
self.condition_duration.unwrap_or_else(|| {
if std::env::var("CI").is_ok() {
Duration::from_secs(2)
} else {
Duration::from_millis(500)
}
})
}
pub fn assert_clipboard_content(&mut self, expected_content: Option<&str>) {
self.update(|cx| {
let actual_content = cx.read_from_clipboard().map(|item| item.text().to_owned());
let expected_content = expected_content.map(|content| content.to_owned());
assert_eq!(actual_content, expected_content);
})
}
}
impl AsyncAppContext {
pub fn spawn<F, Fut, T>(&self, f: F) -> Task<T>
where
@@ -894,60 +571,6 @@ impl ReadViewWith for AsyncAppContext {
}
}
#[cfg(any(test, feature = "test-support"))]
impl UpdateModel for TestAppContext {
fn update_model<T: Entity, O>(
&mut self,
handle: &ModelHandle<T>,
update: &mut dyn FnMut(&mut T, &mut ModelContext<T>) -> O,
) -> O {
self.cx.borrow_mut().update_model(handle, update)
}
}
#[cfg(any(test, feature = "test-support"))]
impl ReadModelWith for TestAppContext {
fn read_model_with<E: Entity, T>(
&self,
handle: &ModelHandle<E>,
read: &mut dyn FnMut(&E, &AppContext) -> T,
) -> T {
let cx = self.cx.borrow();
let cx = cx.as_ref();
read(handle.read(cx), cx)
}
}
#[cfg(any(test, feature = "test-support"))]
impl UpdateView for TestAppContext {
fn update_view<T, S>(
&mut self,
handle: &ViewHandle<T>,
update: &mut dyn FnMut(&mut T, &mut ViewContext<T>) -> S,
) -> S
where
T: View,
{
self.cx.borrow_mut().update_view(handle, update)
}
}
#[cfg(any(test, feature = "test-support"))]
impl ReadViewWith for TestAppContext {
fn read_view_with<V, T>(
&self,
handle: &ViewHandle<V>,
read: &mut dyn FnMut(&V, &AppContext) -> T,
) -> T
where
V: View,
{
let cx = self.cx.borrow();
let cx = cx.as_ref();
read(handle.read(cx), cx)
}
}
type ActionCallback =
dyn FnMut(&mut dyn AnyView, &dyn Action, &mut MutableAppContext, usize, usize);
type GlobalActionCallback = dyn FnMut(&dyn Action, &mut MutableAppContext);
@@ -4446,117 +4069,6 @@ impl<T: Entity> ModelHandle<T> {
update(model, cx)
})
}
#[cfg(any(test, feature = "test-support"))]
pub fn next_notification(&self, cx: &TestAppContext) -> impl Future<Output = ()> {
let (tx, mut rx) = futures::channel::mpsc::unbounded();
let mut cx = cx.cx.borrow_mut();
let subscription = cx.observe(self, move |_, _| {
tx.unbounded_send(()).ok();
});
let duration = if std::env::var("CI").is_ok() {
Duration::from_secs(5)
} else {
Duration::from_secs(1)
};
async move {
let notification = crate::util::timeout(duration, rx.next())
.await
.expect("next notification timed out");
drop(subscription);
notification.expect("model dropped while test was waiting for its next notification")
}
}
#[cfg(any(test, feature = "test-support"))]
pub fn next_event(&self, cx: &TestAppContext) -> impl Future<Output = T::Event>
where
T::Event: Clone,
{
let (tx, mut rx) = futures::channel::mpsc::unbounded();
let mut cx = cx.cx.borrow_mut();
let subscription = cx.subscribe(self, move |_, event, _| {
tx.unbounded_send(event.clone()).ok();
});
let duration = if std::env::var("CI").is_ok() {
Duration::from_secs(5)
} else {
Duration::from_secs(1)
};
cx.foreground.start_waiting();
async move {
let event = crate::util::timeout(duration, rx.next())
.await
.expect("next event timed out");
drop(subscription);
event.expect("model dropped while test was waiting for its next event")
}
}
#[cfg(any(test, feature = "test-support"))]
pub fn condition(
&self,
cx: &TestAppContext,
mut predicate: impl FnMut(&T, &AppContext) -> bool,
) -> impl Future<Output = ()> {
let (tx, mut rx) = futures::channel::mpsc::unbounded();
let mut cx = cx.cx.borrow_mut();
let subscriptions = (
cx.observe(self, {
let tx = tx.clone();
move |_, _| {
tx.unbounded_send(()).ok();
}
}),
cx.subscribe(self, {
move |_, _, _| {
tx.unbounded_send(()).ok();
}
}),
);
let cx = cx.weak_self.as_ref().unwrap().upgrade().unwrap();
let handle = self.downgrade();
let duration = if std::env::var("CI").is_ok() {
Duration::from_secs(5)
} else {
Duration::from_secs(1)
};
async move {
crate::util::timeout(duration, async move {
loop {
{
let cx = cx.borrow();
let cx = cx.as_ref();
if predicate(
handle
.upgrade(cx)
.expect("model dropped with pending condition")
.read(cx),
cx,
) {
break;
}
}
cx.borrow().foreground().start_waiting();
rx.next()
.await
.expect("model dropped with pending condition");
cx.borrow().foreground().finish_waiting();
}
})
.await
.expect("condition timed out");
drop(subscriptions);
}
}
}
impl<T: Entity> Clone for ModelHandle<T> {
@@ -4789,93 +4301,6 @@ impl<T: View> ViewHandle<T> {
cx.focused_view_id(self.window_id)
.map_or(false, |focused_id| focused_id == self.view_id)
}
#[cfg(any(test, feature = "test-support"))]
pub fn next_notification(&self, cx: &TestAppContext) -> impl Future<Output = ()> {
use postage::prelude::{Sink as _, Stream as _};
let (mut tx, mut rx) = postage::mpsc::channel(1);
let mut cx = cx.cx.borrow_mut();
let subscription = cx.observe(self, move |_, _| {
tx.try_send(()).ok();
});
let duration = if std::env::var("CI").is_ok() {
Duration::from_secs(5)
} else {
Duration::from_secs(1)
};
async move {
let notification = crate::util::timeout(duration, rx.recv())
.await
.expect("next notification timed out");
drop(subscription);
notification.expect("model dropped while test was waiting for its next notification")
}
}
#[cfg(any(test, feature = "test-support"))]
pub fn condition(
&self,
cx: &TestAppContext,
mut predicate: impl FnMut(&T, &AppContext) -> bool,
) -> impl Future<Output = ()> {
use postage::prelude::{Sink as _, Stream as _};
let (tx, mut rx) = postage::mpsc::channel(1024);
let timeout_duration = cx.condition_duration();
let mut cx = cx.cx.borrow_mut();
let subscriptions = self.update(&mut *cx, |_, cx| {
(
cx.observe(self, {
let mut tx = tx.clone();
move |_, _, _| {
tx.blocking_send(()).ok();
}
}),
cx.subscribe(self, {
let mut tx = tx.clone();
move |_, _, _, _| {
tx.blocking_send(()).ok();
}
}),
)
});
let cx = cx.weak_self.as_ref().unwrap().upgrade().unwrap();
let handle = self.downgrade();
async move {
crate::util::timeout(timeout_duration, async move {
loop {
{
let cx = cx.borrow();
let cx = cx.as_ref();
if predicate(
handle
.upgrade(cx)
.expect("view dropped with pending condition")
.read(cx),
cx,
) {
break;
}
}
cx.borrow().foreground().start_waiting();
rx.recv()
.await
.expect("view dropped with pending condition");
cx.borrow().foreground().finish_waiting();
}
})
.await
.expect("condition timed out");
drop(subscriptions);
}
}
}
impl<T: View> Clone for ViewHandle<T> {
+655
View File
@@ -0,0 +1,655 @@
use std::{
cell::RefCell,
marker::PhantomData,
mem,
path::PathBuf,
rc::Rc,
sync::{
atomic::{AtomicUsize, Ordering},
Arc,
},
time::Duration,
};
use futures::Future;
use itertools::Itertools;
use parking_lot::{Mutex, RwLock};
use smol::stream::StreamExt;
use crate::{
executor, keymap::Keystroke, platform, Action, AnyViewHandle, AppContext, Appearance, Entity,
Event, FontCache, InputHandler, KeyDownEvent, LeakDetector, ModelContext, ModelHandle,
MutableAppContext, Platform, ReadModelWith, ReadViewWith, RenderContext, Task, UpdateModel,
UpdateView, View, ViewContext, ViewHandle, WeakHandle, WindowInputHandler,
};
use collections::BTreeMap;
use super::{AsyncAppContext, RefCounts};
pub struct TestAppContext {
cx: Rc<RefCell<MutableAppContext>>,
foreground_platform: Rc<platform::test::ForegroundPlatform>,
condition_duration: Option<Duration>,
pub function_name: String,
assertion_context: AssertionContextManager,
}
impl TestAppContext {
pub fn new(
foreground_platform: Rc<platform::test::ForegroundPlatform>,
platform: Arc<dyn Platform>,
foreground: Rc<executor::Foreground>,
background: Arc<executor::Background>,
font_cache: Arc<FontCache>,
leak_detector: Arc<Mutex<LeakDetector>>,
first_entity_id: usize,
function_name: String,
) -> Self {
let mut cx = MutableAppContext::new(
foreground,
background,
platform,
foreground_platform.clone(),
font_cache,
RefCounts {
#[cfg(any(test, feature = "test-support"))]
leak_detector,
..Default::default()
},
(),
);
cx.next_entity_id = first_entity_id;
let cx = TestAppContext {
cx: Rc::new(RefCell::new(cx)),
foreground_platform,
condition_duration: None,
function_name,
assertion_context: AssertionContextManager::new(),
};
cx.cx.borrow_mut().weak_self = Some(Rc::downgrade(&cx.cx));
cx
}
pub fn dispatch_action<A: Action>(&self, window_id: usize, action: A) {
let mut cx = self.cx.borrow_mut();
if let Some(view_id) = cx.focused_view_id(window_id) {
cx.handle_dispatch_action_from_effect(window_id, Some(view_id), &action);
}
}
pub fn dispatch_global_action<A: Action>(&self, action: A) {
self.cx.borrow_mut().dispatch_global_action(action);
}
pub fn dispatch_keystroke(&mut self, window_id: usize, keystroke: Keystroke, is_held: bool) {
let handled = self.cx.borrow_mut().update(|cx| {
let presenter = cx
.presenters_and_platform_windows
.get(&window_id)
.unwrap()
.0
.clone();
if cx.dispatch_keystroke(window_id, &keystroke) {
return true;
}
if presenter.borrow_mut().dispatch_event(
Event::KeyDown(KeyDownEvent {
keystroke: keystroke.clone(),
is_held,
}),
false,
cx,
) {
return true;
}
false
});
if !handled && !keystroke.cmd && !keystroke.ctrl {
WindowInputHandler {
app: self.cx.clone(),
window_id,
}
.replace_text_in_range(None, &keystroke.key)
}
}
pub fn add_model<T, F>(&mut self, build_model: F) -> ModelHandle<T>
where
T: Entity,
F: FnOnce(&mut ModelContext<T>) -> T,
{
self.cx.borrow_mut().add_model(build_model)
}
pub fn add_window<T, F>(&mut self, build_root_view: F) -> (usize, ViewHandle<T>)
where
T: View,
F: FnOnce(&mut ViewContext<T>) -> T,
{
let (window_id, view) = self
.cx
.borrow_mut()
.add_window(Default::default(), build_root_view);
self.simulate_window_activation(Some(window_id));
(window_id, view)
}
pub fn add_view<T, F>(
&mut self,
parent_handle: impl Into<AnyViewHandle>,
build_view: F,
) -> ViewHandle<T>
where
T: View,
F: FnOnce(&mut ViewContext<T>) -> T,
{
self.cx.borrow_mut().add_view(parent_handle, build_view)
}
pub fn window_ids(&self) -> Vec<usize> {
self.cx.borrow().window_ids().collect()
}
pub fn root_view<T: View>(&self, window_id: usize) -> Option<ViewHandle<T>> {
self.cx.borrow().root_view(window_id)
}
pub fn read<T, F: FnOnce(&AppContext) -> T>(&self, callback: F) -> T {
callback(self.cx.borrow().as_ref())
}
pub fn update<T, F: FnOnce(&mut MutableAppContext) -> T>(&mut self, callback: F) -> T {
let mut state = self.cx.borrow_mut();
// Don't increment pending flushes in order for effects to be flushed before the callback
// completes, which is helpful in tests.
let result = callback(&mut *state);
// Flush effects after the callback just in case there are any. This can happen in edge
// cases such as the closure dropping handles.
state.flush_effects();
result
}
pub fn render<F, V, T>(&mut self, handle: &ViewHandle<V>, f: F) -> T
where
F: FnOnce(&mut V, &mut RenderContext<V>) -> T,
V: View,
{
handle.update(&mut *self.cx.borrow_mut(), |view, cx| {
let mut render_cx = RenderContext {
app: cx,
window_id: handle.window_id(),
view_id: handle.id(),
view_type: PhantomData,
titlebar_height: 0.,
hovered_region_ids: Default::default(),
clicked_region_ids: None,
refreshing: false,
appearance: Appearance::Light,
};
f(view, &mut render_cx)
})
}
pub fn to_async(&self) -> AsyncAppContext {
AsyncAppContext(self.cx.clone())
}
pub fn font_cache(&self) -> Arc<FontCache> {
self.cx.borrow().cx.font_cache.clone()
}
pub fn foreground_platform(&self) -> Rc<platform::test::ForegroundPlatform> {
self.foreground_platform.clone()
}
pub fn platform(&self) -> Arc<dyn platform::Platform> {
self.cx.borrow().cx.platform.clone()
}
pub fn foreground(&self) -> Rc<executor::Foreground> {
self.cx.borrow().foreground().clone()
}
pub fn background(&self) -> Arc<executor::Background> {
self.cx.borrow().background().clone()
}
pub fn spawn<F, Fut, T>(&self, f: F) -> Task<T>
where
F: FnOnce(AsyncAppContext) -> Fut,
Fut: 'static + Future<Output = T>,
T: 'static,
{
let foreground = self.foreground();
let future = f(self.to_async());
let cx = self.to_async();
foreground.spawn(async move {
let result = future.await;
cx.0.borrow_mut().flush_effects();
result
})
}
pub fn simulate_new_path_selection(&self, result: impl FnOnce(PathBuf) -> Option<PathBuf>) {
self.foreground_platform.simulate_new_path_selection(result);
}
pub fn did_prompt_for_new_path(&self) -> bool {
self.foreground_platform.as_ref().did_prompt_for_new_path()
}
pub fn simulate_prompt_answer(&self, window_id: usize, answer: usize) {
use postage::prelude::Sink as _;
let mut done_tx = self
.window_mut(window_id)
.pending_prompts
.borrow_mut()
.pop_front()
.expect("prompt was not called");
let _ = done_tx.try_send(answer);
}
pub fn has_pending_prompt(&self, window_id: usize) -> bool {
let window = self.window_mut(window_id);
let prompts = window.pending_prompts.borrow_mut();
!prompts.is_empty()
}
pub fn current_window_title(&self, window_id: usize) -> Option<String> {
self.window_mut(window_id).title.clone()
}
pub fn simulate_window_close(&self, window_id: usize) -> bool {
let handler = self.window_mut(window_id).should_close_handler.take();
if let Some(mut handler) = handler {
let should_close = handler();
self.window_mut(window_id).should_close_handler = Some(handler);
should_close
} else {
false
}
}
pub fn simulate_window_activation(&self, to_activate: Option<usize>) {
let mut handlers = BTreeMap::new();
{
let mut cx = self.cx.borrow_mut();
for (window_id, (_, window)) in &mut cx.presenters_and_platform_windows {
let window = window
.as_any_mut()
.downcast_mut::<platform::test::Window>()
.unwrap();
handlers.insert(
*window_id,
mem::take(&mut window.active_status_change_handlers),
);
}
};
let mut handlers = handlers.into_iter().collect::<Vec<_>>();
handlers.sort_unstable_by_key(|(window_id, _)| Some(*window_id) == to_activate);
for (window_id, mut window_handlers) in handlers {
for window_handler in &mut window_handlers {
window_handler(Some(window_id) == to_activate);
}
self.window_mut(window_id)
.active_status_change_handlers
.extend(window_handlers);
}
}
pub fn is_window_edited(&self, window_id: usize) -> bool {
self.window_mut(window_id).edited
}
pub fn leak_detector(&self) -> Arc<Mutex<LeakDetector>> {
self.cx.borrow().leak_detector()
}
pub fn assert_dropped(&self, handle: impl WeakHandle) {
self.cx
.borrow()
.leak_detector()
.lock()
.assert_dropped(handle.id())
}
fn window_mut(&self, window_id: usize) -> std::cell::RefMut<platform::test::Window> {
std::cell::RefMut::map(self.cx.borrow_mut(), |state| {
let (_, window) = state
.presenters_and_platform_windows
.get_mut(&window_id)
.unwrap();
let test_window = window
.as_any_mut()
.downcast_mut::<platform::test::Window>()
.unwrap();
test_window
})
}
pub fn set_condition_duration(&mut self, duration: Option<Duration>) {
self.condition_duration = duration;
}
pub fn condition_duration(&self) -> Duration {
self.condition_duration.unwrap_or_else(|| {
if std::env::var("CI").is_ok() {
Duration::from_secs(2)
} else {
Duration::from_millis(500)
}
})
}
pub fn assert_clipboard_content(&mut self, expected_content: Option<&str>) {
self.update(|cx| {
let actual_content = cx.read_from_clipboard().map(|item| item.text().to_owned());
let expected_content = expected_content.map(|content| content.to_owned());
assert_eq!(actual_content, expected_content);
})
}
pub fn add_assertion_context(&self, context: String) -> ContextHandle {
self.assertion_context.add_context(context)
}
pub fn assertion_context(&self) -> String {
self.assertion_context.context()
}
}
impl UpdateModel for TestAppContext {
fn update_model<T: Entity, O>(
&mut self,
handle: &ModelHandle<T>,
update: &mut dyn FnMut(&mut T, &mut ModelContext<T>) -> O,
) -> O {
self.cx.borrow_mut().update_model(handle, update)
}
}
impl ReadModelWith for TestAppContext {
fn read_model_with<E: Entity, T>(
&self,
handle: &ModelHandle<E>,
read: &mut dyn FnMut(&E, &AppContext) -> T,
) -> T {
let cx = self.cx.borrow();
let cx = cx.as_ref();
read(handle.read(cx), cx)
}
}
impl UpdateView for TestAppContext {
fn update_view<T, S>(
&mut self,
handle: &ViewHandle<T>,
update: &mut dyn FnMut(&mut T, &mut ViewContext<T>) -> S,
) -> S
where
T: View,
{
self.cx.borrow_mut().update_view(handle, update)
}
}
impl ReadViewWith for TestAppContext {
fn read_view_with<V, T>(
&self,
handle: &ViewHandle<V>,
read: &mut dyn FnMut(&V, &AppContext) -> T,
) -> T
where
V: View,
{
let cx = self.cx.borrow();
let cx = cx.as_ref();
read(handle.read(cx), cx)
}
}
impl<T: Entity> ModelHandle<T> {
pub fn next_notification(&self, cx: &TestAppContext) -> impl Future<Output = ()> {
let (tx, mut rx) = futures::channel::mpsc::unbounded();
let mut cx = cx.cx.borrow_mut();
let subscription = cx.observe(self, move |_, _| {
tx.unbounded_send(()).ok();
});
let duration = if std::env::var("CI").is_ok() {
Duration::from_secs(5)
} else {
Duration::from_secs(1)
};
async move {
let notification = crate::util::timeout(duration, rx.next())
.await
.expect("next notification timed out");
drop(subscription);
notification.expect("model dropped while test was waiting for its next notification")
}
}
pub fn next_event(&self, cx: &TestAppContext) -> impl Future<Output = T::Event>
where
T::Event: Clone,
{
let (tx, mut rx) = futures::channel::mpsc::unbounded();
let mut cx = cx.cx.borrow_mut();
let subscription = cx.subscribe(self, move |_, event, _| {
tx.unbounded_send(event.clone()).ok();
});
let duration = if std::env::var("CI").is_ok() {
Duration::from_secs(5)
} else {
Duration::from_secs(1)
};
cx.foreground.start_waiting();
async move {
let event = crate::util::timeout(duration, rx.next())
.await
.expect("next event timed out");
drop(subscription);
event.expect("model dropped while test was waiting for its next event")
}
}
pub fn condition(
&self,
cx: &TestAppContext,
mut predicate: impl FnMut(&T, &AppContext) -> bool,
) -> impl Future<Output = ()> {
let (tx, mut rx) = futures::channel::mpsc::unbounded();
let mut cx = cx.cx.borrow_mut();
let subscriptions = (
cx.observe(self, {
let tx = tx.clone();
move |_, _| {
tx.unbounded_send(()).ok();
}
}),
cx.subscribe(self, {
move |_, _, _| {
tx.unbounded_send(()).ok();
}
}),
);
let cx = cx.weak_self.as_ref().unwrap().upgrade().unwrap();
let handle = self.downgrade();
let duration = if std::env::var("CI").is_ok() {
Duration::from_secs(5)
} else {
Duration::from_secs(1)
};
async move {
crate::util::timeout(duration, async move {
loop {
{
let cx = cx.borrow();
let cx = cx.as_ref();
if predicate(
handle
.upgrade(cx)
.expect("model dropped with pending condition")
.read(cx),
cx,
) {
break;
}
}
cx.borrow().foreground().start_waiting();
rx.next()
.await
.expect("model dropped with pending condition");
cx.borrow().foreground().finish_waiting();
}
})
.await
.expect("condition timed out");
drop(subscriptions);
}
}
}
impl<T: View> ViewHandle<T> {
pub fn next_notification(&self, cx: &TestAppContext) -> impl Future<Output = ()> {
use postage::prelude::{Sink as _, Stream as _};
let (mut tx, mut rx) = postage::mpsc::channel(1);
let mut cx = cx.cx.borrow_mut();
let subscription = cx.observe(self, move |_, _| {
tx.try_send(()).ok();
});
let duration = if std::env::var("CI").is_ok() {
Duration::from_secs(5)
} else {
Duration::from_secs(1)
};
async move {
let notification = crate::util::timeout(duration, rx.recv())
.await
.expect("next notification timed out");
drop(subscription);
notification.expect("model dropped while test was waiting for its next notification")
}
}
pub fn condition(
&self,
cx: &TestAppContext,
mut predicate: impl FnMut(&T, &AppContext) -> bool,
) -> impl Future<Output = ()> {
use postage::prelude::{Sink as _, Stream as _};
let (tx, mut rx) = postage::mpsc::channel(1024);
let timeout_duration = cx.condition_duration();
let mut cx = cx.cx.borrow_mut();
let subscriptions = self.update(&mut *cx, |_, cx| {
(
cx.observe(self, {
let mut tx = tx.clone();
move |_, _, _| {
tx.blocking_send(()).ok();
}
}),
cx.subscribe(self, {
let mut tx = tx.clone();
move |_, _, _, _| {
tx.blocking_send(()).ok();
}
}),
)
});
let cx = cx.weak_self.as_ref().unwrap().upgrade().unwrap();
let handle = self.downgrade();
async move {
crate::util::timeout(timeout_duration, async move {
loop {
{
let cx = cx.borrow();
let cx = cx.as_ref();
if predicate(
handle
.upgrade(cx)
.expect("view dropped with pending condition")
.read(cx),
cx,
) {
break;
}
}
cx.borrow().foreground().start_waiting();
rx.recv()
.await
.expect("view dropped with pending condition");
cx.borrow().foreground().finish_waiting();
}
})
.await
.expect("condition timed out");
drop(subscriptions);
}
}
}
#[derive(Clone)]
pub struct AssertionContextManager {
id: Arc<AtomicUsize>,
contexts: Arc<RwLock<BTreeMap<usize, String>>>,
}
impl AssertionContextManager {
pub fn new() -> Self {
Self {
id: Arc::new(AtomicUsize::new(0)),
contexts: Arc::new(RwLock::new(BTreeMap::new())),
}
}
pub fn add_context(&self, context: String) -> ContextHandle {
let id = self.id.fetch_add(1, Ordering::Relaxed);
let mut contexts = self.contexts.write();
contexts.insert(id, context);
ContextHandle {
id,
manager: self.clone(),
}
}
pub fn context(&self) -> String {
let contexts = self.contexts.read();
format!("\n{}\n", contexts.values().join("\n"))
}
}
pub struct ContextHandle {
id: usize,
manager: AssertionContextManager,
}
impl Drop for ContextHandle {
fn drop(&mut self) {
let mut contexts = self.manager.contexts.write();
contexts.remove(&self.id);
}
}
+2
View File
@@ -37,6 +37,7 @@ pub fn run_test(
u64,
bool,
)),
fn_name: String,
) {
// let _profiler = dhat::Profiler::new_heap();
@@ -78,6 +79,7 @@ pub fn run_test(
font_cache.clone(),
leak_detector.clone(),
0,
fn_name.clone(),
);
cx.update(|cx| {
test_fn(
+5 -2
View File
@@ -117,6 +117,7 @@ pub fn test(args: TokenStream, function: TokenStream) -> TokenStream {
cx.font_cache().clone(),
cx.leak_detector(),
#first_entity_id,
stringify!(#outer_fn_name).to_string(),
);
));
cx_teardowns.extend(quote!(
@@ -149,7 +150,8 @@ pub fn test(args: TokenStream, function: TokenStream) -> TokenStream {
#cx_vars
cx.foreground().run(#inner_fn_name(#inner_fn_args));
#cx_teardowns
}
},
stringify!(#outer_fn_name).to_string(),
);
}
}
@@ -187,7 +189,8 @@ pub fn test(args: TokenStream, function: TokenStream) -> TokenStream {
#num_iterations as u64,
#starting_seed as u64,
#max_retries,
&mut |cx, _, _, seed, is_last_iteration| #inner_fn_name(#inner_fn_args)
&mut |cx, _, _, seed, is_last_iteration| #inner_fn_name(#inner_fn_args),
stringify!(#outer_fn_name).to_string(),
);
}
}
+16 -3
View File
@@ -7,7 +7,20 @@ edition = "2021"
path = "src/vim.rs"
doctest = false
[features]
neovim = ["nvim-rs", "async-compat", "async-trait", "tokio"]
[dependencies]
serde = { version = "1.0", features = ["derive", "rc"] }
itertools = "0.10"
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
async-compat = { version = "0.2.1", "optional" = true }
async-trait = { version = "0.1", "optional" = true }
nvim-rs = { git = "https://github.com/KillTheMule/nvim-rs", branch = "master", features = ["use_tokio"], optional = true }
tokio = { version = "1.15", "optional" = true }
serde_json = { version = "1.0", features = ["preserve_order"] }
assets = { path = "../assets" }
collections = { path = "../collections" }
command_palette = { path = "../command_palette" }
@@ -15,14 +28,14 @@ editor = { path = "../editor" }
gpui = { path = "../gpui" }
language = { path = "../language" }
search = { path = "../search" }
serde = { version = "1.0", features = ["derive", "rc"] }
settings = { path = "../settings" }
workspace = { path = "../workspace" }
itertools = "0.10"
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
[dev-dependencies]
indoc = "1.0.4"
parking_lot = "0.11.1"
lazy_static = "1.4"
editor = { path = "../editor", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] }
language = { path = "../language", features = ["test-support"] }
+1 -1
View File
@@ -26,7 +26,7 @@ fn normal_before(_: &mut Workspace, _: &NormalBefore, cx: &mut ViewContext<Works
#[cfg(test)]
mod test {
use crate::{state::Mode, vim_test_context::VimTestContext};
use crate::{state::Mode, test::VimTestContext};
#[gpui::test]
async fn test_enter_and_exit_insert_mode(cx: &mut gpui::TestAppContext) {
+144 -80
View File
@@ -18,6 +18,7 @@ use crate::{
#[derive(Copy, Clone, Debug)]
pub enum Motion {
Left,
Backspace,
Down,
Up,
Right,
@@ -58,6 +59,7 @@ actions!(
vim,
[
Left,
Backspace,
Down,
Up,
Right,
@@ -74,6 +76,7 @@ impl_actions!(vim, [NextWordStart, NextWordEnd, PreviousWordStart]);
pub fn init(cx: &mut MutableAppContext) {
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));
@@ -106,19 +109,21 @@ pub fn init(cx: &mut MutableAppContext) {
);
}
fn motion(motion: Motion, cx: &mut MutableAppContext) {
Vim::update(cx, |vim, cx| {
if let Some(Operator::Namespace(_)) = vim.active_operator() {
vim.pop_operator(cx);
}
});
pub(crate) fn motion(motion: Motion, cx: &mut MutableAppContext) {
if let Some(Operator::Namespace(_)) = Vim::read(cx).active_operator() {
Vim::update(cx, |vim, cx| vim.pop_operator(cx));
}
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 {
Mode::Normal => normal_motion(motion, cx),
Mode::Visual { .. } => visual_motion(motion, cx),
Mode::Normal => normal_motion(motion, operator, times, cx),
Mode::Visual { .. } => visual_motion(motion, times, cx),
Mode::Insert => {
// Shouldn't execute a motion in insert mode. Ignoring
}
}
Vim::update(cx, |vim, cx| vim.clear_operator(cx));
}
// Motion handling is specified here:
@@ -150,30 +155,32 @@ impl Motion {
map: &DisplaySnapshot,
point: DisplayPoint,
goal: SelectionGoal,
times: usize,
) -> (DisplayPoint, SelectionGoal) {
use Motion::*;
match self {
Left => (left(map, point), SelectionGoal::None),
Down => movement::down(map, point, goal, true),
Up => movement::up(map, point, goal, true),
Right => (right(map, point), SelectionGoal::None),
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),
Right => (right(map, point, times), SelectionGoal::None),
NextWordStart { ignore_punctuation } => (
next_word_start(map, point, ignore_punctuation),
next_word_start(map, point, ignore_punctuation, times),
SelectionGoal::None,
),
NextWordEnd { ignore_punctuation } => (
next_word_end(map, point, ignore_punctuation),
next_word_end(map, point, ignore_punctuation, times),
SelectionGoal::None,
),
PreviousWordStart { ignore_punctuation } => (
previous_word_start(map, point, ignore_punctuation),
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),
CurrentLine => (end_of_line(map, point), SelectionGoal::None),
StartOfDocument => (start_of_document(map, point), SelectionGoal::None),
StartOfDocument => (start_of_document(map, point, times), SelectionGoal::None),
EndOfDocument => (end_of_document(map, point), SelectionGoal::None),
Matching => (matching(map, point), SelectionGoal::None),
}
@@ -184,9 +191,10 @@ impl Motion {
self,
map: &DisplaySnapshot,
selection: &mut Selection<DisplayPoint>,
times: usize,
expand_to_surrounding_newline: bool,
) {
let (head, goal) = self.move_point(map, selection.head(), selection.goal);
let (head, goal) = self.move_point(map, selection.head(), selection.goal, times);
selection.set_head(head, goal);
if self.linewise() {
@@ -206,7 +214,7 @@ impl Motion {
}
}
selection.end = map.next_line_boundary(selection.end.to_point(map)).1;
(_, selection.end) = map.next_line_boundary(selection.end.to_point(map));
} else {
// If the motion is exclusive and the end of the motion is in column 1, the
// end of the motion is moved to the end of the previous line and the motion
@@ -234,95 +242,151 @@ impl Motion {
}
}
fn left(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
*point.column_mut() = point.column().saturating_sub(1);
map.clip_point(point, Bias::Left)
}
fn right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
*point.column_mut() += 1;
map.clip_point(point, Bias::Right)
}
fn next_word_start(
map: &DisplaySnapshot,
point: DisplayPoint,
ignore_punctuation: bool,
) -> DisplayPoint {
let mut crossed_newline = false;
movement::find_boundary(map, point, |left, right| {
let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
let at_newline = right == '\n';
let found = (left_kind != right_kind && !right.is_whitespace())
|| at_newline && crossed_newline
|| at_newline && left == '\n'; // Prevents skipping repeated empty lines
if at_newline {
crossed_newline = true;
fn left(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
for _ in 0..times {
*point.column_mut() = point.column().saturating_sub(1);
point = map.clip_point(point, Bias::Right);
if point.column() == 0 {
break;
}
found
})
}
point
}
fn backspace(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
for _ in 0..times {
point = movement::left(map, point);
}
point
}
fn down(
map: &DisplaySnapshot,
mut point: DisplayPoint,
mut goal: SelectionGoal,
times: usize,
) -> (DisplayPoint, SelectionGoal) {
for _ in 0..times {
(point, goal) = movement::down(map, point, goal, true);
}
(point, goal)
}
fn up(
map: &DisplaySnapshot,
mut point: DisplayPoint,
mut goal: SelectionGoal,
times: usize,
) -> (DisplayPoint, SelectionGoal) {
for _ in 0..times {
(point, goal) = movement::up(map, point, goal, true);
}
(point, goal)
}
pub(crate) fn right(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
for _ in 0..times {
let mut new_point = point;
*new_point.column_mut() += 1;
let new_point = map.clip_point(new_point, Bias::Right);
if point == new_point {
break;
}
point = new_point;
}
point
}
pub(crate) fn next_word_start(
map: &DisplaySnapshot,
mut point: DisplayPoint,
ignore_punctuation: bool,
times: usize,
) -> DisplayPoint {
for _ in 0..times {
let mut crossed_newline = false;
point = movement::find_boundary(map, point, |left, right| {
let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
let at_newline = right == '\n';
let found = (left_kind != right_kind && right_kind != CharKind::Whitespace)
|| at_newline && crossed_newline
|| at_newline && left == '\n'; // Prevents skipping repeated empty lines
if at_newline {
crossed_newline = true;
}
found
})
}
point
}
fn next_word_end(
map: &DisplaySnapshot,
mut point: DisplayPoint,
ignore_punctuation: bool,
times: usize,
) -> DisplayPoint {
*point.column_mut() += 1;
point = movement::find_boundary(map, point, |left, right| {
let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
for _ in 0..times {
*point.column_mut() += 1;
point = movement::find_boundary(map, point, |left, right| {
let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
left_kind != right_kind && !left.is_whitespace()
});
// find_boundary clips, so if the character after the next character is a newline or at the end of the document, we know
// we have backtraced already
if !map
.chars_at(point)
.nth(1)
.map(|c| c == '\n')
.unwrap_or(true)
{
*point.column_mut() = point.column().saturating_sub(1);
left_kind != right_kind && left_kind != CharKind::Whitespace
});
// find_boundary clips, so if the character after the next character is a newline or at the end of the document, we know
// we have backtraced already
if !map
.chars_at(point)
.nth(1)
.map(|(c, _)| c == '\n')
.unwrap_or(true)
{
*point.column_mut() = point.column().saturating_sub(1);
}
point = map.clip_point(point, Bias::Left);
}
map.clip_point(point, Bias::Left)
point
}
fn previous_word_start(
map: &DisplaySnapshot,
mut point: DisplayPoint,
ignore_punctuation: bool,
times: usize,
) -> DisplayPoint {
// 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(left).coerce_punctuation(ignore_punctuation);
let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
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(left).coerce_punctuation(ignore_punctuation);
let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
(left_kind != right_kind && !right.is_whitespace()) || left == '\n'
});
(left_kind != right_kind && !right.is_whitespace()) || left == '\n'
});
}
point
}
fn first_non_whitespace(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
let mut column = 0;
for ch in map.chars_at(DisplayPoint::new(point.row(), 0)) {
fn first_non_whitespace(map: &DisplaySnapshot, from: DisplayPoint) -> DisplayPoint {
let mut last_point = DisplayPoint::new(from.row(), 0);
for (ch, point) in map.chars_at(last_point) {
if ch == '\n' {
return point;
return from;
}
last_point = point;
if char_kind(ch) != CharKind::Whitespace {
break;
}
column += ch.len_utf8() as u32;
}
*point.column_mut() = column;
map.clip_point(point, Bias::Left)
map.clip_point(last_point, Bias::Left)
}
fn start_of_line(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
@@ -333,8 +397,8 @@ fn end_of_line(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left)
}
fn start_of_document(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
let mut new_point = 0usize.to_display_point(map);
fn start_of_document(map: &DisplaySnapshot, point: DisplayPoint, line: usize) -> DisplayPoint {
let mut new_point = (line - 1).to_display_point(map);
*new_point.column_mut() = point.column();
map.clip_point(new_point, Bias::Left)
}
+340 -712
View File
File diff suppressed because it is too large. Load diff
+143 -52
View File
@@ -1,30 +1,20 @@
use crate::{motion::Motion, state::Mode, utils::copy_selections_content, Vim};
use editor::{char_kind, movement, Autoscroll};
use gpui::{impl_actions, MutableAppContext, ViewContext};
use serde::Deserialize;
use workspace::Workspace;
use crate::{motion::Motion, object::Object, state::Mode, utils::copy_selections_content, Vim};
use editor::{char_kind, display_map::DisplaySnapshot, movement, Autoscroll, DisplayPoint};
use gpui::MutableAppContext;
use language::Selection;
#[derive(Clone, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
struct ChangeWord {
#[serde(default)]
ignore_punctuation: bool,
}
impl_actions!(vim, [ChangeWord]);
pub fn init(cx: &mut MutableAppContext) {
cx.add_action(change_word);
}
pub fn change_over(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) {
pub fn change_motion(vim: &mut Vim, motion: Motion, times: usize, cx: &mut MutableAppContext) {
vim.update_active_editor(cx, |editor, cx| {
editor.transact(cx, |editor, cx| {
// We are swapping to insert mode anyway. Just set the line end clipping behavior now
editor.set_clip_at_line_ends(false, cx);
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
s.move_with(|map, selection| {
motion.expand_selection(map, selection, false);
if let Motion::NextWordStart { ignore_punctuation } = motion {
expand_changed_word_selection(map, selection, times, ignore_punctuation);
} else {
motion.expand_selection(map, selection, times, false);
}
});
});
copy_selections_content(editor, motion.linewise(), cx);
@@ -34,43 +24,60 @@ pub fn change_over(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) {
vim.switch_mode(Mode::Insert, false, cx)
}
pub fn change_object(vim: &mut Vim, object: Object, around: bool, cx: &mut MutableAppContext) {
let mut objects_found = false;
vim.update_active_editor(cx, |editor, cx| {
// We are swapping to insert mode anyway. Just set the line end clipping behavior now
editor.set_clip_at_line_ends(false, cx);
editor.transact(cx, |editor, cx| {
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
s.move_with(|map, selection| {
objects_found |= object.expand_selection(map, selection, around);
});
});
if objects_found {
copy_selections_content(editor, false, cx);
editor.insert("", cx);
}
});
});
if objects_found {
vim.switch_mode(Mode::Insert, false, cx);
} else {
vim.switch_mode(Mode::Normal, false, cx);
}
}
// From the docs https://vimhelp.org/change.txt.html#cw
// Special case: When the cursor is in a word, "cw" and "cW" do not include the
// white space after a word, they only change up to the end of the word. This is
// because Vim interprets "cw" as change-word, and a word does not include the
// following white space.
fn change_word(
_: &mut Workspace,
&ChangeWord { ignore_punctuation }: &ChangeWord,
cx: &mut ViewContext<Workspace>,
fn expand_changed_word_selection(
map: &DisplaySnapshot,
selection: &mut Selection<DisplayPoint>,
times: usize,
ignore_punctuation: bool,
) {
Vim::update(cx, |vim, cx| {
vim.update_active_editor(cx, |editor, cx| {
editor.transact(cx, |editor, cx| {
// We are swapping to insert mode anyway. Just set the line end clipping behavior now
editor.set_clip_at_line_ends(false, cx);
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
s.move_with(|map, selection| {
if selection.end.column() == map.line_len(selection.end.row()) {
return;
}
if times > 1 {
Motion::NextWordStart { ignore_punctuation }.expand_selection(
map,
selection,
times - 1,
false,
);
}
selection.end =
movement::find_boundary(map, selection.end, |left, right| {
let left_kind =
char_kind(left).coerce_punctuation(ignore_punctuation);
let right_kind =
char_kind(right).coerce_punctuation(ignore_punctuation);
if times == 1 && selection.end.column() == map.line_len(selection.end.row()) {
return;
}
left_kind != right_kind || left == '\n' || right == '\n'
});
});
});
copy_selections_content(editor, false, cx);
editor.insert("", cx);
});
});
vim.switch_mode(Mode::Insert, false, cx);
selection.end = movement::find_boundary(map, selection.end, |left, right| {
let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
left_kind != right_kind || left == '\n' || right == '\n'
});
}
@@ -78,7 +85,10 @@ fn change_word(
mod test {
use indoc::indoc;
use crate::{state::Mode, vim_test_context::VimTestContext};
use crate::{
state::Mode,
test::{NeovimBackedTestContext, VimTestContext},
};
#[gpui::test]
async fn test_change_h(cx: &mut gpui::TestAppContext) {
@@ -170,8 +180,7 @@ mod test {
test"},
indoc! {"
Test test
ˇ
test"},
ˇ"},
);
let mut cx = cx.binding(["c", "shift-e"]);
@@ -193,6 +202,7 @@ mod test {
Test ˇ
test"},
);
println!("Marker");
cx.assert(
indoc! {"
Test test
@@ -442,4 +452,85 @@ mod test {
the lazy"},
);
}
#[gpui::test]
async fn test_repeated_cj(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
for count in 1..=5 {
cx.assert_binding_matches_all(
["c", &count.to_string(), "j"],
indoc! {"
ˇThe quˇickˇ browˇn
ˇ
ˇfox ˇjumpsˇ-ˇoˇver
ˇthe lazy dog
"},
)
.await;
}
}
#[gpui::test]
async fn test_repeated_cl(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
for count in 1..=5 {
cx.assert_binding_matches_all(
["c", &count.to_string(), "l"],
indoc! {"
ˇThe quˇickˇ browˇn
ˇ
ˇfox ˇjumpsˇ-ˇoˇver
ˇthe lazy dog
"},
)
.await;
}
}
#[gpui::test]
async fn test_repeated_cb(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
// Changing back any number of times from the start of the file doesn't
// switch to insert mode in vim. This is weird and painful to implement
cx.add_initial_state_exemption(indoc! {"
ˇThe quick brown
fox jumps-over
the lazy dog
"});
for count in 1..=5 {
cx.assert_binding_matches_all(
["c", &count.to_string(), "b"],
indoc! {"
ˇThe quˇickˇ browˇn
ˇ
ˇfox ˇjumpsˇ-ˇoˇver
ˇthe lazy dog
"},
)
.await;
}
}
#[gpui::test]
async fn test_repeated_ce(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
for count in 1..=5 {
cx.assert_binding_matches_all(
["c", &count.to_string(), "e"],
indoc! {"
ˇThe quˇickˇ browˇn
ˇ
ˇfox ˇjumpsˇ-ˇoˇver
ˇthe lazy dog
"},
)
.await;
}
}
}
+63 -8
View File
@@ -1,9 +1,9 @@
use crate::{motion::Motion, utils::copy_selections_content, Vim};
use collections::HashMap;
use editor::{Autoscroll, Bias};
use crate::{motion::Motion, object::Object, utils::copy_selections_content, Vim};
use collections::{HashMap, HashSet};
use editor::{display_map::ToDisplayPoint, Autoscroll, Bias};
use gpui::MutableAppContext;
pub fn delete_over(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) {
pub fn delete_motion(vim: &mut Vim, motion: Motion, times: usize, cx: &mut MutableAppContext) {
vim.update_active_editor(cx, |editor, cx| {
editor.transact(cx, |editor, cx| {
editor.set_clip_at_line_ends(false, cx);
@@ -11,8 +11,8 @@ pub fn delete_over(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) {
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
s.move_with(|map, selection| {
let original_head = selection.head();
motion.expand_selection(map, selection, true);
original_columns.insert(selection.id, original_head.column());
motion.expand_selection(map, selection, times, true);
});
});
copy_selections_content(editor, motion.linewise(), cx);
@@ -36,11 +36,67 @@ pub fn delete_over(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) {
});
}
pub fn delete_object(vim: &mut Vim, object: Object, around: bool, cx: &mut MutableAppContext) {
vim.update_active_editor(cx, |editor, cx| {
editor.transact(cx, |editor, cx| {
editor.set_clip_at_line_ends(false, cx);
// Emulates behavior in vim where if we expanded backwards to include a newline
// the cursor gets set back to the start of the line
let mut should_move_to_start: HashSet<_> = Default::default();
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
s.move_with(|map, selection| {
object.expand_selection(map, selection, around);
let offset_range = selection.map(|p| p.to_offset(map, Bias::Left)).range();
let contains_only_newlines = map
.chars_at(selection.start)
.take_while(|(_, p)| p < &selection.end)
.all(|(char, _)| char == '\n')
&& !offset_range.is_empty();
let end_at_newline = map
.chars_at(selection.end)
.next()
.map(|(c, _)| c == '\n')
.unwrap_or(false);
// If expanded range contains only newlines and
// the object is around or sentence, expand to include a newline
// at the end or start
if (around || object == Object::Sentence) && contains_only_newlines {
if end_at_newline {
selection.end =
(offset_range.end + '\n'.len_utf8()).to_display_point(map);
} else if selection.start.row() > 0 {
should_move_to_start.insert(selection.id);
selection.start =
(offset_range.start - '\n'.len_utf8()).to_display_point(map);
}
}
});
});
copy_selections_content(editor, false, cx);
editor.insert("", cx);
// Fixup cursor position after the deletion
editor.set_clip_at_line_ends(true, cx);
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
s.move_with(|map, selection| {
let mut cursor = selection.head();
if should_move_to_start.contains(&selection.id) {
*cursor.column_mut() = 0;
}
cursor = map.clip_point(cursor, Bias::Left);
selection.collapse_to(cursor, selection.goal)
});
});
});
});
}
#[cfg(test)]
mod test {
use indoc::indoc;
use crate::{state::Mode, vim_test_context::VimTestContext};
use crate::{state::Mode, test::VimTestContext};
#[gpui::test]
async fn test_delete_h(cx: &mut gpui::TestAppContext) {
@@ -140,8 +196,7 @@ mod test {
test"},
indoc! {"
Test test
ˇ
test"},
ˇ"},
);
let mut cx = cx.binding(["d", "shift-e"]);
+26 -3
View File
@@ -1,8 +1,8 @@
use crate::{motion::Motion, utils::copy_selections_content, Vim};
use crate::{motion::Motion, object::Object, utils::copy_selections_content, Vim};
use collections::HashMap;
use gpui::MutableAppContext;
pub fn yank_over(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) {
pub fn yank_motion(vim: &mut Vim, motion: Motion, times: usize, cx: &mut MutableAppContext) {
vim.update_active_editor(cx, |editor, cx| {
editor.transact(cx, |editor, cx| {
editor.set_clip_at_line_ends(false, cx);
@@ -10,8 +10,8 @@ pub fn yank_over(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) {
editor.change_selections(None, cx, |s| {
s.move_with(|map, selection| {
let original_position = (selection.head(), selection.goal);
motion.expand_selection(map, selection, true);
original_positions.insert(selection.id, original_position);
motion.expand_selection(map, selection, times, true);
});
});
copy_selections_content(editor, motion.linewise(), cx);
@@ -24,3 +24,26 @@ pub fn yank_over(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) {
});
});
}
pub fn yank_object(vim: &mut Vim, object: Object, around: bool, cx: &mut MutableAppContext) {
vim.update_active_editor(cx, |editor, cx| {
editor.transact(cx, |editor, cx| {
editor.set_clip_at_line_ends(false, cx);
let mut original_positions: HashMap<_, _> = Default::default();
editor.change_selections(None, cx, |s| {
s.move_with(|map, selection| {
let original_position = (selection.head(), selection.goal);
object.expand_selection(map, selection, around);
original_positions.insert(selection.id, original_position);
});
});
copy_selections_content(editor, false, cx);
editor.change_selections(None, cx, |s| {
s.move_with(|_, selection| {
let (head, goal) = original_positions.remove(&selection.id).unwrap();
selection.collapse_to(head, goal);
});
});
});
});
}
+640
View File
@@ -0,0 +1,640 @@
use std::ops::Range;
use editor::{char_kind, display_map::DisplaySnapshot, movement, Bias, CharKind, DisplayPoint};
use gpui::{actions, impl_actions, MutableAppContext};
use language::Selection;
use serde::Deserialize;
use workspace::Workspace;
use crate::{motion::right, normal::normal_object, state::Mode, visual::visual_object, Vim};
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum Object {
Word { ignore_punctuation: bool },
Sentence,
Quotes,
BackQuotes,
DoubleQuotes,
Parentheses,
SquareBrackets,
CurlyBrackets,
AngleBrackets,
}
#[derive(Clone, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
struct Word {
#[serde(default)]
ignore_punctuation: bool,
}
actions!(
vim,
[
Sentence,
Quotes,
BackQuotes,
DoubleQuotes,
Parentheses,
SquareBrackets,
CurlyBrackets,
AngleBrackets
]
);
impl_actions!(vim, [Word]);
pub fn init(cx: &mut MutableAppContext) {
cx.add_action(
|_: &mut Workspace, &Word { ignore_punctuation }: &Word, cx: _| {
object(Object::Word { ignore_punctuation }, cx)
},
);
cx.add_action(|_: &mut Workspace, _: &Sentence, cx: _| object(Object::Sentence, cx));
cx.add_action(|_: &mut Workspace, _: &Quotes, cx: _| object(Object::Quotes, cx));
cx.add_action(|_: &mut Workspace, _: &BackQuotes, cx: _| object(Object::BackQuotes, cx));
cx.add_action(|_: &mut Workspace, _: &DoubleQuotes, cx: _| object(Object::DoubleQuotes, cx));
cx.add_action(|_: &mut Workspace, _: &Parentheses, cx: _| object(Object::Parentheses, cx));
cx.add_action(|_: &mut Workspace, _: &SquareBrackets, cx: _| {
object(Object::SquareBrackets, cx)
});
cx.add_action(|_: &mut Workspace, _: &CurlyBrackets, cx: _| object(Object::CurlyBrackets, cx));
cx.add_action(|_: &mut Workspace, _: &AngleBrackets, cx: _| object(Object::AngleBrackets, cx));
}
fn object(object: Object, cx: &mut MutableAppContext) {
match Vim::read(cx).state.mode {
Mode::Normal => normal_object(object, cx),
Mode::Visual { .. } => visual_object(object, cx),
Mode::Insert => {
// Shouldn't execute a text object in insert mode. Ignoring
}
}
}
impl Object {
pub fn range(
self,
map: &DisplaySnapshot,
relative_to: DisplayPoint,
around: bool,
) -> Option<Range<DisplayPoint>> {
match self {
Object::Word { ignore_punctuation } => {
if around {
around_word(map, relative_to, ignore_punctuation)
} else {
in_word(map, relative_to, ignore_punctuation)
}
}
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, '<', '>'),
}
}
pub fn expand_selection(
self,
map: &DisplaySnapshot,
selection: &mut Selection<DisplayPoint>,
around: bool,
) -> bool {
if let Some(range) = self.range(map, selection.head(), around) {
selection.start = range.start;
selection.end = range.end;
true
} else {
false
}
}
}
/// Return a range that surrounds the word relative_to is in
/// If relative_to is at the start of a word, return the word.
/// If relative_to is between words, return the space between
fn in_word(
map: &DisplaySnapshot,
relative_to: DisplayPoint,
ignore_punctuation: bool,
) -> Option<Range<DisplayPoint>> {
// Use motion::right so that we consider the character under the cursor when looking for the start
let start = movement::find_preceding_boundary_in_line(
map,
right(map, relative_to, 1),
|left, right| {
char_kind(left).coerce_punctuation(ignore_punctuation)
!= char_kind(right).coerce_punctuation(ignore_punctuation)
},
);
let end = movement::find_boundary_in_line(map, relative_to, |left, right| {
char_kind(left).coerce_punctuation(ignore_punctuation)
!= char_kind(right).coerce_punctuation(ignore_punctuation)
});
Some(start..end)
}
/// Return a range that surrounds the word and following whitespace
/// relative_to is in.
/// If relative_to is at the start of a word, return the word and following whitespace.
/// If relative_to is between words, return the whitespace back and the following word
/// if in word
/// delete that word
/// if there is whitespace following the word, delete that as well
/// otherwise, delete any preceding whitespace
/// otherwise
/// delete whitespace around cursor
/// delete word following the cursor
fn around_word(
map: &DisplaySnapshot,
relative_to: DisplayPoint,
ignore_punctuation: bool,
) -> Option<Range<DisplayPoint>> {
let in_word = map
.chars_at(relative_to)
.next()
.map(|(c, _)| char_kind(c) != CharKind::Whitespace)
.unwrap_or(false);
if in_word {
around_containing_word(map, relative_to, ignore_punctuation)
} else {
around_next_word(map, relative_to, ignore_punctuation)
}
}
fn around_containing_word(
map: &DisplaySnapshot,
relative_to: DisplayPoint,
ignore_punctuation: bool,
) -> Option<Range<DisplayPoint>> {
in_word(map, relative_to, ignore_punctuation)
.map(|range| expand_to_include_whitespace(map, range, true))
}
fn around_next_word(
map: &DisplaySnapshot,
relative_to: DisplayPoint,
ignore_punctuation: bool,
) -> Option<Range<DisplayPoint>> {
// Get the start of the word
let start = movement::find_preceding_boundary_in_line(
map,
right(map, relative_to, 1),
|left, right| {
char_kind(left).coerce_punctuation(ignore_punctuation)
!= char_kind(right).coerce_punctuation(ignore_punctuation)
},
);
let mut word_found = false;
let end = movement::find_boundary(map, relative_to, |left, right| {
let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
let found = (word_found && left_kind != right_kind) || right == '\n' && left == '\n';
if right_kind != CharKind::Whitespace {
word_found = true;
}
found
});
Some(start..end)
}
fn sentence(
map: &DisplaySnapshot,
relative_to: DisplayPoint,
around: bool,
) -> Option<Range<DisplayPoint>> {
let mut start = None;
let mut previous_end = relative_to;
let mut chars = map.chars_at(relative_to).peekable();
// Search backwards for the previous sentence end or current sentence start. Include the character under relative_to
for (char, point) in chars
.peek()
.cloned()
.into_iter()
.chain(map.reverse_chars_at(relative_to))
{
if is_sentence_end(map, point) {
break;
}
if is_possible_sentence_start(char) {
start = Some(point);
}
previous_end = point;
}
// Search forward for the end of the current sentence or if we are between sentences, the start of the next one
let mut end = relative_to;
for (char, point) in chars {
if start.is_none() && is_possible_sentence_start(char) {
if around {
start = Some(point);
continue;
} else {
end = point;
break;
}
}
end = point;
*end.column_mut() += char.len_utf8() as u32;
end = map.clip_point(end, Bias::Left);
if is_sentence_end(map, end) {
break;
}
}
let mut range = start.unwrap_or(previous_end)..end;
if around {
range = expand_to_include_whitespace(map, range, false);
}
Some(range)
}
fn is_possible_sentence_start(character: char) -> bool {
!character.is_whitespace() && character != '.'
}
const SENTENCE_END_PUNCTUATION: &[char] = &['.', '!', '?'];
const SENTENCE_END_FILLERS: &[char] = &[')', ']', '"', '\''];
const SENTENCE_END_WHITESPACE: &[char] = &[' ', '\t', '\n'];
fn is_sentence_end(map: &DisplaySnapshot, point: DisplayPoint) -> bool {
let mut next_chars = map.chars_at(point).peekable();
if let Some((char, _)) = next_chars.next() {
// We are at a double newline. This position is a sentence end.
if char == '\n' && next_chars.peek().map(|(c, _)| c == &'\n').unwrap_or(false) {
return true;
}
// The next text is not a valid whitespace. This is not a sentence end
if !SENTENCE_END_WHITESPACE.contains(&char) {
return false;
}
}
for (char, _) in map.reverse_chars_at(point) {
if SENTENCE_END_PUNCTUATION.contains(&char) {
return true;
}
if !SENTENCE_END_FILLERS.contains(&char) {
return false;
}
}
return false;
}
/// Expands the passed range to include whitespace on one side or the other in a line. Attempts to add the
/// whitespace to the end first and falls back to the start if there was none.
fn expand_to_include_whitespace(
map: &DisplaySnapshot,
mut range: Range<DisplayPoint>,
stop_at_newline: bool,
) -> Range<DisplayPoint> {
let mut whitespace_included = false;
let mut chars = map.chars_at(range.end).peekable();
while let Some((char, point)) = chars.next() {
if char == '\n' && stop_at_newline {
break;
}
if char.is_whitespace() {
// Set end to the next display_point or the character position after the current display_point
range.end = chars.peek().map(|(_, point)| *point).unwrap_or_else(|| {
let mut end = point;
*end.column_mut() += char.len_utf8() as u32;
map.clip_point(end, Bias::Left)
});
if char != '\n' {
whitespace_included = true;
}
} else {
// Found non whitespace. Quit out.
break;
}
}
if !whitespace_included {
for (char, point) in map.reverse_chars_at(range.start) {
if char == '\n' && stop_at_newline {
break;
}
if !char.is_whitespace() {
break;
}
range.start = point;
}
}
range
}
fn surrounding_markers(
map: &DisplaySnapshot,
relative_to: DisplayPoint,
around: bool,
search_across_lines: bool,
start_marker: char,
end_marker: char,
) -> Option<Range<DisplayPoint>> {
let mut matched_ends = 0;
let mut start = None;
for (char, mut point) in map.reverse_chars_at(relative_to) {
if char == start_marker {
if matched_ends > 0 {
matched_ends -= 1;
} else {
if around {
start = Some(point)
} else {
*point.column_mut() += char.len_utf8() as u32;
start = Some(point);
}
break;
}
} else if char == end_marker {
matched_ends += 1;
} else if char == '\n' && !search_across_lines {
break;
}
}
let mut matched_starts = 0;
let mut end = None;
for (char, mut point) in map.chars_at(relative_to) {
if char == end_marker {
if start.is_none() {
break;
}
if matched_starts > 0 {
matched_starts -= 1;
} else {
if around {
*point.column_mut() += char.len_utf8() as u32;
end = Some(point);
} else {
end = Some(point);
}
break;
}
}
if char == start_marker {
if start.is_none() {
if around {
start = Some(point);
} else {
*point.column_mut() += char.len_utf8() as u32;
start = Some(point);
}
} else {
matched_starts += 1;
}
}
if char == '\n' && !search_across_lines {
break;
}
}
if let (Some(start), Some(end)) = (start, end) {
Some(start..end)
} else {
None
}
}
#[cfg(test)]
mod test {
use indoc::indoc;
use crate::test::NeovimBackedTestContext;
const WORD_LOCATIONS: &'static str = indoc! {"
The quick ˇbrowˇnˇ
fox ˇjuˇmpsˇ over
the lazy dogˇ
ˇ
ˇ
ˇ
Thˇeˇ-ˇquˇickˇ ˇbrownˇ
ˇ
ˇ
ˇ fox-jumpˇs over
the lazy dogˇ
ˇ
"};
#[gpui::test]
async fn test_change_word_object(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
cx.assert_binding_matches_all(["c", "i", "w"], WORD_LOCATIONS)
.await;
cx.assert_binding_matches_all(["c", "i", "shift-w"], WORD_LOCATIONS)
.await;
cx.assert_binding_matches_all(["c", "a", "w"], WORD_LOCATIONS)
.await;
cx.assert_binding_matches_all(["c", "a", "shift-w"], WORD_LOCATIONS)
.await;
}
#[gpui::test]
async fn test_delete_word_object(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
cx.assert_binding_matches_all(["d", "i", "w"], WORD_LOCATIONS)
.await;
cx.assert_binding_matches_all(["d", "i", "shift-w"], WORD_LOCATIONS)
.await;
cx.assert_binding_matches_all(["d", "a", "w"], WORD_LOCATIONS)
.await;
cx.assert_binding_matches_all(["d", "a", "shift-w"], WORD_LOCATIONS)
.await;
}
#[gpui::test]
async fn test_visual_word_object(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
cx.assert_binding_matches_all(["v", "i", "w"], WORD_LOCATIONS)
.await;
// Visual text objects are slightly broken when used with non empty selections
// cx.assert_binding_matches_all(["v", "h", "i", "w"], WORD_LOCATIONS)
// .await;
// cx.assert_binding_matches_all(["v", "l", "i", "w"], WORD_LOCATIONS)
// .await;
cx.assert_binding_matches_all(["v", "i", "shift-w"], WORD_LOCATIONS)
.await;
// Visual text objects are slightly broken when used with non empty selections
// cx.assert_binding_matches_all(["v", "i", "h", "shift-w"], WORD_LOCATIONS)
// .await;
// cx.assert_binding_matches_all(["v", "i", "l", "shift-w"], WORD_LOCATIONS)
// .await;
// Visual around words is somewhat broken right now when it comes to newlines
// cx.assert_binding_matches_all(["v", "a", "w"], WORD_LOCATIONS)
// .await;
// cx.assert_binding_matches_all(["v", "a", "shift-w"], WORD_LOCATIONS)
// .await;
}
const SENTENCE_EXAMPLES: &[&'static str] = &[
"ˇThe quick ˇbrownˇ?ˇ ˇFox Jˇumpsˇ!ˇ Ovˇer theˇ lazyˇ.",
indoc! {"
ˇThe quick ˇbrownˇ
fox jumps over
the lazy doˇgˇ.ˇ ˇThe quick ˇ
brown fox jumps over
"},
// Position of the cursor after deletion between lines isn't quite right.
// Deletion in a sentence at the start of a line with whitespace is incorrect.
// indoc! {"
// The quick brown fox jumps.
// Over the lazy dog
// ˇ
// ˇ
// ˇ fox-jumpˇs over
// the lazy dog.ˇ
// ˇ
// "},
r#"ˇThe ˇquick brownˇ.)ˇ]ˇ'ˇ" Brown ˇfox jumpsˇ.ˇ "#,
];
#[gpui::test]
async fn test_change_sentence_object(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx)
.await
.binding(["c", "i", "s"]);
for sentence_example in SENTENCE_EXAMPLES {
cx.assert_all(sentence_example).await;
}
let mut cx = cx.binding(["c", "a", "s"]);
// Resulting position is slightly incorrect for unintuitive reasons.
cx.add_initial_state_exemption("The quick brown?ˇ Fox Jumps! Over the lazy.");
// Changing around the sentence at the end of the line doesn't remove whitespace.'
cx.add_initial_state_exemption("The quick brown.)]\'\" Brown fox jumps.ˇ ");
for sentence_example in SENTENCE_EXAMPLES {
cx.assert_all(sentence_example).await;
}
}
#[gpui::test]
async fn test_delete_sentence_object(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx)
.await
.binding(["d", "i", "s"]);
for sentence_example in SENTENCE_EXAMPLES {
cx.assert_all(sentence_example).await;
}
let mut cx = cx.binding(["d", "a", "s"]);
// Resulting position is slightly incorrect for unintuitive reasons.
cx.add_initial_state_exemption("The quick brown?ˇ Fox Jumps! Over the lazy.");
// Changing around the sentence at the end of the line doesn't remove whitespace.'
cx.add_initial_state_exemption("The quick brown.)]\'\" Brown fox jumps.ˇ ");
for sentence_example in SENTENCE_EXAMPLES {
cx.assert_all(sentence_example).await;
}
}
#[gpui::test]
async fn test_visual_sentence_object(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx)
.await
.binding(["v", "i", "s"]);
for sentence_example in SENTENCE_EXAMPLES {
cx.assert_all(sentence_example).await;
}
// Visual around sentences is somewhat broken right now when it comes to newlines
// let mut cx = cx.binding(["d", "a", "s"]);
// for sentence_example in SENTENCE_EXAMPLES {
// cx.assert_all(sentence_example).await;
// }
}
// Test string with "`" for opening surrounders and "'" for closing surrounders
const SURROUNDING_MARKER_STRING: &str = indoc! {"
ˇTh'ˇe ˇ`ˇ'ˇquˇi`ˇck broˇ'wn`
'ˇfox juˇmps ovˇ`ˇer
the ˇlazy dˇ'ˇoˇ`ˇg"};
const SURROUNDING_OBJECTS: &[(char, char)] = &[
// ('\'', '\''), // Quote,
// ('`', '`'), // Back Quote
// ('"', '"'), // Double Quote
// ('"', '"'), // Double Quote
('(', ')'), // Parentheses
('[', ']'), // SquareBrackets
('{', '}'), // CurlyBrackets
('<', '>'), // AngleBrackets
];
#[gpui::test]
async fn test_change_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
for (start, end) in SURROUNDING_OBJECTS {
let marked_string = SURROUNDING_MARKER_STRING
.replace('`', &start.to_string())
.replace('\'', &end.to_string());
// cx.assert_binding_matches_all(["c", "i", &start.to_string()], &marked_string)
// .await;
cx.assert_binding_matches_all(["c", "i", &end.to_string()], &marked_string)
.await;
// cx.assert_binding_matches_all(["c", "a", &start.to_string()], &marked_string)
// .await;
cx.assert_binding_matches_all(["c", "a", &end.to_string()], &marked_string)
.await;
}
}
#[gpui::test]
async fn test_delete_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
for (start, end) in SURROUNDING_OBJECTS {
let marked_string = SURROUNDING_MARKER_STRING
.replace('`', &start.to_string())
.replace('\'', &end.to_string());
// cx.assert_binding_matches_all(["d", "i", &start.to_string()], &marked_string)
// .await;
cx.assert_binding_matches_all(["d", "i", &end.to_string()], &marked_string)
.await;
// cx.assert_binding_matches_all(["d", "a", &start.to_string()], &marked_string)
// .await;
cx.assert_binding_matches_all(["d", "a", &end.to_string()], &marked_string)
.await;
}
}
}
+14 -3
View File
@@ -1,8 +1,8 @@
use editor::CursorShape;
use gpui::keymap::Context;
use serde::Deserialize;
use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub enum Mode {
Normal,
Insert,
@@ -22,10 +22,12 @@ pub enum Namespace {
#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
pub enum Operator {
Number(usize),
Namespace(Namespace),
Change,
Delete,
Yank,
Object { around: bool },
}
#[derive(Default)]
@@ -77,7 +79,12 @@ impl VimState {
context.set.insert("VimControl".to_string());
}
Operator::set_context(self.operator_stack.last(), &mut context);
let active_operator = self.operator_stack.last();
if matches!(active_operator, Some(Operator::Object { .. })) {
context.set.insert("VimObject".to_string());
}
Operator::set_context(active_operator, &mut context);
context
}
@@ -86,10 +93,14 @@ impl VimState {
impl Operator {
pub fn set_context(operator: Option<&Operator>, context: &mut Context) {
let operator_context = match operator {
Some(Operator::Number(_)) => "n",
Some(Operator::Namespace(Namespace::G)) => "g",
Some(Operator::Object { around: false }) => "i",
Some(Operator::Object { around: true }) => "a",
Some(Operator::Change) => "c",
Some(Operator::Delete) => "d",
Some(Operator::Yank) => "y",
None => "none",
}
.to_owned();
+103
View File
@@ -0,0 +1,103 @@
mod neovim_backed_binding_test_context;
mod neovim_backed_test_context;
mod neovim_connection;
mod vim_binding_test_context;
mod vim_test_context;
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;
use search::BufferSearchBar;
use crate::state::Mode;
#[gpui::test]
async fn test_initially_disabled(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestContext::new(cx, false).await;
cx.simulate_keystrokes(["h", "j", "k", "l"]);
cx.assert_editor_state("hjklˇ");
}
#[gpui::test]
async fn test_neovim(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
cx.simulate_shared_keystroke("i").await;
cx.assert_state_matches().await;
cx.simulate_shared_keystrokes([
"shift-T", "e", "s", "t", " ", "t", "e", "s", "t", "escape", "0", "d", "w",
])
.await;
cx.assert_state_matches().await;
cx.assert_editor_state("ˇtest");
}
#[gpui::test]
async fn test_toggle_through_settings(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestContext::new(cx, true).await;
cx.simulate_keystroke("i");
assert_eq!(cx.mode(), Mode::Insert);
// Editor acts as though vim is disabled
cx.disable_vim();
cx.simulate_keystrokes(["h", "j", "k", "l"]);
cx.assert_editor_state("hjklˇ");
// Selections aren't changed if editor is blurred but vim-mode is still disabled.
cx.set_state("«hjklˇ»", Mode::Normal);
cx.assert_editor_state("«hjklˇ»");
cx.update_editor(|_, cx| cx.blur());
cx.assert_editor_state("«hjklˇ»");
cx.update_editor(|_, cx| cx.focus_self());
cx.assert_editor_state("«hjklˇ»");
// Enabling dynamically sets vim mode again and restores normal mode
cx.enable_vim();
assert_eq!(cx.mode(), Mode::Normal);
cx.simulate_keystrokes(["h", "h", "h", "l"]);
assert_eq!(cx.buffer_text(), "hjkl".to_owned());
cx.assert_editor_state("hˇjkl");
cx.simulate_keystrokes(["i", "T", "e", "s", "t"]);
cx.assert_editor_state("hTestˇjkl");
// Disabling and enabling resets to normal mode
assert_eq!(cx.mode(), Mode::Insert);
cx.disable_vim();
cx.enable_vim();
assert_eq!(cx.mode(), Mode::Normal);
}
#[gpui::test]
async fn test_buffer_search(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestContext::new(cx, true).await;
cx.set_state(
indoc! {"
The quick brown
fox juˇmps over
the lazy dog"},
Mode::Normal,
);
cx.simulate_keystroke("/");
// We now use a weird insert mode with selection when jumping to a single line editor
assert_eq!(cx.mode(), Mode::Insert);
let search_bar = cx.workspace(|workspace, cx| {
workspace
.active_pane()
.read(cx)
.toolbar()
.read(cx)
.item_of_type::<BufferSearchBar>()
.expect("Buffer search bar should be deployed")
});
search_bar.read_with(cx.cx, |bar, cx| {
assert_eq!(bar.query_editor.read(cx).text(cx), "jumps");
})
}
@@ -0,0 +1,80 @@
use std::ops::{Deref, DerefMut};
use gpui::ContextHandle;
use crate::state::Mode;
use super::NeovimBackedTestContext;
pub struct NeovimBackedBindingTestContext<'a, const COUNT: usize> {
cx: NeovimBackedTestContext<'a>,
keystrokes_under_test: [&'static str; COUNT],
}
impl<'a, const COUNT: usize> NeovimBackedBindingTestContext<'a, COUNT> {
pub fn new(
keystrokes_under_test: [&'static str; COUNT],
cx: NeovimBackedTestContext<'a>,
) -> Self {
Self {
cx,
keystrokes_under_test,
}
}
pub fn consume(self) -> NeovimBackedTestContext<'a> {
self.cx
}
pub fn binding<const NEW_COUNT: usize>(
self,
keystrokes: [&'static str; NEW_COUNT],
) -> NeovimBackedBindingTestContext<'a, NEW_COUNT> {
self.consume().binding(keystrokes)
}
pub async fn assert(
&mut self,
marked_positions: &str,
) -> Option<(ContextHandle, ContextHandle)> {
self.cx
.assert_binding_matches(self.keystrokes_under_test, marked_positions)
.await
}
pub fn assert_manual(
&mut self,
initial_state: &str,
mode_before: Mode,
state_after: &str,
mode_after: Mode,
) {
self.cx.assert_binding(
self.keystrokes_under_test,
initial_state,
mode_before,
state_after,
mode_after,
);
}
pub async fn assert_all(&mut self, marked_positions: &str) {
self.cx
.assert_binding_matches_all(self.keystrokes_under_test, marked_positions)
.await
}
}
impl<'a, const COUNT: usize> Deref for NeovimBackedBindingTestContext<'a, COUNT> {
type Target = NeovimBackedTestContext<'a>;
fn deref(&self) -> &Self::Target {
&self.cx
}
}
impl<'a, const COUNT: usize> DerefMut for NeovimBackedBindingTestContext<'a, COUNT> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.cx
}
}
@@ -0,0 +1,158 @@
use std::ops::{Deref, DerefMut};
use collections::{HashMap, HashSet};
use gpui::ContextHandle;
use language::OffsetRangeExt;
use util::test::marked_text_offsets;
use super::{neovim_connection::NeovimConnection, NeovimBackedBindingTestContext, VimTestContext};
use crate::state::Mode;
pub struct NeovimBackedTestContext<'a> {
cx: VimTestContext<'a>,
// Lookup for exempted assertions. Keyed by the insertion text, and with a value indicating which
// bindings are exempted. If None, all bindings are ignored for that insertion text.
exemptions: HashMap<String, Option<HashSet<String>>>,
neovim: NeovimConnection,
}
impl<'a> NeovimBackedTestContext<'a> {
pub async fn new(cx: &'a mut gpui::TestAppContext) -> NeovimBackedTestContext<'a> {
let function_name = cx.function_name.clone();
let cx = VimTestContext::new(cx, true).await;
Self {
cx,
exemptions: Default::default(),
neovim: NeovimConnection::new(function_name).await,
}
}
pub fn add_initial_state_exemption(&mut self, initial_state: &str) {
let initial_state = initial_state.to_string();
// None represents all keybindings being exempted for that initial state
self.exemptions.insert(initial_state, None);
}
pub async fn simulate_shared_keystroke(&mut self, keystroke_text: &str) -> ContextHandle {
self.neovim.send_keystroke(keystroke_text).await;
self.simulate_keystroke(keystroke_text)
}
pub async fn simulate_shared_keystrokes<const COUNT: usize>(
&mut self,
keystroke_texts: [&str; COUNT],
) -> ContextHandle {
for keystroke_text in keystroke_texts.into_iter() {
self.neovim.send_keystroke(keystroke_text).await;
}
self.simulate_keystrokes(keystroke_texts)
}
pub async fn set_shared_state(&mut self, marked_text: &str) -> ContextHandle {
let context_handle = self.set_state(marked_text, Mode::Normal);
let selection = self.editor(|editor, cx| editor.selections.newest::<language::Point>(cx));
let text = self.buffer_text();
self.neovim.set_state(selection, &text).await;
context_handle
}
pub async fn assert_state_matches(&mut self) {
assert_eq!(
self.neovim.text().await,
self.buffer_text(),
"{}",
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]);
if let Some(neovim_mode) = self.neovim.mode().await {
assert_eq!(neovim_mode, self.mode(), "{}", self.assertion_context(),);
}
}
pub async fn assert_binding_matches<const COUNT: usize>(
&mut self,
keystrokes: [&str; COUNT],
initial_state: &str,
) -> Option<(ContextHandle, ContextHandle)> {
if let Some(possible_exempted_keystrokes) = self.exemptions.get(initial_state) {
match possible_exempted_keystrokes {
Some(exempted_keystrokes) => {
if exempted_keystrokes.contains(&format!("{keystrokes:?}")) {
// This keystroke was exempted for this insertion text
return None;
}
}
None => {
// All keystrokes for this insertion text are exempted
return None;
}
}
}
let _state_context = self.set_shared_state(initial_state).await;
let _keystroke_context = self.simulate_shared_keystrokes(keystrokes).await;
self.assert_state_matches().await;
Some((_state_context, _keystroke_context))
}
pub async fn assert_binding_matches_all<const COUNT: usize>(
&mut self,
keystrokes: [&str; COUNT],
marked_positions: &str,
) {
let (unmarked_text, cursor_offsets) = marked_text_offsets(marked_positions);
for cursor_offset in cursor_offsets.iter() {
let mut marked_text = unmarked_text.clone();
marked_text.insert(*cursor_offset, 'ˇ');
self.assert_binding_matches(keystrokes, &marked_text).await;
}
}
pub fn binding<const COUNT: usize>(
self,
keystrokes: [&'static str; COUNT],
) -> NeovimBackedBindingTestContext<'a, COUNT> {
NeovimBackedBindingTestContext::new(keystrokes, self)
}
}
impl<'a> Deref for NeovimBackedTestContext<'a> {
type Target = VimTestContext<'a>;
fn deref(&self) -> &Self::Target {
&self.cx
}
}
impl<'a> DerefMut for NeovimBackedTestContext<'a> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.cx
}
}
#[cfg(test)]
mod test {
use gpui::TestAppContext;
use crate::test::NeovimBackedTestContext;
#[gpui::test]
async fn neovim_backed_test_context_works(cx: &mut TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
cx.assert_state_matches().await;
cx.set_shared_state("This is a tesˇt").await;
cx.assert_state_matches().await;
}
}
+383
View File
@@ -0,0 +1,383 @@
#[cfg(feature = "neovim")]
use std::ops::{Deref, DerefMut};
use std::{ops::Range, path::PathBuf};
#[cfg(feature = "neovim")]
use async_compat::Compat;
#[cfg(feature = "neovim")]
use async_trait::async_trait;
#[cfg(feature = "neovim")]
use gpui::keymap::Keystroke;
use language::{Point, Selection};
#[cfg(feature = "neovim")]
use lazy_static::lazy_static;
#[cfg(feature = "neovim")]
use nvim_rs::{
create::tokio::new_child_cmd, error::LoopError, Handler, Neovim, UiAttachOptions, Value,
};
#[cfg(feature = "neovim")]
use parking_lot::ReentrantMutex;
use serde::{Deserialize, Serialize};
#[cfg(feature = "neovim")]
use tokio::{
process::{Child, ChildStdin, Command},
task::JoinHandle,
};
use crate::state::Mode;
use collections::VecDeque;
// Neovim doesn't like to be started simultaneously from multiple threads. We use thsi lock
// to ensure we are only constructing one neovim connection at a time.
#[cfg(feature = "neovim")]
lazy_static! {
static ref NEOVIM_LOCK: ReentrantMutex<()> = ReentrantMutex::new(());
}
#[derive(Serialize, Deserialize)]
pub enum NeovimData {
Text(String),
Selection { start: (u32, u32), end: (u32, u32) },
Mode(Option<Mode>),
}
pub struct NeovimConnection {
data: VecDeque<NeovimData>,
#[cfg(feature = "neovim")]
test_case_id: String,
#[cfg(feature = "neovim")]
nvim: Neovim<nvim_rs::compat::tokio::Compat<ChildStdin>>,
#[cfg(feature = "neovim")]
_join_handle: JoinHandle<Result<(), Box<LoopError>>>,
#[cfg(feature = "neovim")]
_child: Child,
}
impl NeovimConnection {
pub async fn new(test_case_id: String) -> Self {
#[cfg(feature = "neovim")]
let handler = NvimHandler {};
#[cfg(feature = "neovim")]
let (nvim, join_handle, child) = Compat::new(async {
// Ensure we don't create neovim connections in parallel
let _lock = NEOVIM_LOCK.lock();
let (nvim, join_handle, child) = new_child_cmd(
&mut Command::new("nvim").arg("--embed").arg("--clean"),
handler,
)
.await
.expect("Could not connect to neovim process");
nvim.ui_attach(100, 100, &UiAttachOptions::default())
.await
.expect("Could not attach to ui");
// Makes system act a little more like zed in terms of indentation
nvim.set_option("smartindent", nvim_rs::Value::Boolean(true))
.await
.expect("Could not set smartindent on startup");
(nvim, join_handle, child)
})
.await;
Self {
#[cfg(feature = "neovim")]
data: Default::default(),
#[cfg(not(feature = "neovim"))]
data: Self::read_test_data(&test_case_id),
#[cfg(feature = "neovim")]
test_case_id,
#[cfg(feature = "neovim")]
nvim,
#[cfg(feature = "neovim")]
_join_handle: join_handle,
#[cfg(feature = "neovim")]
_child: child,
}
}
// Sends a keystroke to the neovim process.
#[cfg(feature = "neovim")]
pub async fn send_keystroke(&mut self, keystroke_text: &str) {
let keystroke = Keystroke::parse(keystroke_text).unwrap();
let special = keystroke.shift
|| keystroke.ctrl
|| keystroke.alt
|| keystroke.cmd
|| keystroke.key.len() > 1;
let start = if special { "<" } else { "" };
let shift = if keystroke.shift { "S-" } else { "" };
let ctrl = if keystroke.ctrl { "C-" } else { "" };
let alt = if keystroke.alt { "M-" } else { "" };
let cmd = if keystroke.cmd { "D-" } else { "" };
let end = if special { ">" } else { "" };
let key = format!("{start}{shift}{ctrl}{alt}{cmd}{}{end}", keystroke.key);
self.nvim
.input(&key)
.await
.expect("Could not input keystroke");
}
// If not running with a live neovim connection, this is a no-op
#[cfg(not(feature = "neovim"))]
pub async fn send_keystroke(&mut self, _keystroke_text: &str) {}
#[cfg(feature = "neovim")]
pub async fn set_state(&mut self, selection: Selection<Point>, text: &str) {
let nvim_buffer = self
.nvim
.get_current_buf()
.await
.expect("Could not get neovim buffer");
let lines = text
.split('\n')
.map(|line| line.to_string())
.collect::<Vec<_>>();
nvim_buffer
.set_lines(0, -1, false, lines)
.await
.expect("Could not set nvim buffer text");
self.nvim
.input("<escape>")
.await
.expect("Could not send escape to nvim");
self.nvim
.input("<escape>")
.await
.expect("Could not send escape to nvim");
let nvim_window = self
.nvim
.get_current_win()
.await
.expect("Could not get neovim window");
if !selection.is_empty() {
panic!("Setting neovim state with non empty selection not yet supported");
}
let cursor = selection.head();
nvim_window
.set_cursor((cursor.row as i64 + 1, cursor.column as i64))
.await
.expect("Could not set nvim cursor position");
}
#[cfg(not(feature = "neovim"))]
pub async fn set_state(&mut self, _selection: Selection<Point>, _text: &str) {}
#[cfg(feature = "neovim")]
pub async fn text(&mut self) -> String {
let nvim_buffer = self
.nvim
.get_current_buf()
.await
.expect("Could not get neovim buffer");
let text = nvim_buffer
.get_lines(0, -1, false)
.await
.expect("Could not get buffer text")
.join("\n");
self.data.push_back(NeovimData::Text(text.clone()));
text
}
#[cfg(not(feature = "neovim"))]
pub async fn text(&mut self) -> String {
if let Some(NeovimData::Text(text)) = self.data.pop_front() {
text
} else {
panic!("Invalid test data. Is test deterministic? Try running with '--features neovim' to regenerate");
}
}
#[cfg(feature = "neovim")]
pub async fn selection(&mut self) -> Range<Point> {
let cursor_row: u32 = self
.nvim
.command_output("echo line('.')")
.await
.unwrap()
.parse::<u32>()
.unwrap()
- 1; // Neovim rows start at 1
let cursor_col: u32 = self
.nvim
.command_output("echo col('.')")
.await
.unwrap()
.parse::<u32>()
.unwrap()
- 1; // Neovim columns start at 1
let (start, end) = if let Some(Mode::Visual { .. }) = self.mode().await {
self.nvim
.input("<escape>")
.await
.expect("Could not exit visual mode");
let nvim_buffer = self
.nvim
.get_current_buf()
.await
.expect("Could not get neovim buffer");
let (start_row, start_col) = nvim_buffer
.get_mark("<")
.await
.expect("Could not get selection start");
let (end_row, end_col) = nvim_buffer
.get_mark(">")
.await
.expect("Could not get selection end");
self.nvim
.input("gv")
.await
.expect("Could not reselect visual selection");
if cursor_row == start_row as u32 - 1 && cursor_col == start_col as u32 {
(
(end_row as u32 - 1, end_col as u32),
(start_row as u32 - 1, start_col as u32),
)
} else {
(
(start_row as u32 - 1, start_col as u32),
(end_row as u32 - 1, end_col as u32),
)
}
} else {
((cursor_row, cursor_col), (cursor_row, cursor_col))
};
self.data.push_back(NeovimData::Selection { start, end });
Point::new(start.0, start.1)..Point::new(end.0, end.1)
}
#[cfg(not(feature = "neovim"))]
pub async fn selection(&mut self) -> Range<Point> {
// Selection code fetches the mode. This emulates that.
let _mode = self.mode().await;
if let Some(NeovimData::Selection { start, end }) = self.data.pop_front() {
Point::new(start.0, start.1)..Point::new(end.0, end.1)
} else {
panic!("Invalid test data. Is test deterministic? Try running with '--features neovim' to regenerate");
}
}
#[cfg(feature = "neovim")]
pub async fn mode(&mut self) -> Option<Mode> {
let nvim_mode_text = self
.nvim
.get_mode()
.await
.expect("Could not get mode")
.into_iter()
.find_map(|(key, value)| {
if key.as_str() == Some("mode") {
Some(value.as_str().unwrap().to_owned())
} else {
None
}
})
.expect("Could not find mode value");
let mode = match nvim_mode_text.as_ref() {
"i" => Some(Mode::Insert),
"n" => Some(Mode::Normal),
"v" => Some(Mode::Visual { line: false }),
"V" => Some(Mode::Visual { line: true }),
_ => None,
};
self.data.push_back(NeovimData::Mode(mode.clone()));
mode
}
#[cfg(not(feature = "neovim"))]
pub async fn mode(&mut self) -> Option<Mode> {
if let Some(NeovimData::Mode(mode)) = self.data.pop_front() {
mode
} else {
panic!("Invalid test data. Is test deterministic? Try running with '--features neovim' to regenerate");
}
}
fn test_data_path(test_case_id: &str) -> PathBuf {
let mut data_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
data_path.push("test_data");
data_path.push(format!("{}.json", test_case_id));
data_path
}
#[cfg(not(feature = "neovim"))]
fn read_test_data(test_case_id: &str) -> VecDeque<NeovimData> {
let path = Self::test_data_path(test_case_id);
let json = std::fs::read_to_string(path).expect(
"Could not read test data. Is it generated? Try running test with '--features neovim'",
);
serde_json::from_str(&json)
.expect("Test data corrupted. Try regenerating it with '--features neovim'")
}
}
#[cfg(feature = "neovim")]
impl Deref for NeovimConnection {
type Target = Neovim<nvim_rs::compat::tokio::Compat<ChildStdin>>;
fn deref(&self) -> &Self::Target {
&self.nvim
}
}
#[cfg(feature = "neovim")]
impl DerefMut for NeovimConnection {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.nvim
}
}
#[cfg(feature = "neovim")]
impl Drop for NeovimConnection {
fn drop(&mut self) {
let path = Self::test_data_path(&self.test_case_id);
std::fs::create_dir_all(path.parent().unwrap())
.expect("Could not create test data directory");
let json = serde_json::to_string(&self.data).expect("Could not serialize test data");
std::fs::write(path, json).expect("Could not write out test data");
}
}
#[cfg(feature = "neovim")]
#[derive(Clone)]
struct NvimHandler {}
#[cfg(feature = "neovim")]
#[async_trait]
impl Handler for NvimHandler {
type Writer = nvim_rs::compat::tokio::Compat<ChildStdin>;
async fn handle_request(
&self,
_event_name: String,
_arguments: Vec<Value>,
_neovim: Neovim<Self::Writer>,
) -> Result<Value, Value> {
unimplemented!();
}
async fn handle_notify(
&self,
_event_name: String,
_arguments: Vec<Value>,
_neovim: Neovim<Self::Writer>,
) {
}
}
@@ -0,0 +1,69 @@
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 mode_after(mut self, mode_after: Mode) -> Self {
self.mode_after = mode_after;
self
}
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
}
}
@@ -1,13 +1,15 @@
use std::ops::{Deref, DerefMut};
use editor::test::EditorTestContext;
use gpui::{json::json, AppContext, ViewHandle};
use editor::test::editor_test_context::EditorTestContext;
use gpui::{json::json, AppContext, ContextHandle, ViewHandle};
use project::Project;
use search::{BufferSearchBar, ProjectSearchBar};
use workspace::{pane, AppState, WorkspaceHandle};
use crate::{state::Operator, *};
use super::VimBindingTestContext;
pub struct VimTestContext<'a> {
cx: EditorTestContext<'a>,
workspace: ViewHandle<Workspace>,
@@ -117,18 +119,18 @@ impl<'a> VimTestContext<'a> {
.read(|cx| cx.global::<Vim>().state.operator_stack.last().copied())
}
pub fn set_state(&mut self, text: &str, mode: Mode) {
pub fn set_state(&mut self, text: &str, mode: Mode) -> ContextHandle {
self.cx.update(|cx| {
Vim::update(cx, |vim, cx| {
vim.switch_mode(mode, false, cx);
})
});
self.cx.set_state(text);
self.cx.set_state(text)
}
pub fn assert_state(&mut self, text: &str, mode: Mode) {
self.assert_editor_state(text);
assert_eq!(self.mode(), mode);
assert_eq!(self.mode(), mode, "{}", self.assertion_context());
}
pub fn assert_binding<const COUNT: usize>(
@@ -142,8 +144,8 @@ impl<'a> VimTestContext<'a> {
self.set_state(initial_state, initial_mode);
self.cx.simulate_keystrokes(keystrokes);
self.cx.assert_editor_state(state_after);
assert_eq!(self.mode(), mode_after);
assert_eq!(self.active_operator(), None);
assert_eq!(self.mode(), mode_after, "{}", self.assertion_context());
assert_eq!(self.active_operator(), None, "{}", self.assertion_context());
}
pub fn binding<const COUNT: usize>(
@@ -168,67 +170,3 @@ impl<'a> DerefMut for VimTestContext<'a> {
&mut self.cx
}
}
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 mode_after(mut self, mode_after: Mode) -> Self {
self.mode_after = mode_after;
self
}
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
}
}
+30 -85
View File
@@ -1,10 +1,11 @@
#[cfg(test)]
mod vim_test_context;
mod test;
mod editor_events;
mod insert;
mod motion;
mod normal;
mod object;
mod state;
mod utils;
mod visual;
@@ -25,13 +26,17 @@ pub struct SwitchMode(pub Mode);
#[derive(Clone, Deserialize, PartialEq)]
pub struct PushOperator(pub Operator);
impl_actions!(vim, [SwitchMode, PushOperator]);
#[derive(Clone, Deserialize, PartialEq)]
struct Number(u8);
impl_actions!(vim, [Number, SwitchMode, PushOperator]);
pub fn init(cx: &mut MutableAppContext) {
editor_events::init(cx);
normal::init(cx);
visual::init(cx);
insert::init(cx);
object::init(cx);
motion::init(cx);
// Vim Actions
@@ -43,6 +48,9 @@ pub fn init(cx: &mut MutableAppContext) {
Vim::update(cx, |vim, cx| vim.push_operator(operator, cx))
},
);
cx.add_action(|_: &mut Workspace, n: &Number, cx: _| {
Vim::update(cx, |vim, cx| vim.push_number(n, cx));
});
// Editor Actions
cx.add_action(|_: &mut Editor, _: &Cancel, cx| {
@@ -143,12 +151,31 @@ impl Vim {
self.sync_vim_settings(cx);
}
fn push_number(&mut self, Number(number): &Number, cx: &mut MutableAppContext) {
if let Some(Operator::Number(current_number)) = self.active_operator() {
self.pop_operator(cx);
self.push_operator(Operator::Number(current_number * 10 + *number as usize), cx);
} else {
self.push_operator(Operator::Number(*number as usize), cx);
}
}
fn pop_operator(&mut self, cx: &mut MutableAppContext) -> 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.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
}
fn pop_number_operator(&mut self, cx: &mut MutableAppContext) -> usize {
let mut times = 1;
if let Some(Operator::Number(number)) = self.active_operator() {
times = number;
self.pop_operator(cx);
}
times
}
fn clear_operator(&mut self, cx: &mut MutableAppContext) {
self.state.operator_stack.clear();
self.sync_vim_settings(cx);
@@ -204,85 +231,3 @@ impl Vim {
}
}
}
#[cfg(test)]
mod test {
use indoc::indoc;
use search::BufferSearchBar;
use crate::{state::Mode, vim_test_context::VimTestContext};
#[gpui::test]
async fn test_initially_disabled(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestContext::new(cx, false).await;
cx.simulate_keystrokes(["h", "j", "k", "l"]);
cx.assert_editor_state("hjklˇ");
}
#[gpui::test]
async fn test_toggle_through_settings(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestContext::new(cx, true).await;
cx.simulate_keystroke("i");
assert_eq!(cx.mode(), Mode::Insert);
// Editor acts as though vim is disabled
cx.disable_vim();
cx.simulate_keystrokes(["h", "j", "k", "l"]);
cx.assert_editor_state("hjklˇ");
// Selections aren't changed if editor is blurred but vim-mode is still disabled.
cx.set_state("«hjklˇ»", Mode::Normal);
cx.assert_editor_state("«hjklˇ»");
cx.update_editor(|_, cx| cx.blur());
cx.assert_editor_state("«hjklˇ»");
cx.update_editor(|_, cx| cx.focus_self());
cx.assert_editor_state("«hjklˇ»");
// Enabling dynamically sets vim mode again and restores normal mode
cx.enable_vim();
assert_eq!(cx.mode(), Mode::Normal);
cx.simulate_keystrokes(["h", "h", "h", "l"]);
assert_eq!(cx.buffer_text(), "hjkl".to_owned());
cx.assert_editor_state("hˇjkl");
cx.simulate_keystrokes(["i", "T", "e", "s", "t"]);
cx.assert_editor_state("hTestˇjkl");
// Disabling and enabling resets to normal mode
assert_eq!(cx.mode(), Mode::Insert);
cx.disable_vim();
cx.enable_vim();
assert_eq!(cx.mode(), Mode::Normal);
}
#[gpui::test]
async fn test_buffer_search(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestContext::new(cx, true).await;
cx.set_state(
indoc! {"
The quick brown
fox juˇmps over
the lazy dog"},
Mode::Normal,
);
cx.simulate_keystroke("/");
// We now use a weird insert mode with selection when jumping to a single line editor
assert_eq!(cx.mode(), Mode::Insert);
let search_bar = cx.workspace(|workspace, cx| {
workspace
.active_pane()
.read(cx)
.toolbar()
.read(cx)
.item_of_type::<BufferSearchBar>()
.expect("Buffer search bar should be deployed")
});
search_bar.read_with(cx.cx, |bar, cx| {
assert_eq!(bar.query_editor.read(cx).text(cx), "jumps");
})
}
}
+129 -305
View File
@@ -6,7 +6,13 @@ use gpui::{actions, MutableAppContext, ViewContext};
use language::{AutoindentMode, SelectionGoal};
use workspace::Workspace;
use crate::{motion::Motion, state::Mode, utils::copy_selections_content, Vim};
use crate::{
motion::Motion,
object::Object,
state::{Mode, Operator},
utils::copy_selections_content,
Vim,
};
actions!(vim, [VisualDelete, VisualChange, VisualYank, VisualPaste]);
@@ -17,13 +23,15 @@ pub fn init(cx: &mut MutableAppContext) {
cx.add_action(paste);
}
pub fn visual_motion(motion: Motion, cx: &mut MutableAppContext) {
pub fn visual_motion(motion: Motion, times: usize, cx: &mut MutableAppContext) {
Vim::update(cx, |vim, cx| {
vim.update_active_editor(cx, |editor, cx| {
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
s.move_with(|map, selection| {
let (new_head, goal) = motion.move_point(map, selection.head(), selection.goal);
let was_reversed = selection.reversed;
let (new_head, goal) =
motion.move_point(map, selection.head(), selection.goal, times);
selection.set_head(new_head, goal);
if was_reversed && !selection.reversed {
@@ -43,6 +51,36 @@ pub fn visual_motion(motion: Motion, cx: &mut MutableAppContext) {
});
}
pub fn visual_object(object: Object, cx: &mut MutableAppContext) {
Vim::update(cx, |vim, cx| {
if let Operator::Object { around } = vim.pop_operator(cx) {
vim.update_active_editor(cx, |editor, cx| {
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
s.move_with(|map, selection| {
let head = selection.head();
if let Some(mut range) = object.range(map, head, around) {
if !range.is_empty() {
if let Some((_, end)) = map.reverse_chars_at(range.end).next() {
range.end = end;
}
if selection.is_empty() {
selection.start = range.start;
selection.end = range.end;
} else if selection.reversed {
selection.start = range.start;
} else {
selection.end = range.end;
}
}
}
});
});
});
}
});
}
pub fn change(_: &mut Workspace, _: &VisualChange, cx: &mut ViewContext<Workspace>) {
Vim::update(cx, |vim, cx| {
vim.update_active_editor(cx, |editor, cx| {
@@ -274,365 +312,151 @@ pub fn paste(_: &mut Workspace, _: &VisualPaste, cx: &mut ViewContext<Workspace>
mod test {
use indoc::indoc;
use crate::{state::Mode, vim_test_context::VimTestContext};
use crate::{
state::Mode,
test::{NeovimBackedTestContext, VimTestContext},
};
#[gpui::test]
async fn test_enter_visual_mode(cx: &mut gpui::TestAppContext) {
let cx = VimTestContext::new(cx, true).await;
let mut cx = cx
.binding(["v", "w", "j"])
.mode_after(Mode::Visual { line: false });
cx.assert(
indoc! {"
let mut cx = NeovimBackedTestContext::new(cx)
.await
.binding(["v", "w", "j"]);
cx.assert_all(indoc! {"
The ˇquick brown
fox jumps over
the lazy dog"},
indoc! {"
The «quick brown
fox jumps ˇ»over
the lazy dog"},
);
cx.assert(
indoc! {"
The quick brown
fox jumps over
the ˇlazy dog"},
indoc! {"
The quick brown
fox jumps over
the «lazy ˇ»dog"},
);
cx.assert(
indoc! {"
The quick brown
fox jumps ˇover
the lazy dog"},
indoc! {"
The quick brown
fox jumps «over
ˇ»the lazy dog"},
);
let mut cx = cx
.binding(["v", "b", "k"])
.mode_after(Mode::Visual { line: false });
cx.assert(
indoc! {"
the ˇlazy dog"})
.await;
let mut cx = cx.binding(["v", "b", "k"]);
cx.assert_all(indoc! {"
The ˇquick brown
fox jumps over
the lazy dog"},
indoc! {"
«ˇThe q»uick brown
fox jumps over
the lazy dog"},
);
cx.assert(
indoc! {"
The quick brown
fox jumps over
the ˇlazy dog"},
indoc! {"
The quick brown
«ˇfox jumps over
the l»azy dog"},
);
cx.assert(
indoc! {"
The quick brown
fox jumps ˇover
the lazy dog"},
indoc! {"
The «ˇquick brown
fox jumps o»ver
the lazy dog"},
);
the ˇlazy dog"})
.await;
}
#[gpui::test]
async fn test_visual_delete(cx: &mut gpui::TestAppContext) {
let cx = VimTestContext::new(cx, true).await;
let mut cx = cx.binding(["v", "w", "x"]);
cx.assert("The quick ˇbrown", "The quickˇ ");
let mut cx = cx.binding(["v", "w", "j", "x"]);
cx.assert(
indoc! {"
The ˇquick brown
fox jumps over
the lazy dog"},
indoc! {"
The ˇver
the lazy dog"},
);
// Test pasting code copied on delete
cx.simulate_keystrokes(["j", "p"]);
cx.assert_editor_state(indoc! {"
The ver
the lˇquick brown
fox jumps oazy dog"});
let mut cx = NeovimBackedTestContext::new(cx).await;
cx.assert(
indoc! {"
The quick brown
fox jumps over
the ˇlazy dog"},
indoc! {"
The quick brown
fox jumps over
the ˇog"},
);
cx.assert(
indoc! {"
The quick brown
fox jumps ˇover
the lazy dog"},
indoc! {"
The quick brown
fox jumps ˇhe lazy dog"},
);
let mut cx = cx.binding(["v", "b", "k", "x"]);
cx.assert(
cx.assert_binding_matches(["v", "w", "x"], "The quick ˇbrown")
.await;
cx.assert_binding_matches(
["v", "w", "j", "x"],
indoc! {"
The ˇquick brown
fox jumps over
the lazy dog"},
indoc! {"
ˇuick brown
)
.await;
// Test pasting code copied on delete
cx.simulate_shared_keystrokes(["j", "p"]).await;
cx.assert_state_matches().await;
let mut cx = cx.binding(["v", "w", "j", "x"]);
cx.assert_all(indoc! {"
The ˇquick brown
fox jumps over
the lazy dog"},
);
cx.assert(
indoc! {"
The quick brown
fox jumps over
the ˇlazy dog"},
indoc! {"
The quick brown
ˇazy dog"},
);
cx.assert(
indoc! {"
The quick brown
the ˇlazy dog"})
.await;
let mut cx = cx.binding(["v", "b", "k", "x"]);
cx.assert_all(indoc! {"
The ˇquick brown
fox jumps ˇover
the lazy dog"},
indoc! {"
The ˇver
the lazy dog"},
);
the ˇlazy dog"})
.await;
}
#[gpui::test]
async fn test_visual_line_delete(cx: &mut gpui::TestAppContext) {
let cx = VimTestContext::new(cx, true).await;
let mut cx = cx.binding(["shift-v", "x"]);
cx.assert(
indoc! {"
let mut cx = NeovimBackedTestContext::new(cx)
.await
.binding(["shift-v", "x"]);
cx.assert(indoc! {"
The quˇick brown
fox jumps over
the lazy dog"},
indoc! {"
fox juˇmps over
the lazy dog"},
);
the lazy dog"})
.await;
// Test pasting code copied on delete
cx.simulate_keystroke("p");
cx.assert_editor_state(indoc! {"
fox jumps over
ˇThe quick brown
the lazy dog"});
cx.simulate_shared_keystroke("p").await;
cx.assert_state_matches().await;
cx.assert(
indoc! {"
cx.assert_all(indoc! {"
The quick brown
fox juˇmps over
the lazy dog"},
indoc! {"
The quick brown
the laˇzy dog"},
);
cx.assert(
indoc! {"
The quick brown
fox jumps over
the laˇzy dog"},
indoc! {"
The quick brown
fox juˇmps over"},
);
the laˇzy dog"})
.await;
let mut cx = cx.binding(["shift-v", "j", "x"]);
cx.assert(
indoc! {"
cx.assert(indoc! {"
The quˇick brown
fox jumps over
the lazy dog"},
"the laˇzy dog",
);
the lazy dog"})
.await;
// Test pasting code copied on delete
cx.simulate_keystroke("p");
cx.assert_editor_state(indoc! {"
the lazy dog
ˇThe quick brown
fox jumps over"});
cx.simulate_shared_keystroke("p").await;
cx.assert_state_matches().await;
cx.assert(
indoc! {"
cx.assert_all(indoc! {"
The quick brown
fox juˇmps over
the lazy dog"},
"The quˇick brown",
);
cx.assert(
indoc! {"
The quick brown
fox jumps over
the laˇzy dog"},
indoc! {"
The quick brown
fox juˇmps over"},
);
the laˇzy dog"})
.await;
}
#[gpui::test]
async fn test_visual_change(cx: &mut gpui::TestAppContext) {
let cx = VimTestContext::new(cx, true).await;
let mut cx = cx.binding(["v", "w", "c"]).mode_after(Mode::Insert);
cx.assert("The quick ˇbrown", "The quick ˇ");
let mut cx = cx.binding(["v", "w", "j", "c"]).mode_after(Mode::Insert);
cx.assert(
indoc! {"
let mut cx = NeovimBackedTestContext::new(cx)
.await
.binding(["v", "w", "c"]);
cx.assert("The quick ˇbrown").await;
let mut cx = cx.binding(["v", "w", "j", "c"]);
cx.assert_all(indoc! {"
The ˇquick brown
fox jumps over
the lazy dog"},
indoc! {"
The ˇver
the lazy dog"},
);
cx.assert(
indoc! {"
The quick brown
fox jumps over
the ˇlazy dog"},
indoc! {"
The quick brown
fox jumps over
the ˇog"},
);
cx.assert(
indoc! {"
The quick brown
fox jumps ˇover
the lazy dog"},
indoc! {"
The quick brown
fox jumps ˇhe lazy dog"},
);
let mut cx = cx.binding(["v", "b", "k", "c"]).mode_after(Mode::Insert);
cx.assert(
indoc! {"
the ˇlazy dog"})
.await;
let mut cx = cx.binding(["v", "b", "k", "c"]);
cx.assert_all(indoc! {"
The ˇquick brown
fox jumps over
the lazy dog"},
indoc! {"
ˇuick brown
fox jumps over
the lazy dog"},
);
cx.assert(
indoc! {"
The quick brown
fox jumps over
the ˇlazy dog"},
indoc! {"
The quick brown
ˇazy dog"},
);
cx.assert(
indoc! {"
The quick brown
fox jumps ˇover
the lazy dog"},
indoc! {"
The ˇver
the lazy dog"},
);
the ˇlazy dog"})
.await;
}
#[gpui::test]
async fn test_visual_line_change(cx: &mut gpui::TestAppContext) {
let cx = VimTestContext::new(cx, true).await;
let mut cx = cx.binding(["shift-v", "c"]).mode_after(Mode::Insert);
cx.assert(
indoc! {"
let mut cx = NeovimBackedTestContext::new(cx)
.await
.binding(["shift-v", "c"]);
cx.assert(indoc! {"
The quˇick brown
fox jumps over
the lazy dog"},
indoc! {"
ˇ
fox jumps over
the lazy dog"},
);
the lazy dog"})
.await;
// Test pasting code copied on change
cx.simulate_keystrokes(["escape", "j", "p"]);
cx.assert_editor_state(indoc! {"
fox jumps over
ˇThe quick brown
the lazy dog"});
cx.simulate_shared_keystrokes(["escape", "j", "p"]).await;
cx.assert_state_matches().await;
cx.assert(
indoc! {"
cx.assert_all(indoc! {"
The quick brown
fox juˇmps over
the lazy dog"},
indoc! {"
The quick brown
ˇ
the lazy dog"},
);
cx.assert(
indoc! {"
The quick brown
fox jumps over
the laˇzy dog"},
indoc! {"
The quick brown
fox jumps over
ˇ"},
);
let mut cx = cx.binding(["shift-v", "j", "c"]).mode_after(Mode::Insert);
cx.assert(
indoc! {"
the laˇzy dog"})
.await;
let mut cx = cx.binding(["shift-v", "j", "c"]);
cx.assert(indoc! {"
The quˇick brown
fox jumps over
the lazy dog"},
indoc! {"
ˇ
the lazy dog"},
);
the lazy dog"})
.await;
// Test pasting code copied on delete
cx.simulate_keystrokes(["escape", "j", "p"]);
cx.assert_editor_state(indoc! {"
the lazy dog
ˇThe quick brown
fox jumps over"});
cx.assert(
indoc! {"
cx.simulate_shared_keystrokes(["escape", "j", "p"]).await;
cx.assert_state_matches().await;
cx.assert_all(indoc! {"
The quick brown
fox juˇmps over
the lazy dog"},
indoc! {"
The quick brown
ˇ"},
);
cx.assert(
indoc! {"
The quick brown
fox jumps over
the laˇzy dog"},
indoc! {"
The quick brown
fox jumps over
ˇ"},
);
the laˇzy dog"})
.await;
}
#[gpui::test]
@@ -741,7 +565,7 @@ mod test {
cx.assert_state(
indoc! {"
The quick brown
fox jumpsˇjumps over
fox jumpsjumpˇs over
the lazy dog"},
Mode::Normal,
);
@@ -0,0 +1 @@
[{"Text":""},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"This is a test"},{"Mode":"Normal"},{"Selection":{"start":[0,13],"end":[0,13]}},{"Mode":"Normal"}]
+1
View File
@@ -0,0 +1 @@
[{"Text":"The quick"},{"Mode":"Insert"},{"Selection":{"start":[0,6],"end":[0,6]}},{"Mode":"Insert"},{"Text":"The quick"},{"Mode":"Insert"},{"Selection":{"start":[0,9],"end":[0,9]}},{"Mode":"Insert"}]
+1
View File
@@ -0,0 +1 @@
[{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[0,9],"end":[0,9]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[0,10],"end":[0,10]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[3,0],"end":[3,0]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[3,10],"end":[3,10]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[3,0],"end":[3,0]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[3,10],"end":[3,10]}},{"Mode":"Normal"}]
+1
View File
@@ -0,0 +1 @@
[{"Text":"The quick\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"The quick\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Normal"},{"Text":"The quick\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[0,8],"end":[0,8]}},{"Mode":"Normal"}]
+1
View File
@@ -0,0 +1 @@
[{"Text":""},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":""},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"\nbrown fox\njumps over"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"The quick\n\njumps over"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick\nbrown fox\n"},{"Mode":"Insert"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Insert"},{"Text":"The quick\n\nbrown fox"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"}]
@@ -0,0 +1 @@
[{"Text":" Fox Jumps! Over the lazy."},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":" Fox Jumps! Over the lazy."},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":" Fox Jumps! Over the lazy."},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"The quick brown?Fox Jumps! Over the lazy."},{"Mode":"Insert"},{"Selection":{"start":[0,16],"end":[0,16]}},{"Mode":"Insert"},{"Text":"The quick brown? Over the lazy."},{"Mode":"Insert"},{"Selection":{"start":[0,17],"end":[0,17]}},{"Mode":"Insert"},{"Text":"The quick brown? Over the lazy."},{"Mode":"Insert"},{"Selection":{"start":[0,17],"end":[0,17]}},{"Mode":"Insert"},{"Text":"The quick brown? Over the lazy."},{"Mode":"Insert"},{"Selection":{"start":[0,17],"end":[0,17]}},{"Mode":"Insert"},{"Text":"The quick brown? Fox Jumps!Over the lazy."},{"Mode":"Insert"},{"Selection":{"start":[0,27],"end":[0,27]}},{"Mode":"Insert"},{"Text":"The quick brown? Fox Jumps! "},{"Mode":"Insert"},{"Selection":{"start":[0,28],"end":[0,28]}},{"Mode":"Insert"},{"Text":"The quick brown? Fox Jumps! "},{"Mode":"Insert"},{"Selection":{"start":[0,28],"end":[0,28]}},{"Mode":"Insert"},{"Text":"The quick brown? Fox Jumps! "},{"Mode":"Insert"},{"Selection":{"start":[0,28],"end":[0,28]}},{"Mode":"Insert"},{"Text":" The quick \nbrown fox jumps over\n"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":" The quick \nbrown fox jumps over\n"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":" The quick \nbrown fox jumps over\n"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":" The quick \nbrown fox jumps over\n"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":" The quick \nbrown fox jumps over\n"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"The quick brown \nfox jumps over\nthe lazy dog.The quick \nbrown fox jumps over\n"},{"Mode":"Insert"},{"Selection":{"start":[2,13],"end":[2,13]}},{"Mode":"Insert"},{"Text":"The quick brown \nfox jumps over\nthe lazy dog. \n"},{"Mode":"Insert"},{"Selection":{"start":[2,14],"end":[2,14]}},{"Mode":"Insert"},{"Text":"The quick brown \nfox jumps over\nthe lazy dog. \n"},{"Mode":"Insert"},{"Selection":{"start":[2,14],"end":[2,14]}},{"Mode":"Insert"},{"Text":" Brown fox jumps. "},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":" Brown fox jumps. "},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":" Brown fox jumps. "},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":" Brown fox jumps. "},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":" Brown fox jumps. "},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":" Brown fox jumps. "},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"The quick brown.)]'\" "},{"Mode":"Insert"},{"Selection":{"start":[0,21],"end":[0,21]}},{"Mode":"Insert"},{"Text":"The quick brown.)]'\" "},{"Mode":"Insert"},{"Selection":{"start":[0,21],"end":[0,21]}},{"Mode":"Insert"},{"Text":"The quick brown.)]'\" Brown fox jumps."},{"Mode":"Insert"},{"Selection":{"start":[0,37],"end":[0,37]}},{"Mode":"Insert"},{"Text":"Fox Jumps! Over the lazy."},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"Fox Jumps! Over the lazy."},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"Fox Jumps! Over the lazy."},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"The quick brown? Over the lazy."},{"Mode":"Insert"},{"Selection":{"start":[0,17],"end":[0,17]}},{"Mode":"Insert"},{"Text":"The quick brown? Over the lazy."},{"Mode":"Insert"},{"Selection":{"start":[0,17],"end":[0,17]}},{"Mode":"Insert"},{"Text":"The quick brown? Over the lazy."},{"Mode":"Insert"},{"Selection":{"start":[0,17],"end":[0,17]}},{"Mode":"Insert"},{"Text":"The quick brown? Fox Jumps!"},{"Mode":"Insert"},{"Selection":{"start":[0,27],"end":[0,27]}},{"Mode":"Insert"},{"Text":"The quick brown? Fox Jumps!"},{"Mode":"Insert"},{"Selection":{"start":[0,27],"end":[0,27]}},{"Mode":"Insert"},{"Text":"The quick brown? Fox Jumps!"},{"Mode":"Insert"},{"Selection":{"start":[0,27],"end":[0,27]}},{"Mode":"Insert"},{"Text":"The quick brown? Fox Jumps!"},{"Mode":"Insert"},{"Selection":{"start":[0,27],"end":[0,27]}},{"Mode":"Insert"},{"Text":"The quick \nbrown fox jumps over\n"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"The quick \nbrown fox jumps over\n"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"The quick \nbrown fox jumps over\n"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"InsLine truncated
@@ -0,0 +1 @@
[{"Text":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"},{"Mode":"Insert"},{"Selection":{"start":[0,6],"end":[0,6]}},{"Mode":"Insert"},{"Text":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"},{"Mode":"Insert"},{"Selection":{"start":[0,6],"end":[0,6]}},{"Mode":"Insert"},{"Text":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"},{"Mode":"Insert"},{"Selection":{"start":[0,6],"end":[0,6]}},{"Mode":"Insert"},{"Text":"Th)e ()qui()wn(\n)fox jumps ov(er\nthe lazy d)o(g"},{"Mode":"Insert"},{"Selection":{"start":[0,11],"end":[0,11]}},{"Mode":"Insert"},{"Text":"Th)e ()qui()wn(\n)fox jumps ov(er\nthe lazy d)o(g"},{"Mode":"Insert"},{"Selection":{"start":[0,11],"end":[0,11]}},{"Mode":"Insert"},{"Text":"Th)e ()qui()wn(\n)fox jumps ov(er\nthe lazy d)o(g"},{"Mode":"Insert"},{"Selection":{"start":[0,11],"end":[0,11]}},{"Mode":"Insert"},{"Text":"Th)e ()qui()wn(\n)fox jumps ov(er\nthe lazy d)o(g"},{"Mode":"Insert"},{"Selection":{"start":[0,11],"end":[0,11]}},{"Mode":"Insert"},{"Text":"Th)e ()qui(ck bro)wn(\n)fox jumps ov()o(g"},{"Mode":"Insert"},{"Selection":{"start":[1,14],"end":[1,14]}},{"Mode":"Insert"},{"Text":"Th)e ()qui(ck bro)wn(\n)fox jumps ov()o(g"},{"Mode":"Insert"},{"Selection":{"start":[1,14],"end":[1,14]}},{"Mode":"Insert"},{"Text":"Th)e ()qui(ck bro)wn(\n)fox jumps ov()o(g"},{"Mode":"Insert"},{"Selection":{"start":[1,14],"end":[1,14]}},{"Mode":"Insert"},{"Text":"Th)e ()qui(ck bro)wn(\n)fox jumps ov()o(g"},{"Mode":"Insert"},{"Selection":{"start":[1,14],"end":[1,14]}},{"Mode":"Insert"},{"Text":"Th)e ()qui(ck bro)wn(\n)fox jumps ov()o(g"},{"Mode":"Insert"},{"Selection":{"start":[1,14],"end":[1,14]}},{"Mode":"Insert"},{"Text":"Th)e ()qui(ck bro)wn(\n)fox jumps ov()o(g"},{"Mode":"Insert"},{"Selection":{"start":[1,14],"end":[1,14]}},{"Mode":"Insert"},{"Text":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"},{"Mode":"Normal"},{"Selection":{"start":[2,11],"end":[2,11]}},{"Mode":"Normal"},{"Text":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"},{"Mode":"Normal"},{"Selection":{"start":[2,12],"end":[2,12]}},{"Mode":"Normal"},{"Text":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"},{"Mode":"Normal"},{"Selection":{"start":[2,13],"end":[2,13]}},{"Mode":"Normal"},{"Text":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"Th)e qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"},{"Mode":"Insert"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Insert"},{"Text":"Th)e qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"},{"Mode":"Insert"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Insert"},{"Text":"Th)e qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"},{"Mode":"Insert"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Insert"},{"Text":"Th)e ()quiwn(\n)fox jumps ov(er\nthe lazy d)o(g"},{"Mode":"Insert"},{"Selection":{"start":[0,10],"end":[0,10]}},{"Mode":"Insert"},{"Text":"Th)e ()quiwn(\n)fox jumps ov(er\nthe lazy d)o(g"},{"Mode":"Insert"},{"Selection":{"start":[0,10],"end":[0,10]}},{"Mode":"Insert"},{"Text":"Th)e ()quiwn(\n)fox jumps ov(er\nthe lazy d)o(g"},{"Mode":"Insert"},{"Selection":{"start":[0,10],"end":[0,10]}},{"Mode":"Insert"},{"Text":"Th)e ()quiwn(\n)fox jumps ov(er\nthe lazy d)o(g"},{"Mode":"Insert"},{"Selection":{"start":[0,10],"end":[0,10]}},{"Mode":"Insert"},{"Text":"Th)e ()qui(ck bro)wn(\n)fox jumps ovo(g"},{"Mode":"Insert"},{"Selection":{"start":[1,13],"end":[1,13]}},{"Mode":"Insert"},{"Text":"Th)e ()qui(ck bro)wn(\n)fox jumps ovo(g"},{"Mode":"Insert"},{"Selection":{"start":[1,13],"end":[1,13]}},{"Mode":"Insert"},{"Text":"Th)e ()qui(ck bro)wn(\n)fox jumps ovo(g"},{"Mode":"Insert"},{"Selection":{"start":[1,13],"end":[1,13]}},{"Mode":"Insert"},{"Text":"Th)e ()qui(ck bro)wn(\n)fox jumps ovo(g"},{"Mode":"Insert"},{"Selection":{"start":[1,13],"end":[1,13]}},{"Mode":"Insert"},{"Text":"Th)e ()qui(ck bro)wn(\n)fox jumps ovo(g"},{"Mode":"Insert"},{"Selection":{"start":[1,13],"end":[1,13]}},{"Mode":"Insert"},{"Text":"Th)e ()qui(ck bro)wn(\n)fox jumps ovo(g"},{"Mode":"Insert"},{"Selection":{"start":[1,13],"end":[1,13]}},{"Mode":"Insert"},{"Text":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"},{"Mode":"Normal"},{"Selection":{"start":[2,11],"end":[2,11]}},{"Mode":"Normal"},{"Text":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"},{"Mode":"Normal"},{"Selection":{"start":[2,12],"end":[2,12]}},{"Mode":"Normal"},{"Text":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"},{"Mode":"Normal"},{"Selection":{"start":[2,13],"end":[2,13]}},{"Mode":"Normal"},{"Text":"Th]e []qui[ck bro]wn[\n]fox jumps ov[er\nthe lazy d]o[g"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"Th]e []qui[ck bro]wn[\n]fox jumps ov[er\nthe lazy d]o[g"},{"Mode":"Insert"},{"Selection":{"start":[0,6],Line truncated
@@ -0,0 +1 @@
[{"Text":"The quick \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"},{"Mode":"Insert"},{"Selection":{"start":[0,10],"end":[0,10]}},{"Mode":"Insert"},{"Text":"The quick \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"},{"Mode":"Insert"},{"Selection":{"start":[0,10],"end":[0,10]}},{"Mode":"Insert"},{"Text":"The quick brown\nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"},{"Mode":"Insert"},{"Selection":{"start":[0,15],"end":[0,15]}},{"Mode":"Insert"},{"Text":"The quick brown \nfox over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"},{"Mode":"Insert"},{"Selection":{"start":[1,4],"end":[1,4]}},{"Mode":"Insert"},{"Text":"The quick brown \nfox over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"},{"Mode":"Insert"},{"Selection":{"start":[1,4],"end":[1,4]}},{"Mode":"Insert"},{"Text":"The quick brown \nfox jumpsover\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"},{"Mode":"Insert"},{"Selection":{"start":[1,9],"end":[1,9]}},{"Mode":"Insert"},{"Text":"The quick brown \nfox jumps over\nthe lazy dog\n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"},{"Mode":"Insert"},{"Selection":{"start":[2,12],"end":[2,12]}},{"Mode":"Insert"},{"Text":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"},{"Mode":"Insert"},{"Selection":{"start":[3,0],"end":[3,0]}},{"Mode":"Insert"},{"Text":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"},{"Mode":"Insert"},{"Selection":{"start":[4,0],"end":[4,0]}},{"Mode":"Insert"},{"Text":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"},{"Mode":"Insert"},{"Selection":{"start":[5,0],"end":[5,0]}},{"Mode":"Insert"},{"Text":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\n-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"},{"Mode":"Insert"},{"Selection":{"start":[6,0],"end":[6,0]}},{"Mode":"Insert"},{"Text":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThequick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"},{"Mode":"Insert"},{"Selection":{"start":[6,3],"end":[6,3]}},{"Mode":"Insert"},{"Text":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe- brown \n \n \n fox-jumps over\nthe lazy dog \n\n"},{"Mode":"Insert"},{"Selection":{"start":[6,4],"end":[6,4]}},{"Mode":"Insert"},{"Text":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe- brown \n \n \n fox-jumps over\nthe lazy dog \n\n"},{"Mode":"Insert"},{"Selection":{"start":[6,4],"end":[6,4]}},{"Mode":"Insert"},{"Text":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quickbrown \n \n \n fox-jumps over\nthe lazy dog \n\n"},{"Mode":"Insert"},{"Selection":{"start":[6,9],"end":[6,9]}},{"Mode":"Insert"},{"Text":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick \n \n \n fox-jumps over\nthe lazy dog \n\n"},{"Mode":"Insert"},{"Selection":{"start":[6,10],"end":[6,10]}},{"Mode":"Insert"},{"Text":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown\n \n \n fox-jumps over\nthe lazy dog \n\n"},{"Mode":"Insert"},{"Selection":{"start":[6,15],"end":[6,15]}},{"Mode":"Insert"},{"Text":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n\n \n fox-jumps over\nthe lazy dog \n\n"},{"Mode":"Insert"},{"Selection":{"start":[7,0],"end":[7,0]}},{"Mode":"Insert"},{"Text":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n\n fox-jumps over\nthe lazy dog \n\n"},{"Mode":"Insert"},{"Selection":{"start":[8,0],"end":[8,0]}},{"Mode":"Insert"},{"Text":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \nfox-jumps over\nthe lazy dog \n\n"},{"Mode":"Insert"},{"Selection":{"start":[9,0],"end":[9,0]}},{"Mode":"Insert"},{"Text":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox- over\nthe lazy dog \n\n"},{"Mode":"Insert"},{"Selection":{"start":[9,6],"end":[9,6]}},{"Mode":"Insert"},{"Text":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog\n\n"},{"Mode":"Insert"},{"Selection":{"start":[10,12],"end":[10,12]}},{"Mode":"Insert"},{"Text":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"},{"Mode":"Insert"},{"Selection":{"start":[11,0],"end":[11,0]}},{"Mode":"Insert"},{"Text":"The quick \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"},{"Mode":"Insert"},{"Selection":{"start":[0,10],"end":[0,10]}},{"Mode":"Insert"},{"Text":"The quick \nfoLine truncated
+1
View File
@@ -0,0 +1 @@
[{"Text":""},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":""},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"brown fox\njumps over"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":"The quick\njumps over"},{"Mode":"Normal"},{"Selection":{"start":[1,6],"end":[1,6]}},{"Mode":"Normal"},{"Text":"The quick\nbrown fox"},{"Mode":"Normal"},{"Selection":{"start":[1,6],"end":[1,6]}},{"Mode":"Normal"},{"Text":"The quick\nbrown fox"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"}]
@@ -0,0 +1 @@
[{"Text":"Test"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"est"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"Tst"},{"Mode":"Normal"},{"Selection":{"start":[0,1],"end":[0,1]}},{"Mode":"Normal"},{"Text":"Tet"},{"Mode":"Normal"},{"Selection":{"start":[0,2],"end":[0,2]}},{"Mode":"Normal"},{"Text":"Test\ntest"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"}]
@@ -0,0 +1 @@
[{"Text":" Fox Jumps! Over the lazy."},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":" Fox Jumps! Over the lazy."},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":" Fox Jumps! Over the lazy."},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"The quick brown?Fox Jumps! Over the lazy."},{"Mode":"Normal"},{"Selection":{"start":[0,16],"end":[0,16]}},{"Mode":"Normal"},{"Text":"The quick brown? Over the lazy."},{"Mode":"Normal"},{"Selection":{"start":[0,17],"end":[0,17]}},{"Mode":"Normal"},{"Text":"The quick brown? Over the lazy."},{"Mode":"Normal"},{"Selection":{"start":[0,17],"end":[0,17]}},{"Mode":"Normal"},{"Text":"The quick brown? Over the lazy."},{"Mode":"Normal"},{"Selection":{"start":[0,17],"end":[0,17]}},{"Mode":"Normal"},{"Text":"The quick brown? Fox Jumps!Over the lazy."},{"Mode":"Normal"},{"Selection":{"start":[0,27],"end":[0,27]}},{"Mode":"Normal"},{"Text":"The quick brown? Fox Jumps! "},{"Mode":"Normal"},{"Selection":{"start":[0,27],"end":[0,27]}},{"Mode":"Normal"},{"Text":"The quick brown? Fox Jumps! "},{"Mode":"Normal"},{"Selection":{"start":[0,27],"end":[0,27]}},{"Mode":"Normal"},{"Text":"The quick brown? Fox Jumps! "},{"Mode":"Normal"},{"Selection":{"start":[0,27],"end":[0,27]}},{"Mode":"Normal"},{"Text":" The quick \nbrown fox jumps over\n"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":" The quick \nbrown fox jumps over\n"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":" The quick \nbrown fox jumps over\n"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":" The quick \nbrown fox jumps over\n"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":" The quick \nbrown fox jumps over\n"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"The quick brown \nfox jumps over\nthe lazy dog.The quick \nbrown fox jumps over\n"},{"Mode":"Normal"},{"Selection":{"start":[2,13],"end":[2,13]}},{"Mode":"Normal"},{"Text":"The quick brown \nfox jumps over\nthe lazy dog. \n"},{"Mode":"Normal"},{"Selection":{"start":[2,13],"end":[2,13]}},{"Mode":"Normal"},{"Text":"The quick brown \nfox jumps over\nthe lazy dog. \n"},{"Mode":"Normal"},{"Selection":{"start":[2,13],"end":[2,13]}},{"Mode":"Normal"},{"Text":" Brown fox jumps. "},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":" Brown fox jumps. "},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":" Brown fox jumps. "},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":" Brown fox jumps. "},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":" Brown fox jumps. "},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":" Brown fox jumps. "},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"The quick brown.)]'\" "},{"Mode":"Normal"},{"Selection":{"start":[0,21],"end":[0,21]}},{"Mode":"Normal"},{"Text":"The quick brown.)]'\" "},{"Mode":"Normal"},{"Selection":{"start":[0,21],"end":[0,21]}},{"Mode":"Normal"},{"Text":"The quick brown.)]'\" Brown fox jumps."},{"Mode":"Normal"},{"Selection":{"start":[0,36],"end":[0,36]}},{"Mode":"Normal"},{"Text":"Fox Jumps! Over the lazy."},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"Fox Jumps! Over the lazy."},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"Fox Jumps! Over the lazy."},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"The quick brown? Over the lazy."},{"Mode":"Normal"},{"Selection":{"start":[0,17],"end":[0,17]}},{"Mode":"Normal"},{"Text":"The quick brown? Over the lazy."},{"Mode":"Normal"},{"Selection":{"start":[0,17],"end":[0,17]}},{"Mode":"Normal"},{"Text":"The quick brown? Over the lazy."},{"Mode":"Normal"},{"Selection":{"start":[0,17],"end":[0,17]}},{"Mode":"Normal"},{"Text":"The quick brown? Fox Jumps!"},{"Mode":"Normal"},{"Selection":{"start":[0,26],"end":[0,26]}},{"Mode":"Normal"},{"Text":"The quick brown? Fox Jumps!"},{"Mode":"Normal"},{"Selection":{"start":[0,26],"end":[0,26]}},{"Mode":"Normal"},{"Text":"The quick brown? Fox Jumps!"},{"Mode":"Normal"},{"Selection":{"start":[0,26],"end":[0,26]}},{"Mode":"Normal"},{"Text":"The quick brown? Fox Jumps!"},{"Mode":"Normal"},{"Selection":{"start":[0,26],"end":[0,26]}},{"Mode":"Normal"},{"Text":"The quick \nbrown fox jumps over\n"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"The quick \nbrown fox jumps over\n"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"The quick \nbrown fox jumps over\n"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"NorLine truncated
@@ -0,0 +1 @@
[{"Text":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"},{"Mode":"Normal"},{"Selection":{"start":[0,6],"end":[0,6]}},{"Mode":"Normal"},{"Text":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"},{"Mode":"Normal"},{"Selection":{"start":[0,6],"end":[0,6]}},{"Mode":"Normal"},{"Text":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"},{"Mode":"Normal"},{"Selection":{"start":[0,6],"end":[0,6]}},{"Mode":"Normal"},{"Text":"Th)e ()qui()wn(\n)fox jumps ov(er\nthe lazy d)o(g"},{"Mode":"Normal"},{"Selection":{"start":[0,11],"end":[0,11]}},{"Mode":"Normal"},{"Text":"Th)e ()qui()wn(\n)fox jumps ov(er\nthe lazy d)o(g"},{"Mode":"Normal"},{"Selection":{"start":[0,11],"end":[0,11]}},{"Mode":"Normal"},{"Text":"Th)e ()qui()wn(\n)fox jumps ov(er\nthe lazy d)o(g"},{"Mode":"Normal"},{"Selection":{"start":[0,11],"end":[0,11]}},{"Mode":"Normal"},{"Text":"Th)e ()qui()wn(\n)fox jumps ov(er\nthe lazy d)o(g"},{"Mode":"Normal"},{"Selection":{"start":[0,11],"end":[0,11]}},{"Mode":"Normal"},{"Text":"Th)e ()qui(ck bro)wn(\n)fox jumps ov()o(g"},{"Mode":"Normal"},{"Selection":{"start":[1,14],"end":[1,14]}},{"Mode":"Normal"},{"Text":"Th)e ()qui(ck bro)wn(\n)fox jumps ov()o(g"},{"Mode":"Normal"},{"Selection":{"start":[1,14],"end":[1,14]}},{"Mode":"Normal"},{"Text":"Th)e ()qui(ck bro)wn(\n)fox jumps ov()o(g"},{"Mode":"Normal"},{"Selection":{"start":[1,14],"end":[1,14]}},{"Mode":"Normal"},{"Text":"Th)e ()qui(ck bro)wn(\n)fox jumps ov()o(g"},{"Mode":"Normal"},{"Selection":{"start":[1,14],"end":[1,14]}},{"Mode":"Normal"},{"Text":"Th)e ()qui(ck bro)wn(\n)fox jumps ov()o(g"},{"Mode":"Normal"},{"Selection":{"start":[1,14],"end":[1,14]}},{"Mode":"Normal"},{"Text":"Th)e ()qui(ck bro)wn(\n)fox jumps ov()o(g"},{"Mode":"Normal"},{"Selection":{"start":[1,14],"end":[1,14]}},{"Mode":"Normal"},{"Text":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"},{"Mode":"Normal"},{"Selection":{"start":[2,11],"end":[2,11]}},{"Mode":"Normal"},{"Text":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"},{"Mode":"Normal"},{"Selection":{"start":[2,12],"end":[2,12]}},{"Mode":"Normal"},{"Text":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"},{"Mode":"Normal"},{"Selection":{"start":[2,13],"end":[2,13]}},{"Mode":"Normal"},{"Text":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"Th)e qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":"Th)e qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":"Th)e qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":"Th)e ()quiwn(\n)fox jumps ov(er\nthe lazy d)o(g"},{"Mode":"Normal"},{"Selection":{"start":[0,10],"end":[0,10]}},{"Mode":"Normal"},{"Text":"Th)e ()quiwn(\n)fox jumps ov(er\nthe lazy d)o(g"},{"Mode":"Normal"},{"Selection":{"start":[0,10],"end":[0,10]}},{"Mode":"Normal"},{"Text":"Th)e ()quiwn(\n)fox jumps ov(er\nthe lazy d)o(g"},{"Mode":"Normal"},{"Selection":{"start":[0,10],"end":[0,10]}},{"Mode":"Normal"},{"Text":"Th)e ()quiwn(\n)fox jumps ov(er\nthe lazy d)o(g"},{"Mode":"Normal"},{"Selection":{"start":[0,10],"end":[0,10]}},{"Mode":"Normal"},{"Text":"Th)e ()qui(ck bro)wn(\n)fox jumps ovo(g"},{"Mode":"Normal"},{"Selection":{"start":[1,13],"end":[1,13]}},{"Mode":"Normal"},{"Text":"Th)e ()qui(ck bro)wn(\n)fox jumps ovo(g"},{"Mode":"Normal"},{"Selection":{"start":[1,13],"end":[1,13]}},{"Mode":"Normal"},{"Text":"Th)e ()qui(ck bro)wn(\n)fox jumps ovo(g"},{"Mode":"Normal"},{"Selection":{"start":[1,13],"end":[1,13]}},{"Mode":"Normal"},{"Text":"Th)e ()qui(ck bro)wn(\n)fox jumps ovo(g"},{"Mode":"Normal"},{"Selection":{"start":[1,13],"end":[1,13]}},{"Mode":"Normal"},{"Text":"Th)e ()qui(ck bro)wn(\n)fox jumps ovo(g"},{"Mode":"Normal"},{"Selection":{"start":[1,13],"end":[1,13]}},{"Mode":"Normal"},{"Text":"Th)e ()qui(ck bro)wn(\n)fox jumps ovo(g"},{"Mode":"Normal"},{"Selection":{"start":[1,13],"end":[1,13]}},{"Mode":"Normal"},{"Text":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"},{"Mode":"Normal"},{"Selection":{"start":[2,11],"end":[2,11]}},{"Mode":"Normal"},{"Text":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"},{"Mode":"Normal"},{"Selection":{"start":[2,12],"end":[2,12]}},{"Mode":"Normal"},{"Text":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"},{"Mode":"Normal"},{"Selection":{"start":[2,13],"end":[2,13]}},{"Mode":"Normal"},{"Text":"Th]e []qui[ck bro]wn[\n]fox jumps ov[er\nthe lazy d]o[g"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"Th]e []qui[ck bro]wn[\n]fox jumps ov[er\nthe lazy d]o[g"},{"Mode":"Normal"},{"Selection":{"start":[0,6],Line truncated
@@ -0,0 +1 @@
[{"Text":"The q\nbrown fox"},{"Mode":"Normal"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Normal"},{"Text":"The quick\n\nbrown fox"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"}]
@@ -0,0 +1 @@
[{"Text":"The quick \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"},{"Mode":"Normal"},{"Selection":{"start":[0,10],"end":[0,10]}},{"Mode":"Normal"},{"Text":"The quick \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"},{"Mode":"Normal"},{"Selection":{"start":[0,10],"end":[0,10]}},{"Mode":"Normal"},{"Text":"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":"Normal"},{"Selection":{"start":[0,14],"end":[0,14]}},{"Mode":"Normal"},{"Text":"The quick brown \nfox over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"},{"Mode":"Normal"},{"Selection":{"start":[1,4],"end":[1,4]}},{"Mode":"Normal"},{"Text":"The quick brown \nfox over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"},{"Mode":"Normal"},{"Selection":{"start":[1,4],"end":[1,4]}},{"Mode":"Normal"},{"Text":"The quick brown \nfox jumpsover\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"},{"Mode":"Normal"},{"Selection":{"start":[1,9],"end":[1,9]}},{"Mode":"Normal"},{"Text":"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":"Normal"},{"Selection":{"start":[2,11],"end":[2,11]}},{"Mode":"Normal"},{"Text":"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":"Normal"},{"Selection":{"start":[3,0],"end":[3,0]}},{"Mode":"Normal"},{"Text":"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":"Normal"},{"Selection":{"start":[4,0],"end":[4,0]}},{"Mode":"Normal"},{"Text":"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":"Normal"},{"Selection":{"start":[5,0],"end":[5,0]}},{"Mode":"Normal"},{"Text":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\n-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"},{"Mode":"Normal"},{"Selection":{"start":[6,0],"end":[6,0]}},{"Mode":"Normal"},{"Text":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThequick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"},{"Mode":"Normal"},{"Selection":{"start":[6,3],"end":[6,3]}},{"Mode":"Normal"},{"Text":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe- brown \n \n \n fox-jumps over\nthe lazy dog \n\n"},{"Mode":"Normal"},{"Selection":{"start":[6,4],"end":[6,4]}},{"Mode":"Normal"},{"Text":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe- brown \n \n \n fox-jumps over\nthe lazy dog \n\n"},{"Mode":"Normal"},{"Selection":{"start":[6,4],"end":[6,4]}},{"Mode":"Normal"},{"Text":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quickbrown \n \n \n fox-jumps over\nthe lazy dog \n\n"},{"Mode":"Normal"},{"Selection":{"start":[6,9],"end":[6,9]}},{"Mode":"Normal"},{"Text":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick \n \n \n fox-jumps over\nthe lazy dog \n\n"},{"Mode":"Normal"},{"Selection":{"start":[6,10],"end":[6,10]}},{"Mode":"Normal"},{"Text":"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":"Normal"},{"Selection":{"start":[6,14],"end":[6,14]}},{"Mode":"Normal"},{"Text":"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":"Normal"},{"Selection":{"start":[7,0],"end":[7,0]}},{"Mode":"Normal"},{"Text":"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":"Normal"},{"Selection":{"start":[8,0],"end":[8,0]}},{"Mode":"Normal"},{"Text":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \nfox-jumps over\nthe lazy dog \n\n"},{"Mode":"Normal"},{"Selection":{"start":[9,0],"end":[9,0]}},{"Mode":"Normal"},{"Text":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox- over\nthe lazy dog \n\n"},{"Mode":"Normal"},{"Selection":{"start":[9,6],"end":[9,6]}},{"Mode":"Normal"},{"Text":"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":"Normal"},{"Selection":{"start":[10,11],"end":[10,11]}},{"Mode":"Normal"},{"Text":"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":"Normal"},{"Selection":{"start":[11,0],"end":[11,0]}},{"Mode":"Normal"},{"Text":"The quick \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"},{"Mode":"Normal"},{"Selection":{"start":[0,10],"end":[0,10]}},{"Mode":"Normal"},{"Text":"The quick \nfoLine truncated
+1
View File
@@ -0,0 +1 @@
[{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[0,8],"end":[0,8]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[0,9],"end":[0,9]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[0,14],"end":[0,14]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[3,8],"end":[3,8]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[3,13],"end":[3,13]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[4,2],"end":[4,2]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[4,2],"end":[4,2]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[0,14],"end":[0,14]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[0,14],"end":[0,14]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[0,14],"end":[0,14]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[3,8],"end":[3,8]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[3,13],"end":[3,13]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[4,2],"end":[4,2]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[4,2],"end":[4,2]}},{"Mode":"Normal"}]
@@ -0,0 +1 @@
[{"Text":"The quick brown\nfox jumps over\nthe lazy dog"},{"Mode":{"Visual":{"line":false}}},{"Selection":{"start":[0,4],"end":[1,10]}},{"Mode":{"Visual":{"line":false}}},{"Text":"The quick brown\nfox jumps over\nthe lazy dog"},{"Mode":{"Visual":{"line":false}}},{"Selection":{"start":[1,10],"end":[2,0]}},{"Mode":{"Visual":{"line":false}}},{"Text":"The quick brown\nfox jumps over\nthe lazy dog"},{"Mode":{"Visual":{"line":false}}},{"Selection":{"start":[2,4],"end":[2,9]}},{"Mode":{"Visual":{"line":false}}},{"Text":"The quick brown\nfox jumps over\nthe lazy dog"},{"Mode":{"Visual":{"line":false}}},{"Selection":{"start":[0,4],"end":[0,0]}},{"Mode":{"Visual":{"line":false}}},{"Text":"The quick brown\nfox jumps over\nthe lazy dog"},{"Mode":{"Visual":{"line":false}}},{"Selection":{"start":[1,10],"end":[0,4]}},{"Mode":{"Visual":{"line":false}}},{"Text":"The quick brown\nfox jumps over\nthe lazy dog"},{"Mode":{"Visual":{"line":false}}},{"Selection":{"start":[2,4],"end":[1,0]}},{"Mode":{"Visual":{"line":false}}}]
+1
View File
@@ -0,0 +1 @@
[{"Text":"The quick\n\nbrown fox jumps\nover the lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":"The quick\n\nbrown fox jumps\nover the lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":"The quick\n\nbrown fox jumps\nover the lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[0,8],"end":[0,8]}},{"Mode":"Normal"},{"Text":"\n\nbrown fox jumps\nover the lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"\n\nbrown fox jumps\nover the lazydog"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"}]
+1
View File
@@ -0,0 +1 @@
[{"Text":"The quick\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"The quick\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Normal"},{"Text":"The quick\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"}]
@@ -0,0 +1 @@
[{"Text":"\nThe quick\nbrown fox "},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"\nThe quick\nbrown fox "},{"Mode":"Insert"},{"Selection":{"start":[1,9],"end":[1,9]}},{"Mode":"Insert"},{"Text":"\nThe quick\nbrown fox "},{"Mode":"Insert"},{"Selection":{"start":[2,10],"end":[2,10]}},{"Mode":"Insert"}]
@@ -0,0 +1 @@
[{"Text":"The quick"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":" The quick"},{"Mode":"Insert"},{"Selection":{"start":[0,1],"end":[0,1]}},{"Mode":"Insert"},{"Text":""},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"The quick\nbrown fox"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"\nThe quick"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"}]
@@ -0,0 +1 @@
[{"Text":"\n"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"\nThe quick"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"\nThe quick\nbrown fox\njumps over"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"The quick\n\nbrown fox\njumps over"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick\nbrown fox\n\njumps over"},{"Mode":"Insert"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Insert"},{"Text":"The quick\n\n\nbrown fox"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"}]
+1
View File
@@ -0,0 +1 @@
[{"Text":"The quick brown\nfox jumps"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"},{"Text":"The quick brown\nfox jumps"},{"Mode":"Normal"},{"Selection":{"start":[1,5],"end":[1,5]}},{"Mode":"Normal"},{"Text":"The quick brown\nfox jumps"},{"Mode":"Normal"},{"Selection":{"start":[1,8],"end":[1,8]}},{"Mode":"Normal"},{"Text":"The quick brown\nfox jumps"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"}]
@@ -0,0 +1 @@
[{"Text":"The quick\n\nbrown fox jumps\nover the lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[3,4],"end":[3,4]}},{"Mode":"Normal"},{"Text":"The quick\n\nbrown fox jumps\nover the lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[3,4],"end":[3,4]}},{"Mode":"Normal"},{"Text":"The quick\n\nbrown fox jumps\nover the lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[3,16],"end":[3,16]}},{"Mode":"Normal"},{"Text":"The quick\n\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[2,4],"end":[2,4]}},{"Mode":"Normal"},{"Text":"The quick\n\n"},{"Mode":"Normal"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Normal"}]
@@ -0,0 +1 @@
[{"Text":"The quick"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":" The quick"},{"Mode":"Normal"},{"Selection":{"start":[0,1],"end":[0,1]}},{"Mode":"Normal"},{"Text":""},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"The quick\nbrown fox"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"\nThe quick"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":" \nThe quick"},{"Mode":"Normal"},{"Selection":{"start":[0,3],"end":[0,3]}},{"Mode":"Normal"}]
@@ -0,0 +1 @@
[{"Text":"The quick\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[0,8],"end":[0,8]}},{"Mode":"Normal"},{"Text":"The quick\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[0,8],"end":[0,8]}},{"Mode":"Normal"},{"Text":"The quick\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[0,8],"end":[0,8]}},{"Mode":"Normal"},{"Text":"The quick\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[1,4],"end":[1,4]}},{"Mode":"Normal"},{"Text":"The quick\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[1,4],"end":[1,4]}},{"Mode":"Normal"},{"Text":"The quick\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"The quick\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"The quick\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"The quick\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"},{"Text":"The quick\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"}]
+1
View File
@@ -0,0 +1 @@
[{"Text":"The quick\nbrown fox jumps"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"The quick\nbrown fox jumps"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":"The quick\nbrown fox jumps"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"The quick\nbrown fox jumps"},{"Mode":"Normal"},{"Selection":{"start":[0,7],"end":[0,7]}},{"Mode":"Normal"},{"Text":"The quick\nbrown fox jumps"},{"Mode":"Normal"},{"Selection":{"start":[0,8],"end":[0,8]}},{"Mode":"Normal"}]
+1
View File
@@ -0,0 +1 @@
[{"Text":"The quick\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[0,1],"end":[0,1]}},{"Mode":"Normal"},{"Text":"The quick\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[0,6],"end":[0,6]}},{"Mode":"Normal"},{"Text":"The quick\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[0,8],"end":[0,8]}},{"Mode":"Normal"},{"Text":"The quick\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[1,1],"end":[1,1]}},{"Mode":"Normal"},{"Text":"The quick\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[1,4],"end":[1,4]}},{"Mode":"Normal"}]
+1
View File
@@ -0,0 +1 @@
[{"Text":""},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"test"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"}]
+1
View File
@@ -0,0 +1 @@
[{"Text":"\n"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick\n"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick\n\nbrown fox\njumps over"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick\nbrown fox\n\njumps over"},{"Mode":"Insert"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Insert"},{"Text":"The quick\nbrown fox\njumps over\n"},{"Mode":"Insert"},{"Selection":{"start":[3,0],"end":[3,0]}},{"Mode":"Insert"},{"Text":"The quick\n\n\nbrown fox"},{"Mode":"Insert"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Insert"},{"Text":"fn test() {\n println!();\n \n}\n"},{"Mode":"Insert"},{"Selection":{"start":[2,4],"end":[2,4]}},{"Mode":"Insert"},{"Text":"fn test() {\n\n println!();\n}"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"}]
+1
View File
@@ -0,0 +1 @@
[{"Text":"The quick brown\nthe lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[1,6],"end":[1,6]}},{"Mode":"Normal"},{"Text":"The quick brown\nthe lazy dog\nfox jumps over"},{"Mode":"Normal"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Normal"},{"Text":"The quick brown\nfox jumps overjumps o\nthe lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[1,20],"end":[1,20]}},{"Mode":"Normal"}]
@@ -0,0 +1 @@
[{"Text":"The ick brown\n\nfox jumps-over\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Insert"},{"Text":"The brown\n\nfox jumps-over\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Insert"},{"Text":"The quick n\n\nfox jumps-over\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[0,10],"end":[0,10]}},{"Mode":"Insert"},{"Text":"The quick \n\nfox jumps-over\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[0,10],"end":[0,10]}},{"Mode":"Insert"},{"Text":"The quick brown\n\nfox jumps-over\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick brown\n\njumps-over\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Insert"},{"Text":"The quick brown\n\nfox -over\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[2,4],"end":[2,4]}},{"Mode":"Insert"},{"Text":"The quick brown\n\nfox jumpsover\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[2,9],"end":[2,9]}},{"Mode":"Insert"},{"Text":"The quick brown\n\nfox jumps-ver\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[2,10],"end":[2,10]}},{"Mode":"Insert"},{"Text":"The quick brown\n\nfox jumps-\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[2,10],"end":[2,10]}},{"Mode":"Insert"},{"Text":"ick brown\n\nfox jumps-over\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":" brown\n\nfox jumps-over\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"The n\n\nfox jumps-over\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Insert"},{"Text":"The \n\nfox jumps-over\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Insert"},{"Text":"The quick \nfox jumps-over\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[0,10],"end":[0,10]}},{"Mode":"Insert"},{"Text":"The quick brown\njumps-over\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick brown\n\n-over\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Insert"},{"Text":"The quick brown\n\nfox over\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[2,4],"end":[2,4]}},{"Mode":"Insert"},{"Text":"The quick brown\n\nfox jumpsver\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[2,9],"end":[2,9]}},{"Mode":"Insert"},{"Text":"The quick brown\n\nfox jumps\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[2,9],"end":[2,9]}},{"Mode":"Insert"},{"Text":"ick brown\n\nfox jumps-over\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":" brown\n\nfox jumps-over\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"n\n\nfox jumps-over\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"\n\nfox jumps-over\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"The \nfox jumps-over\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Insert"},{"Text":"The quick jumps-over\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[0,10],"end":[0,10]}},{"Mode":"Insert"},{"Text":"The quick brown\n-over\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick brown\n\nover\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Insert"},{"Text":"The quick brown\n\nfox ver\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[2,4],"end":[2,4]}},{"Mode":"Insert"},{"Text":"The quick brown\n\nfox \nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[2,4],"end":[2,4]}},{"Mode":"Insert"},{"Text":"ick brown\n\nfox jumps-over\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":" brown\n\nfox jumps-over\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"n\n\nfox jumps-over\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"\n\nfox jumps-over\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"\nfox jumps-over\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"The jumps-over\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Insert"},{"Text":"The quick -over\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[0,10],"end":[0,10]}},{"Mode":"Insert"},{"Text":"The quick brown\nover\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick broLine truncated
@@ -0,0 +1 @@
[{"Text":" quick brown\n\nfox jumps-over\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"The qu brown\n\nfox jumps-over\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[0,6],"end":[0,6]}},{"Mode":"Insert"},{"Text":"The quick\n\nfox jumps-over\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[0,9],"end":[0,9]}},{"Mode":"Insert"},{"Text":"The quick brow jumps-over\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[0,14],"end":[0,14]}},{"Mode":"Insert"},{"Text":"The quick brown\n jumps-over\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick brown\n\n jumps-over\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Insert"},{"Text":"The quick brown\n\nfox -over\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[2,4],"end":[2,4]}},{"Mode":"Insert"},{"Text":"The quick brown\n\nfox jumps\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[2,9],"end":[2,9]}},{"Mode":"Insert"},{"Text":"The quick brown\n\nfox jumps-\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[2,10],"end":[2,10]}},{"Mode":"Insert"},{"Text":"The quick brown\n\nfox jumps-o\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[2,11],"end":[2,11]}},{"Mode":"Insert"},{"Text":"The quick brown\n\nfox jumps-over\n lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[3,0],"end":[3,0]}},{"Mode":"Insert"},{"Text":" brown\n\nfox jumps-over\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"The qu\n\nfox jumps-over\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[0,6],"end":[0,6]}},{"Mode":"Insert"},{"Text":"The quick jumps-over\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[0,9],"end":[0,9]}},{"Mode":"Insert"},{"Text":"The quick brow-over\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[0,14],"end":[0,14]}},{"Mode":"Insert"},{"Text":"The quick brown\n-over\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick brown\n\n-over\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Insert"},{"Text":"The quick brown\n\nfox over\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[2,4],"end":[2,4]}},{"Mode":"Insert"},{"Text":"The quick brown\n\nfox jumps lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[2,9],"end":[2,9]}},{"Mode":"Insert"},{"Text":"The quick brown\n\nfox jumps- lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[2,10],"end":[2,10]}},{"Mode":"Insert"},{"Text":"The quick brown\n\nfox jumps-o lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[2,11],"end":[2,11]}},{"Mode":"Insert"},{"Text":"The quick brown\n\nfox jumps-over\n dog\n"},{"Mode":"Insert"},{"Selection":{"start":[3,0],"end":[3,0]}},{"Mode":"Insert"},{"Text":"\n\nfox jumps-over\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"The qu jumps-over\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[0,6],"end":[0,6]}},{"Mode":"Insert"},{"Text":"The quick-over\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[0,9],"end":[0,9]}},{"Mode":"Insert"},{"Text":"The quick browover\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[0,14],"end":[0,14]}},{"Mode":"Insert"},{"Text":"The quick brown\nover\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick brown\n\nover\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Insert"},{"Text":"The quick brown\n\nfox \nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[2,4],"end":[2,4]}},{"Mode":"Insert"},{"Text":"The quick brown\n\nfox jumps dog\n"},{"Mode":"Insert"},{"Selection":{"start":[2,9],"end":[2,9]}},{"Mode":"Insert"},{"Text":"The quick brown\n\nfox jumps- dog\n"},{"Mode":"Insert"},{"Selection":{"start":[2,10],"end":[2,10]}},{"Mode":"Insert"},{"Text":"The quick brown\n\nfox jumps-o dog\n"},{"Mode":"Insert"},{"Selection":{"start":[2,11],"end":[2,11]}},{"Mode":"Insert"},{"Text":"The quick brown\n\nfox jumps-over\n\n"},{"Mode":"Insert"},{"Selection":{"start":[3,0],"end":[3,0]}},{"Mode":"Insert"},{"Text":" jumps-over\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"The qu-over\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[0,6],"end":[0,6]}},{"Mode":"Insert"},{"Text":"The quickover\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[0,9],"end":[0,9]}},{"Mode":"Insert"},{"Text":"The quick brow\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[0,14],"end":[0,14]}},{"Mode":"Insert"},{"Text":"The quick brown\n\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick brown\n\n\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[2,0],"end":[Line truncated
@@ -0,0 +1 @@
[{"Text":"\nfox jumps-over\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"\nfox jumps-over\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"\nfox jumps-over\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"\nfox jumps-over\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"The quick brown\n\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick brown\n\n\n"},{"Mode":"Insert"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Insert"},{"Text":"The quick brown\n\n\n"},{"Mode":"Insert"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Insert"},{"Text":"The quick brown\n\n\n"},{"Mode":"Insert"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Insert"},{"Text":"The quick brown\n\n\n"},{"Mode":"Insert"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Insert"},{"Text":"The quick brown\n\n\n"},{"Mode":"Insert"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Insert"},{"Text":"The quick brown\n\nfox jumps-over\n"},{"Mode":"Insert"},{"Selection":{"start":[3,0],"end":[3,0]}},{"Mode":"Insert"},{"Text":"\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"The quick brown\n\n"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick brown\n\n"},{"Mode":"Insert"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Insert"},{"Text":"The quick brown\n\n"},{"Mode":"Insert"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Insert"},{"Text":"The quick brown\n\n"},{"Mode":"Insert"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Insert"},{"Text":"The quick brown\n\n"},{"Mode":"Insert"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Insert"},{"Text":"The quick brown\n\n"},{"Mode":"Insert"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Insert"},{"Text":"The quick brown\n\nfox jumps-over\n"},{"Mode":"Insert"},{"Selection":{"start":[3,0],"end":[3,0]}},{"Mode":"Insert"},{"Text":"\n"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"\n"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"\n"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"\n"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"The quick brown\n"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick brown\n\n"},{"Mode":"Insert"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Insert"},{"Text":"The quick brown\n\n"},{"Mode":"Insert"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Insert"},{"Text":"The quick brown\n\n"},{"Mode":"Insert"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Insert"},{"Text":"The quick brown\n\n"},{"Mode":"Insert"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Insert"},{"Text":"The quick brown\n\n"},{"Mode":"Insert"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Insert"},{"Text":"The quick brown\n\nfox jumps-over\n"},{"Mode":"Insert"},{"Selection":{"start":[3,0],"end":[3,0]}},{"Mode":"Insert"},{"Text":""},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":""},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":""},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":""},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"The quick brown\n"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick brown\n\n"},{"Mode":"Insert"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Insert"},{"Text":"The quick brown\n\n"},{"Mode":"Insert"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Insert"},{"Text":"The quick brown\n\n"},{"Mode":"Insert"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Insert"},{"Text":"The quick brown\n\n"},{"Mode":"Insert"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Insert"},{"Text":"The quick brown\n\n"},{"Mode":"Insert"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Insert"},{"Text":"The quick brown\n\nfox jumps-over\n"},{"Mode":"Insert"},{"Selection":{"start":[3,0],"end":[3,0]}},{"Mode":"Insert"},{"Text":""},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":""},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":""},{"Mode":"Insert"},{"Selection"Line truncated
@@ -0,0 +1 @@
[{"Text":"he quick brown\n\nfox jumps-over\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"The quck brown\n\nfox jumps-over\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[0,6],"end":[0,6]}},{"Mode":"Insert"},{"Text":"The quickbrown\n\nfox jumps-over\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[0,9],"end":[0,9]}},{"Mode":"Insert"},{"Text":"The quick brow\n\nfox jumps-over\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[0,14],"end":[0,14]}},{"Mode":"Insert"},{"Text":"The quick brown\n\nfox jumps-over\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick brown\n\nox jumps-over\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Insert"},{"Text":"The quick brown\n\nfox umps-over\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[2,4],"end":[2,4]}},{"Mode":"Insert"},{"Text":"The quick brown\n\nfox jumpsover\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[2,9],"end":[2,9]}},{"Mode":"Insert"},{"Text":"The quick brown\n\nfox jumps-ver\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[2,10],"end":[2,10]}},{"Mode":"Insert"},{"Text":"The quick brown\n\nfox jumps-oer\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[2,11],"end":[2,11]}},{"Mode":"Insert"},{"Text":"The quick brown\n\nfox jumps-over\nhe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[3,0],"end":[3,0]}},{"Mode":"Insert"},{"Text":"e quick brown\n\nfox jumps-over\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"The quk brown\n\nfox jumps-over\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[0,6],"end":[0,6]}},{"Mode":"Insert"},{"Text":"The quickrown\n\nfox jumps-over\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[0,9],"end":[0,9]}},{"Mode":"Insert"},{"Text":"The quick brow\n\nfox jumps-over\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[0,14],"end":[0,14]}},{"Mode":"Insert"},{"Text":"The quick brown\n\nfox jumps-over\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick brown\n\nx jumps-over\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Insert"},{"Text":"The quick brown\n\nfox mps-over\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[2,4],"end":[2,4]}},{"Mode":"Insert"},{"Text":"The quick brown\n\nfox jumpsver\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[2,9],"end":[2,9]}},{"Mode":"Insert"},{"Text":"The quick brown\n\nfox jumps-er\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[2,10],"end":[2,10]}},{"Mode":"Insert"},{"Text":"The quick brown\n\nfox jumps-or\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[2,11],"end":[2,11]}},{"Mode":"Insert"},{"Text":"The quick brown\n\nfox jumps-over\ne lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[3,0],"end":[3,0]}},{"Mode":"Insert"},{"Text":" quick brown\n\nfox jumps-over\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"The qu brown\n\nfox jumps-over\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[0,6],"end":[0,6]}},{"Mode":"Insert"},{"Text":"The quickown\n\nfox jumps-over\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[0,9],"end":[0,9]}},{"Mode":"Insert"},{"Text":"The quick brow\n\nfox jumps-over\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[0,14],"end":[0,14]}},{"Mode":"Insert"},{"Text":"The quick brown\n\nfox jumps-over\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick brown\n\n jumps-over\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Insert"},{"Text":"The quick brown\n\nfox ps-over\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[2,4],"end":[2,4]}},{"Mode":"Insert"},{"Text":"The quick brown\n\nfox jumpser\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[2,9],"end":[2,9]}},{"Mode":"Insert"},{"Text":"The quick brown\n\nfox jumps-r\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[2,10],"end":[2,10]}},{"Mode":"Insert"},{"Text":"The quick brown\n\nfox jumps-o\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[2,11],"end":[2,11]}},{"Mode":"Insert"},{"Text":"The quick brown\n\nfox jumps-over\n lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[3,0],"end":[3,0]}},{"Mode":"Insert"},{"Text":"quick brown\n\nfox jumps-over\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"The qubrown\n\nfox jumps-over\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[0,6],"end":[0,6]}},{"Mode":"Insert"},{"Text":"The quickwn\n\nfox jumps-over\nthe lazy dog\n"},{"Mode":"Insert"},{"Selection":{"start":[0,9],"end":[0,9]}},{"Mode":"Insert"},{"Text":"The quick brow\n\nfox jumps-over\nthLine truncated
@@ -0,0 +1 @@
[{"Text":"The quick brown\n\nfox jumps-over\nthe lazy dog\n"},{"Mode":"Normal"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Normal"},{"Text":"The quick brown\n\nfox jumps-over\nthe lazy dog\n"},{"Mode":"Normal"},{"Selection":{"start":[0,10],"end":[0,10]}},{"Mode":"Normal"},{"Text":"The quick brown\n\nfox jumps-over\nthe lazy dog\n"},{"Mode":"Normal"},{"Selection":{"start":[0,10],"end":[0,10]}},{"Mode":"Normal"},{"Text":"The quick brown\n\nfox jumps-over\nthe lazy dog\n"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"},{"Text":"The quick brown\n\nfox jumps-over\nthe lazy dog\n"},{"Mode":"Normal"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Normal"},{"Text":"The quick brown\n\nfox jumps-over\nthe lazy dog\n"},{"Mode":"Normal"},{"Selection":{"start":[2,4],"end":[2,4]}},{"Mode":"Normal"},{"Text":"The quick brown\n\nfox jumps-over\nthe lazy dog\n"},{"Mode":"Normal"},{"Selection":{"start":[2,9],"end":[2,9]}},{"Mode":"Normal"},{"Text":"The quick brown\n\nfox jumps-over\nthe lazy dog\n"},{"Mode":"Normal"},{"Selection":{"start":[2,10],"end":[2,10]}},{"Mode":"Normal"},{"Text":"The quick brown\n\nfox jumps-over\nthe lazy dog\n"},{"Mode":"Normal"},{"Selection":{"start":[3,0],"end":[3,0]}},{"Mode":"Normal"},{"Text":"The quick brown\n\nfox jumps-over\nthe lazy dog\n"},{"Mode":"Normal"},{"Selection":{"start":[3,0],"end":[3,0]}},{"Mode":"Normal"},{"Text":"The quick brown\n\nfox jumps-over\nthe lazy dog\n"},{"Mode":"Normal"},{"Selection":{"start":[3,4],"end":[3,4]}},{"Mode":"Normal"},{"Text":"The quick brown\n\nfox jumps-over\nthe lazy dog\n"},{"Mode":"Normal"},{"Selection":{"start":[0,10],"end":[0,10]}},{"Mode":"Normal"},{"Text":"The quick brown\n\nfox jumps-over\nthe lazy dog\n"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"},{"Text":"The quick brown\n\nfox jumps-over\nthe lazy dog\n"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"},{"Text":"The quick brown\n\nfox jumps-over\nthe lazy dog\n"},{"Mode":"Normal"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Normal"},{"Text":"The quick brown\n\nfox jumps-over\nthe lazy dog\n"},{"Mode":"Normal"},{"Selection":{"start":[2,4],"end":[2,4]}},{"Mode":"Normal"},{"Text":"The quick brown\n\nfox jumps-over\nthe lazy dog\n"},{"Mode":"Normal"},{"Selection":{"start":[2,9],"end":[2,9]}},{"Mode":"Normal"},{"Text":"The quick brown\n\nfox jumps-over\nthe lazy dog\n"},{"Mode":"Normal"},{"Selection":{"start":[2,10],"end":[2,10]}},{"Mode":"Normal"},{"Text":"The quick brown\n\nfox jumps-over\nthe lazy dog\n"},{"Mode":"Normal"},{"Selection":{"start":[3,0],"end":[3,0]}},{"Mode":"Normal"},{"Text":"The quick brown\n\nfox jumps-over\nthe lazy dog\n"},{"Mode":"Normal"},{"Selection":{"start":[3,4],"end":[3,4]}},{"Mode":"Normal"},{"Text":"The quick brown\n\nfox jumps-over\nthe lazy dog\n"},{"Mode":"Normal"},{"Selection":{"start":[3,4],"end":[3,4]}},{"Mode":"Normal"},{"Text":"The quick brown\n\nfox jumps-over\nthe lazy dog\n"},{"Mode":"Normal"},{"Selection":{"start":[3,9],"end":[3,9]}},{"Mode":"Normal"},{"Text":"The quick brown\n\nfox jumps-over\nthe lazy dog\n"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"},{"Text":"The quick brown\n\nfox jumps-over\nthe lazy dog\n"},{"Mode":"Normal"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Normal"},{"Text":"The quick brown\n\nfox jumps-over\nthe lazy dog\n"},{"Mode":"Normal"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Normal"},{"Text":"The quick brown\n\nfox jumps-over\nthe lazy dog\n"},{"Mode":"Normal"},{"Selection":{"start":[2,4],"end":[2,4]}},{"Mode":"Normal"},{"Text":"The quick brown\n\nfox jumps-over\nthe lazy dog\n"},{"Mode":"Normal"},{"Selection":{"start":[2,9],"end":[2,9]}},{"Mode":"Normal"},{"Text":"The quick brown\n\nfox jumps-over\nthe lazy dog\n"},{"Mode":"Normal"},{"Selection":{"start":[2,10],"end":[2,10]}},{"Mode":"Normal"},{"Text":"The quick brown\n\nfox jumps-over\nthe lazy dog\n"},{"Mode":"Normal"},{"Selection":{"start":[3,0],"end":[3,0]}},{"Mode":"Normal"},{"Text":"The quick brown\n\nfox jumps-over\nthe lazy dog\n"},{"Mode":"Normal"},{"Selection":{"start":[3,4],"end":[3,4]}},{"Mode":"Normal"},{"Text":"The quick brown\n\nfox jumps-over\nthe lazy dog\n"},{"Mode":"Normal"},{"Selection":{"start":[3,9],"end":[3,9]}},{"Mode":"Normal"},{"Text":"The quick brown\n\nfox jumps-over\nthe lazy dog\n"},{"Mode":"Normal"},{"Selection":{"start":[3,9],"end":[3,9]}},{"Mode":"Normal"},{"Text":"The quick brown\n\nfox jumps-over\nthe lazy dog\n"},{"Mode":"Normal"},{"Selection":{"start":[4,0],"end":[4,0]}},{"Mode":"Normal"},{"Text":"The quick brown\n\nfox jumps-over\nthe lazy dog\n"},{"Mode":"Normal"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Normal"},{"Text":"The quick brown\n\nfox jumps-over\nthe lazy dog\n"},{"Mode":"Normal"},{"Selection":{"start":[2,4],"end":[2,4]}},{"Mode":"Normal"},{"Text":"The quick brown\n\nfox jumps-over\nthe lazy dog\n"},{"Mode":"Normal"},{"Selection":{"start":[2,4],"end":[2,4]}},Line truncated
@@ -0,0 +1 @@
[{"Text":"The quick "},{"Mode":"Insert"},{"Selection":{"start":[0,10],"end":[0,10]}},{"Mode":"Insert"},{"Text":"The ver\nthe lazy dog"},{"Mode":"Insert"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Insert"},{"Text":"The quick brown\nfox jumps he lazy dog"},{"Mode":"Insert"},{"Selection":{"start":[1,10],"end":[1,10]}},{"Mode":"Insert"},{"Text":"The quick brown\nfox jumps over\nthe og"},{"Mode":"Insert"},{"Selection":{"start":[2,4],"end":[2,4]}},{"Mode":"Insert"},{"Text":"uick brown\nfox jumps over\nthe lazy dog"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"The ver\nthe lazy dog"},{"Mode":"Insert"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Insert"},{"Text":"The quick brown\nazy dog"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"}]
@@ -0,0 +1 @@
[{"Text":"The quick "},{"Mode":"Normal"},{"Selection":{"start":[0,9],"end":[0,9]}},{"Mode":"Normal"},{"Text":"The ver\nthe lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Normal"},{"Text":"The ver\nthe lquick brown\nfox jumps oazy dog"},{"Mode":"Normal"},{"Selection":{"start":[1,5],"end":[1,5]}},{"Mode":"Normal"},{"Text":"The ver\nthe lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Normal"},{"Text":"The quick brown\nfox jumps over\nthe og"},{"Mode":"Normal"},{"Selection":{"start":[2,4],"end":[2,4]}},{"Mode":"Normal"},{"Text":"uick brown\nfox jumps over\nthe lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"The ver\nthe lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Normal"},{"Text":"The quick brown\nazy dog"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"}]
@@ -0,0 +1 @@
[{"Text":"\nfox jumps over\nthe lazy dog"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"\nfox jumps over\nThe quick brown\nthe lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Normal"},{"Text":"The quick brown\n\nthe lazy dog"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick brown\nfox jumps over\n"},{"Mode":"Insert"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Insert"},{"Text":"\nthe lazy dog"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"\nthe lazy dog\nThe quick brown\nfox jumps over"},{"Mode":"Normal"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Normal"},{"Text":"The quick brown\n"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick brown\nfox jumps over\n"},{"Mode":"Insert"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Insert"}]
@@ -0,0 +1 @@
[{"Text":"fox jumps over\nthe lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[0,6],"end":[0,6]}},{"Mode":"Normal"},{"Text":"fox jumps over\nThe quick brown\nthe lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"},{"Text":"The quick brown\nthe lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[1,6],"end":[1,6]}},{"Mode":"Normal"},{"Text":"The quick brown\nfox jumps over"},{"Mode":"Normal"},{"Selection":{"start":[1,6],"end":[1,6]}},{"Mode":"Normal"},{"Text":"the lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[0,6],"end":[0,6]}},{"Mode":"Normal"},{"Text":"the lazy dog\nThe quick brown\nfox jumps over"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"},{"Text":"The quick brown"},{"Mode":"Normal"},{"Selection":{"start":[0,6],"end":[0,6]}},{"Mode":"Normal"},{"Text":"The quick brown\nfox jumps over"},{"Mode":"Normal"},{"Selection":{"start":[1,6],"end":[1,6]}},{"Mode":"Normal"}]
@@ -0,0 +1 @@
[{"Text":"The quick brown? Fox Jumps! Over the lazy."},{"Mode":{"Visual":{"line":false}}},{"Selection":{"start":[0,0],"end":[0,15]}},{"Mode":{"Visual":{"line":false}}},{"Text":"The quick brown? Fox Jumps! Over the lazy."},{"Mode":{"Visual":{"line":false}}},{"Selection":{"start":[0,0],"end":[0,15]}},{"Mode":{"Visual":{"line":false}}},{"Text":"The quick brown? Fox Jumps! Over the lazy."},{"Mode":{"Visual":{"line":false}}},{"Selection":{"start":[0,0],"end":[0,15]}},{"Mode":{"Visual":{"line":false}}},{"Text":"The quick brown? Fox Jumps! Over the lazy."},{"Mode":{"Visual":{"line":false}}},{"Selection":{"start":[0,16],"end":[0,16]}},{"Mode":{"Visual":{"line":false}}},{"Text":"The quick brown? Fox Jumps! Over the lazy."},{"Mode":{"Visual":{"line":false}}},{"Selection":{"start":[0,17],"end":[0,26]}},{"Mode":{"Visual":{"line":false}}},{"Text":"The quick brown? Fox Jumps! Over the lazy."},{"Mode":{"Visual":{"line":false}}},{"Selection":{"start":[0,17],"end":[0,26]}},{"Mode":{"Visual":{"line":false}}},{"Text":"The quick brown? Fox Jumps! Over the lazy."},{"Mode":{"Visual":{"line":false}}},{"Selection":{"start":[0,17],"end":[0,26]}},{"Mode":{"Visual":{"line":false}}},{"Text":"The quick brown? Fox Jumps! Over the lazy."},{"Mode":{"Visual":{"line":false}}},{"Selection":{"start":[0,27],"end":[0,27]}},{"Mode":{"Visual":{"line":false}}},{"Text":"The quick brown? Fox Jumps! Over the lazy."},{"Mode":{"Visual":{"line":false}}},{"Selection":{"start":[0,28],"end":[0,41]}},{"Mode":{"Visual":{"line":false}}},{"Text":"The quick brown? Fox Jumps! Over the lazy."},{"Mode":{"Visual":{"line":false}}},{"Selection":{"start":[0,28],"end":[0,41]}},{"Mode":{"Visual":{"line":false}}},{"Text":"The quick brown? Fox Jumps! Over the lazy."},{"Mode":{"Visual":{"line":false}}},{"Selection":{"start":[0,28],"end":[0,41]}},{"Mode":{"Visual":{"line":false}}},{"Text":"The quick brown \nfox jumps over\nthe lazy dog. The quick \nbrown fox jumps over\n"},{"Mode":{"Visual":{"line":false}}},{"Selection":{"start":[0,0],"end":[2,12]}},{"Mode":{"Visual":{"line":false}}},{"Text":"The quick brown \nfox jumps over\nthe lazy dog. The quick \nbrown fox jumps over\n"},{"Mode":{"Visual":{"line":false}}},{"Selection":{"start":[0,0],"end":[2,12]}},{"Mode":{"Visual":{"line":false}}},{"Text":"The quick brown \nfox jumps over\nthe lazy dog. The quick \nbrown fox jumps over\n"},{"Mode":{"Visual":{"line":false}}},{"Selection":{"start":[0,0],"end":[2,12]}},{"Mode":{"Visual":{"line":false}}},{"Text":"The quick brown \nfox jumps over\nthe lazy dog. The quick \nbrown fox jumps over\n"},{"Mode":{"Visual":{"line":false}}},{"Selection":{"start":[0,0],"end":[2,12]}},{"Mode":{"Visual":{"line":false}}},{"Text":"The quick brown \nfox jumps over\nthe lazy dog. The quick \nbrown fox jumps over\n"},{"Mode":{"Visual":{"line":false}}},{"Selection":{"start":[0,0],"end":[2,12]}},{"Mode":{"Visual":{"line":false}}},{"Text":"The quick brown \nfox jumps over\nthe lazy dog. The quick \nbrown fox jumps over\n"},{"Mode":{"Visual":{"line":false}}},{"Selection":{"start":[2,13],"end":[2,13]}},{"Mode":{"Visual":{"line":false}}},{"Text":"The quick brown \nfox jumps over\nthe lazy dog. The quick \nbrown fox jumps over\n"},{"Mode":{"Visual":{"line":false}}},{"Selection":{"start":[2,14],"end":[3,19]}},{"Mode":{"Visual":{"line":false}}},{"Text":"The quick brown \nfox jumps over\nthe lazy dog. The quick \nbrown fox jumps over\n"},{"Mode":{"Visual":{"line":false}}},{"Selection":{"start":[2,14],"end":[3,19]}},{"Mode":{"Visual":{"line":false}}},{"Text":"The quick brown.)]'\" Brown fox jumps. "},{"Mode":{"Visual":{"line":false}}},{"Selection":{"start":[0,0],"end":[0,19]}},{"Mode":{"Visual":{"line":false}}},{"Text":"The quick brown.)]'\" Brown fox jumps. "},{"Mode":{"Visual":{"line":false}}},{"Selection":{"start":[0,0],"end":[0,19]}},{"Mode":{"Visual":{"line":false}}},{"Text":"The quick brown.)]'\" Brown fox jumps. "},{"Mode":{"Visual":{"line":false}}},{"Selection":{"start":[0,0],"end":[0,19]}},{"Mode":{"Visual":{"line":false}}},{"Text":"The quick brown.)]'\" Brown fox jumps. "},{"Mode":{"Visual":{"line":false}}},{"Selection":{"start":[0,0],"end":[0,19]}},{"Mode":{"Visual":{"line":false}}},{"Text":"The quick brown.)]'\" Brown fox jumps. "},{"Mode":{"Visual":{"line":false}}},{"Selection":{"start":[0,0],"end":[0,19]}},{"Mode":{"Visual":{"line":false}}},{"Text":"The quick brown.)]'\" Brown fox jumps. "},{"Mode":{"Visual":{"line":false}}},{"Selection":{"start":[0,0],"end":[0,19]}},{"Mode":{"Visual":{"line":false}}},{"Text":"The quick brown.)]'\" Brown fox jumps. "},{"Mode":{"Visual":{"line":false}}},{"Selection":{"start":[0,21],"end":[0,36]}},{"Mode":{"Visual":{"line":false}}},{"Text":"The quick brown.)]'\" Brown fox jumps. "},{"Mode":{"Visual":{"line":false}}},{"Selection":{"start":[0,21],"end":[0,36]}},{"Mode":{"Visual":{"line":false}}},{"Text":"The quick brown.)]'\" Brown fox jumps. "},{"Mode":{"Visual":{"line":false}}},{"Selection":{"start":[0,37],"end":[0,37]}},{"Mode":{"Visual":{"line":false}}}Line truncated
@@ -0,0 +1 @@
[{"Text":"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}}},{"Selection":{"start":[0,10],"end":[0,14]}},{"Mode":{"Visual":{"line":false}}},{"Text":"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}}},{"Selection":{"start":[0,10],"end":[0,14]}},{"Mode":{"Visual":{"line":false}}},{"Text":"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}}},{"Selection":{"start":[0,15],"end":[0,17]}},{"Mode":{"Visual":{"line":false}}},{"Text":"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}}},{"Selection":{"start":[1,4],"end":[1,8]}},{"Mode":{"Visual":{"line":false}}},{"Text":"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}}},{"Selection":{"start":[1,4],"end":[1,8]}},{"Mode":{"Visual":{"line":false}}},{"Text":"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}}},{"Selection":{"start":[1,9],"end":[1,9]}},{"Mode":{"Visual":{"line":false}}},{"Text":"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}}},{"Selection":{"start":[2,12],"end":[2,13]}},{"Mode":{"Visual":{"line":false}}},{"Text":"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}}},{"Selection":{"start":[3,0],"end":[3,0]}},{"Mode":{"Visual":{"line":false}}},{"Text":"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}}},{"Selection":{"start":[4,0],"end":[4,0]}},{"Mode":{"Visual":{"line":false}}},{"Text":"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}}},{"Selection":{"start":[5,0],"end":[5,0]}},{"Mode":{"Visual":{"line":false}}},{"Text":"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}}},{"Selection":{"start":[6,0],"end":[6,2]}},{"Mode":{"Visual":{"line":false}}},{"Text":"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}}},{"Selection":{"start":[6,3],"end":[6,3]}},{"Mode":{"Visual":{"line":false}}},{"Text":"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}}},{"Selection":{"start":[6,4],"end":[6,8]}},{"Mode":{"Visual":{"line":false}}},{"Text":"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}}},{"Selection":{"start":[6,4],"end":[6,8]}},{"Mode":{"Visual":{"line":false}}},{"Text":"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}}},{"Selection":{"start":[6,9],"end":[6,9]}},{"Mode":{"Visual":{"line":false}}},{"Text":"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}}},{"Selection":{"start":[6,10],"end":[6,14]}},{"Mode":{"Visual":{"line":false}}},{"Text":"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}}},{"Selection":{"start":[6,15],"end":[6,15]}},{"Mode":{"Visual":{"line":false}}},{"Text":"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}}},{"Selection":{"start":[7,0],"end":[7,1]}},{"Mode":{"Visual":{"line":false}}},{"Text":"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}}},{"Selection":{"start":[8,0],"end":[8,1]}},{"Mode":{"Visual":{"line":false}}},{"Text":"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}}},{"Selection":{"start":[9,0],"end":[9,1]}},{"Mode":{"Visual":{"line":false}}},{"Text":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \nLine truncated
+1
View File
@@ -0,0 +1 @@
[{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[0,9],"end":[0,9]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[0,10],"end":[0,10]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[3,0],"end":[3,0]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[3,10],"end":[3,10]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[4,0],"end":[4,0]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[4,2],"end":[4,2]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[4,2],"end":[4,2]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[3,0],"end":[3,0]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[3,10],"end":[3,10]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[4,0],"end":[4,0]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[4,2],"end":[4,2]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[4,2],"end":[4,2]}},{"Mode":"Normal"}]
+1
View File
@@ -0,0 +1 @@
[{"Text":"est"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"Tet"},{"Mode":"Normal"},{"Selection":{"start":[0,2],"end":[0,2]}},{"Mode":"Normal"},{"Text":"Tes"},{"Mode":"Normal"},{"Selection":{"start":[0,2],"end":[0,2]}},{"Mode":"Normal"},{"Text":"Tes\ntest"},{"Mode":"Normal"},{"Selection":{"start":[0,2],"end":[0,2]}},{"Mode":"Normal"}]