Detailed changes
@@ -2591,9 +2591,9 @@ dependencies = [
[[package]]
name = "glob"
-version = "0.3.0"
+version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574"
+checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
[[package]]
name = "globset"
@@ -4625,12 +4625,15 @@ dependencies = [
"client",
"clock",
"collections",
+ "ctor",
"db",
+ "env_logger",
"fs",
"fsevent",
"futures 0.3.25",
"fuzzy",
"git",
+ "glob",
"gpui",
"ignore",
"language",
@@ -71,6 +71,7 @@ serde = { version = "1.0", features = ["derive", "rc"] }
serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
serde_json = { version = "1.0", features = ["preserve_order", "raw_value"] }
rand = { version = "0.8" }
+postage = { version = "0.4.1", features = ["futures-traits"] }
[patch.crates-io]
tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "c51896d32dcc11a38e41f36e3deb1a6a9c4f4b14" }
@@ -34,7 +34,7 @@ util = { path = "../util" }
anyhow = "1.0.38"
async-broadcast = "0.4"
futures = "0.3"
-postage = { version = "0.4.1", features = ["futures-traits"] }
+postage = { workspace = true }
[dev-dependencies]
client = { path = "../client", features = ["test-support"] }
@@ -27,7 +27,7 @@ isahc = "1.7"
lazy_static = "1.4.0"
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
parking_lot = "0.11.1"
-postage = { version = "0.4.1", features = ["futures-traits"] }
+postage = { workspace = true }
rand = "0.8.3"
smol = "1.2.5"
thiserror = "1.0.29"
@@ -1744,10 +1744,6 @@ async fn test_project_reconnect(
vec![
"a.txt",
"b.txt",
- "subdir1",
- "subdir1/c.txt",
- "subdir1/d.txt",
- "subdir1/e.txt",
"subdir2",
"subdir2/f.txt",
"subdir2/g.txt",
@@ -1780,10 +1776,6 @@ async fn test_project_reconnect(
vec![
"a.txt",
"b.txt",
- "subdir1",
- "subdir1/c.txt",
- "subdir1/d.txt",
- "subdir1/e.txt",
"subdir2",
"subdir2/f.txt",
"subdir2/g.txt",
@@ -1875,10 +1867,6 @@ async fn test_project_reconnect(
vec![
"a.txt",
"b.txt",
- "subdir1",
- "subdir1/c.txt",
- "subdir1/d.txt",
- "subdir1/e.txt",
"subdir2",
"subdir2/f.txt",
"subdir2/g.txt",
@@ -42,7 +42,7 @@ workspace = { path = "../workspace" }
anyhow = "1.0"
futures = "0.3"
log = "0.4"
-postage = { version = "0.4.1", features = ["futures-traits"] }
+postage = { workspace = true }
serde = { version = "1.0", features = ["derive", "rc"] }
serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
@@ -20,7 +20,7 @@ settings = { path = "../settings" }
theme = { path = "../theme" }
util = { path = "../util" }
workspace = { path = "../workspace" }
-postage = { version = "0.4", features = ["futures-traits"] }
+postage = { workspace = true }
[dev-dependencies]
unindent = "0.1"
@@ -51,7 +51,7 @@ lazy_static = "1.4"
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
ordered-float = "2.1.1"
parking_lot = "0.11"
-postage = { version = "0.4", features = ["futures-traits"] }
+postage = { workspace = true }
rand = { version = "0.8.3", optional = true }
serde = { workspace = true }
serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
@@ -21,7 +21,7 @@ gpui = { path = "../gpui" }
human_bytes = "0.4.1"
isahc = "1.7"
lazy_static = "1.4.0"
-postage = { version = "0.4", features = ["futures-traits"] }
+postage = { workspace = true }
project = { path = "../project" }
search = { path = "../search" }
serde = { version = "1.0", features = ["derive", "rc"] }
@@ -19,7 +19,7 @@ settings = { path = "../settings" }
util = { path = "../util" }
theme = { path = "../theme" }
workspace = { path = "../workspace" }
-postage = { version = "0.4.1", features = ["futures-traits"] }
+postage = { workspace = true }
[dev-dependencies]
gpui = { path = "../gpui", features = ["test-support"] }
@@ -380,6 +380,8 @@ struct FakeFsState {
next_inode: u64,
next_mtime: SystemTime,
event_txs: Vec<smol::channel::Sender<Vec<fsevent::Event>>>,
+ events_paused: bool,
+ buffered_events: Vec<fsevent::Event>,
}
#[cfg(any(test, feature = "test-support"))]
@@ -483,15 +485,21 @@ impl FakeFsState {
I: IntoIterator<Item = T>,
T: Into<PathBuf>,
{
- let events = paths
- .into_iter()
- .map(|path| fsevent::Event {
+ self.buffered_events
+ .extend(paths.into_iter().map(|path| fsevent::Event {
event_id: 0,
flags: fsevent::StreamFlags::empty(),
path: path.into(),
- })
- .collect::<Vec<_>>();
+ }));
+
+ if !self.events_paused {
+ self.flush_events(self.buffered_events.len());
+ }
+ }
+ fn flush_events(&mut self, mut count: usize) {
+ count = count.min(self.buffered_events.len());
+ let events = self.buffered_events.drain(0..count).collect::<Vec<_>>();
self.event_txs.retain(|tx| {
let _ = tx.try_send(events.clone());
!tx.is_closed()
@@ -514,6 +522,8 @@ impl FakeFs {
next_mtime: SystemTime::UNIX_EPOCH,
next_inode: 1,
event_txs: Default::default(),
+ buffered_events: Vec::new(),
+ events_paused: false,
}),
})
}
@@ -567,6 +577,18 @@ impl FakeFs {
state.emit_event(&[path]);
}
+ pub async fn pause_events(&self) {
+ self.state.lock().await.events_paused = true;
+ }
+
+ pub async fn buffered_event_count(&self) -> usize {
+ self.state.lock().await.buffered_events.len()
+ }
+
+ pub async fn flush_events(&self, count: usize) {
+ self.state.lock().await.flush_events(count);
+ }
+
#[must_use]
pub fn insert_tree<'a>(
&'a self,
@@ -868,7 +890,7 @@ impl Fs for FakeFs {
.ok_or_else(|| anyhow!("cannot remove the root"))?;
let base_name = path.file_name().unwrap();
- let state = self.state.lock().await;
+ let mut state = self.state.lock().await;
let parent_entry = state.read_path(parent_path).await?;
let mut parent_entry = parent_entry.lock().await;
let entry = parent_entry
@@ -892,7 +914,7 @@ impl Fs for FakeFs {
e.remove();
}
}
-
+ state.emit_event(&[path]);
Ok(())
}
@@ -15,4 +15,4 @@ menu = { path = "../menu" }
settings = { path = "../settings" }
text = { path = "../text" }
workspace = { path = "../workspace" }
-postage = { version = "0.4", features = ["futures-traits"] }
+postage = { workspace = true }
@@ -36,7 +36,7 @@ parking = "2.0.0"
parking_lot = "0.11.1"
pathfinder_color = "0.5"
pathfinder_geometry = "0.5"
-postage = { version = "0.4.1", features = ["futures-traits"] }
+postage = { workspace = true }
rand = "0.8.3"
resvg = "0.14"
schemars = "0.8"
@@ -43,7 +43,7 @@ futures = "0.3"
lazy_static = "1.4"
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
parking_lot = "0.11.1"
-postage = { version = "0.4.1", features = ["futures-traits"] }
+postage = { workspace = true }
rand = { version = "0.8.3", optional = true }
regex = "1.5"
serde = { version = "1.0", features = ["derive", "rc"] }
@@ -1366,6 +1366,7 @@ impl Buffer {
where
T: Into<Arc<str>>,
{
+ self.autoindent_requests.clear();
self.edit([(0..self.len(), text)], None, cx)
}
@@ -809,7 +809,6 @@ fn test_enclosing_bracket_ranges_where_brackets_are_not_outermost_children(
}"}],
);
- eprintln!("-----------------------");
// Regression test: even though the parent node of the parentheses (the for loop) does
// intersect the given range, the parentheses themselves do not contain the range, so
// they should not be returned. Only the curly braces contain the range.
@@ -35,7 +35,7 @@ core-graphics = "0.22.3"
futures = "0.3"
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
parking_lot = "0.11.1"
-postage = { version = "0.4.1", features = ["futures-traits"] }
+postage = { workspace = true }
async-trait = { version = "0.1", optional = true }
lazy_static = { version = "1.4", optional = true }
@@ -21,7 +21,7 @@ futures = "0.3"
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
lsp-types = "0.91"
parking_lot = "0.11"
-postage = { version = "0.4.1", features = ["futures-traits"] }
+postage = { workspace = true }
serde = { version = "1.0", features = ["derive", "rc"] }
serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
serde_json = { version = "1.0", features = ["raw_value"] }
@@ -319,6 +319,9 @@ impl LanguageServer {
capabilities: ClientCapabilities {
workspace: Some(WorkspaceClientCapabilities {
configuration: Some(true),
+ did_change_watched_files: Some(DynamicRegistrationClientCapabilities {
+ dynamic_registration: Some(true),
+ }),
did_change_configuration: Some(DynamicRegistrationClientCapabilities {
dynamic_registration: Some(true),
}),
@@ -18,5 +18,5 @@ settings = { path = "../settings" }
text = { path = "../text" }
workspace = { path = "../workspace" }
ordered-float = "2.1.1"
-postage = { version = "0.4", features = ["futures-traits"] }
+postage = { workspace = true }
smol = "1.2"
@@ -27,6 +27,7 @@ fs = { path = "../fs" }
fsevent = { path = "../fsevent" }
fuzzy = { path = "../fuzzy" }
git = { path = "../git" }
+glob = { version = "0.3.1" }
gpui = { path = "../gpui" }
language = { path = "../language" }
lsp = { path = "../lsp" }
@@ -44,7 +45,7 @@ ignore = "0.4"
lazy_static = "1.4.0"
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
parking_lot = "0.11.1"
-postage = { version = "0.4.1", features = ["futures-traits"] }
+postage = { workspace = true }
pulldown-cmark = { version = "0.9.1", default-features = false }
rand = "0.8.3"
regex = "1.5"
@@ -58,6 +59,8 @@ thiserror = "1.0.29"
toml = "0.5"
[dev-dependencies]
+ctor = "0.1"
+env_logger = "0.9"
pretty_assertions = "1.3.0"
client = { path = "../client", features = ["test-support"] }
collections = { path = "../collections", features = ["test-support"] }
@@ -0,0 +1,121 @@
+use anyhow::{anyhow, Result};
+use std::path::Path;
+
+#[derive(Default)]
+pub struct LspGlobSet {
+ patterns: Vec<glob::Pattern>,
+}
+
+impl LspGlobSet {
+ pub fn clear(&mut self) {
+ self.patterns.clear();
+ }
+
+ /// Add a pattern to the glob set.
+ ///
+ /// LSP's glob syntax supports bash-style brace expansion. For example,
+ /// the pattern '*.{js,ts}' would match all JavaScript or TypeScript files.
+ /// This is not a part of the standard libc glob syntax, and isn't supported
+ /// by the `glob` crate. So we pre-process the glob patterns, producing a
+ /// separate glob `Pattern` object for each part of a brace expansion.
+ pub fn add_pattern(&mut self, pattern: &str) -> Result<()> {
+ // Find all of the ranges of `pattern` that contain matched curly braces.
+ let mut expansion_ranges = Vec::new();
+ let mut expansion_start_ix = None;
+ for (ix, c) in pattern.match_indices(|c| ['{', '}'].contains(&c)) {
+ match c {
+ "{" => {
+ if expansion_start_ix.is_some() {
+ return Err(anyhow!("nested braces in glob patterns aren't supported"));
+ }
+ expansion_start_ix = Some(ix);
+ }
+ "}" => {
+ if let Some(start_ix) = expansion_start_ix {
+ expansion_ranges.push(start_ix..ix + 1);
+ }
+ expansion_start_ix = None;
+ }
+ _ => {}
+ }
+ }
+
+ // Starting with a single pattern, process each brace expansion by cloning
+ // the pattern once per element of the expansion.
+ let mut unexpanded_patterns = vec![];
+ let mut expanded_patterns = vec![pattern.to_string()];
+
+ for outer_range in expansion_ranges.into_iter().rev() {
+ let inner_range = (outer_range.start + 1)..(outer_range.end - 1);
+ std::mem::swap(&mut unexpanded_patterns, &mut expanded_patterns);
+ for unexpanded_pattern in unexpanded_patterns.drain(..) {
+ for part in unexpanded_pattern[inner_range.clone()].split(',') {
+ let mut expanded_pattern = unexpanded_pattern.clone();
+ expanded_pattern.replace_range(outer_range.clone(), part);
+ expanded_patterns.push(expanded_pattern);
+ }
+ }
+ }
+
+ // Parse the final glob patterns and add them to the set.
+ for pattern in expanded_patterns {
+ let pattern = glob::Pattern::new(&pattern)?;
+ self.patterns.push(pattern);
+ }
+
+ Ok(())
+ }
+
+ pub fn matches(&self, path: &Path) -> bool {
+ self.patterns
+ .iter()
+ .any(|pattern| pattern.matches_path(path))
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_glob_set() {
+ let mut watch = LspGlobSet::default();
+ watch.add_pattern("/a/**/*.rs").unwrap();
+ watch.add_pattern("/a/**/Cargo.toml").unwrap();
+
+ assert!(watch.matches("/a/b.rs".as_ref()));
+ assert!(watch.matches("/a/b/c.rs".as_ref()));
+
+ assert!(!watch.matches("/b/c.rs".as_ref()));
+ assert!(!watch.matches("/a/b.ts".as_ref()));
+ }
+
+ #[test]
+ fn test_brace_expansion() {
+ let mut watch = LspGlobSet::default();
+ watch.add_pattern("/a/*.{ts,js,tsx}").unwrap();
+
+ assert!(watch.matches("/a/one.js".as_ref()));
+ assert!(watch.matches("/a/two.ts".as_ref()));
+ assert!(watch.matches("/a/three.tsx".as_ref()));
+
+ assert!(!watch.matches("/a/one.j".as_ref()));
+ assert!(!watch.matches("/a/two.s".as_ref()));
+ assert!(!watch.matches("/a/three.t".as_ref()));
+ assert!(!watch.matches("/a/four.t".as_ref()));
+ assert!(!watch.matches("/a/five.xt".as_ref()));
+ }
+
+ #[test]
+ fn test_multiple_brace_expansion() {
+ let mut watch = LspGlobSet::default();
+ watch.add_pattern("/a/{one,two,three}.{b*c,d*e}").unwrap();
+
+ assert!(watch.matches("/a/one.bic".as_ref()));
+ assert!(watch.matches("/a/two.dole".as_ref()));
+ assert!(watch.matches("/a/three.deeee".as_ref()));
+
+ assert!(!watch.matches("/a/four.bic".as_ref()));
+ assert!(!watch.matches("/a/one.be".as_ref()));
+ }
+}
@@ -1,5 +1,6 @@
mod ignore;
mod lsp_command;
+mod lsp_glob_set;
pub mod search;
pub mod terminals;
pub mod worktree;
@@ -33,10 +34,11 @@ use language::{
Transaction, Unclipped,
};
use lsp::{
- DiagnosticSeverity, DiagnosticTag, DocumentHighlightKind, LanguageServer, LanguageString,
- MarkedString,
+ DiagnosticSeverity, DiagnosticTag, DidChangeWatchedFilesRegistrationOptions,
+ DocumentHighlightKind, LanguageServer, LanguageString, MarkedString,
};
use lsp_command::*;
+use lsp_glob_set::LspGlobSet;
use postage::watch;
use rand::prelude::*;
use search::SearchQuery;
@@ -188,6 +190,7 @@ pub enum LanguageServerState {
language: Arc<Language>,
adapter: Arc<CachedLspAdapter>,
server: Arc<LanguageServer>,
+ watched_paths: LspGlobSet,
simulate_disk_based_diagnostics_completion: Option<Task<()>>,
},
}
@@ -2046,8 +2049,26 @@ impl Project {
})
.detach();
language_server
- .on_request::<lsp::request::RegisterCapability, _, _>(|_, _| async {
- Ok(())
+ .on_request::<lsp::request::RegisterCapability, _, _>({
+ let this = this.downgrade();
+ move |params, mut cx| async move {
+ let this = this
+ .upgrade(&cx)
+ .ok_or_else(|| anyhow!("project dropped"))?;
+ for reg in params.registrations {
+ if reg.method == "workspace/didChangeWatchedFiles" {
+ if let Some(options) = reg.register_options {
+ let options = serde_json::from_value(options)?;
+ this.update(&mut cx, |this, cx| {
+ this.on_lsp_did_change_watched_files(
+ server_id, options, cx,
+ );
+ });
+ }
+ }
+ }
+ Ok(())
+ }
})
.detach();
@@ -2117,6 +2138,7 @@ impl Project {
LanguageServerState::Running {
adapter: adapter.clone(),
language,
+ watched_paths: Default::default(),
server: language_server.clone(),
simulate_disk_based_diagnostics_completion: None,
},
@@ -2509,6 +2531,23 @@ impl Project {
}
}
+ fn on_lsp_did_change_watched_files(
+ &mut self,
+ language_server_id: usize,
+ params: DidChangeWatchedFilesRegistrationOptions,
+ cx: &mut ModelContext<Self>,
+ ) {
+ if let Some(LanguageServerState::Running { watched_paths, .. }) =
+ self.language_servers.get_mut(&language_server_id)
+ {
+ watched_paths.clear();
+ for watcher in params.watchers {
+ watched_paths.add_pattern(&watcher.glob_pattern).log_err();
+ }
+ cx.notify();
+ }
+ }
+
async fn on_lsp_workspace_edit(
this: WeakModelHandle<Self>,
params: lsp::ApplyWorkspaceEditParams,
@@ -4465,7 +4504,10 @@ impl Project {
cx.observe(worktree, |_, _, cx| cx.notify()).detach();
if worktree.read(cx).is_local() {
cx.subscribe(worktree, |this, worktree, event, cx| match event {
- worktree::Event::UpdatedEntries => this.update_local_worktree_buffers(worktree, cx),
+ worktree::Event::UpdatedEntries(changes) => {
+ this.update_local_worktree_buffers(&worktree, cx);
+ this.update_local_worktree_language_servers(&worktree, changes, cx);
+ }
worktree::Event::UpdatedGitRepositories(updated_repos) => {
this.update_local_worktree_buffers_git_repos(worktree, updated_repos, cx)
}
@@ -4496,7 +4538,7 @@ impl Project {
fn update_local_worktree_buffers(
&mut self,
- worktree_handle: ModelHandle<Worktree>,
+ worktree_handle: &ModelHandle<Worktree>,
cx: &mut ModelContext<Self>,
) {
let snapshot = worktree_handle.read(cx).snapshot();
@@ -4506,7 +4548,7 @@ impl Project {
if let Some(buffer) = buffer.upgrade(cx) {
buffer.update(cx, |buffer, cx| {
if let Some(old_file) = File::from_dyn(buffer.file()) {
- if old_file.worktree != worktree_handle {
+ if old_file.worktree != *worktree_handle {
return;
}
@@ -4578,6 +4620,58 @@ impl Project {
}
}
+ fn update_local_worktree_language_servers(
+ &mut self,
+ worktree_handle: &ModelHandle<Worktree>,
+ changes: &HashMap<Arc<Path>, PathChange>,
+ cx: &mut ModelContext<Self>,
+ ) {
+ let worktree_id = worktree_handle.read(cx).id();
+ let abs_path = worktree_handle.read(cx).abs_path();
+ for ((server_worktree_id, _), server_id) in &self.language_server_ids {
+ if *server_worktree_id == worktree_id {
+ if let Some(server) = self.language_servers.get(server_id) {
+ if let LanguageServerState::Running {
+ server,
+ watched_paths,
+ ..
+ } = server
+ {
+ let params = lsp::DidChangeWatchedFilesParams {
+ changes: changes
+ .iter()
+ .filter_map(|(path, change)| {
+ let path = abs_path.join(path);
+ if watched_paths.matches(&path) {
+ Some(lsp::FileEvent {
+ uri: lsp::Url::from_file_path(path).unwrap(),
+ typ: match change {
+ PathChange::Added => lsp::FileChangeType::CREATED,
+ PathChange::Removed => lsp::FileChangeType::DELETED,
+ PathChange::Updated
+ | PathChange::AddedOrUpdated => {
+ lsp::FileChangeType::CHANGED
+ }
+ },
+ })
+ } else {
+ None
+ }
+ })
+ .collect(),
+ };
+
+ if !params.changes.is_empty() {
+ server
+ .notify::<lsp::notification::DidChangeWatchedFiles>(params)
+ .log_err();
+ }
+ }
+ }
+ }
+ }
+ }
+
fn update_local_worktree_buffers_git_repos(
&mut self,
worktree: ModelHandle<Worktree>,
@@ -8,12 +8,21 @@ use language::{
OffsetRangeExt, Point, ToPoint,
};
use lsp::Url;
+use parking_lot::Mutex;
use pretty_assertions::assert_eq;
use serde_json::json;
use std::{cell::RefCell, os::unix, rc::Rc, task::Poll};
use unindent::Unindent as _;
use util::{assert_set_eq, test::temp_tree};
+#[cfg(test)]
+#[ctor::ctor]
+fn init_logger() {
+ if std::env::var("RUST_LOG").is_ok() {
+ env_logger::init();
+ }
+}
+
#[gpui::test]
async fn test_symlinks(cx: &mut gpui::TestAppContext) {
let dir = temp_tree(json!({
@@ -438,6 +447,111 @@ async fn test_managing_language_servers(
);
}
+#[gpui::test]
+async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppContext) {
+ cx.foreground().forbid_parking();
+
+ let mut language = Language::new(
+ LanguageConfig {
+ name: "Rust".into(),
+ path_suffixes: vec!["rs".to_string()],
+ ..Default::default()
+ },
+ Some(tree_sitter_rust::language()),
+ );
+ let mut fake_servers = language
+ .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+ name: "the-language-server",
+ ..Default::default()
+ }))
+ .await;
+
+ let fs = FakeFs::new(cx.background());
+ fs.insert_tree(
+ "/the-root",
+ json!({
+ "a.rs": "",
+ "b.rs": "",
+ }),
+ )
+ .await;
+
+ let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await;
+ project.update(cx, |project, _| {
+ project.languages.add(Arc::new(language));
+ });
+ cx.foreground().run_until_parked();
+
+ // Start the language server by opening a buffer with a compatible file extension.
+ let _buffer = project
+ .update(cx, |project, cx| {
+ project.open_local_buffer("/the-root/a.rs", cx)
+ })
+ .await
+ .unwrap();
+
+ // Keep track of the FS events reported to the language server.
+ let fake_server = fake_servers.next().await.unwrap();
+ let file_changes = Arc::new(Mutex::new(Vec::new()));
+ fake_server
+ .request::<lsp::request::RegisterCapability>(lsp::RegistrationParams {
+ registrations: vec![lsp::Registration {
+ id: Default::default(),
+ method: "workspace/didChangeWatchedFiles".to_string(),
+ register_options: serde_json::to_value(
+ lsp::DidChangeWatchedFilesRegistrationOptions {
+ watchers: vec![lsp::FileSystemWatcher {
+ glob_pattern: "*.{rs,c}".to_string(),
+ kind: None,
+ }],
+ },
+ )
+ .ok(),
+ }],
+ })
+ .await
+ .unwrap();
+ fake_server.handle_notification::<lsp::notification::DidChangeWatchedFiles, _>({
+ let file_changes = file_changes.clone();
+ move |params, _| {
+ let mut file_changes = file_changes.lock();
+ file_changes.extend(params.changes);
+ file_changes.sort_by(|a, b| a.uri.cmp(&b.uri));
+ }
+ });
+
+ cx.foreground().run_until_parked();
+ assert_eq!(file_changes.lock().len(), 0);
+
+ // Perform some file system mutations, two of which match the watched patterns,
+ // and one of which does not.
+ fs.create_file("/the-root/c.rs".as_ref(), Default::default())
+ .await
+ .unwrap();
+ fs.create_file("/the-root/d.txt".as_ref(), Default::default())
+ .await
+ .unwrap();
+ fs.remove_file("/the-root/b.rs".as_ref(), Default::default())
+ .await
+ .unwrap();
+
+ // The language server receives events for the FS mutations that match its watch patterns.
+ cx.foreground().run_until_parked();
+ assert_eq!(
+ &*file_changes.lock(),
+ &[
+ lsp::FileEvent {
+ uri: lsp::Url::from_file_path("/the-root/b.rs").unwrap(),
+ typ: lsp::FileChangeType::DELETED,
+ },
+ lsp::FileEvent {
+ uri: lsp::Url::from_file_path("/the-root/c.rs").unwrap(),
+ typ: lsp::FileChangeType::CREATED,
+ },
+ ]
+ );
+}
+
#[gpui::test]
async fn test_single_file_worktrees_diagnostics(cx: &mut gpui::TestAppContext) {
cx.foreground().forbid_parking();
@@ -1585,7 +1699,7 @@ async fn test_edits_from_lsp_with_edits_on_adjacent_lines(cx: &mut gpui::TestApp
buffer.text(),
"
use a::{b, c};
-
+
fn f() {
b();
c();
@@ -1603,7 +1717,7 @@ async fn test_invalid_edits_from_lsp(cx: &mut gpui::TestAppContext) {
let text = "
use a::b;
use a::c;
-
+
fn f() {
b();
c();
@@ -1688,7 +1802,7 @@ async fn test_invalid_edits_from_lsp(cx: &mut gpui::TestAppContext) {
buffer.text(),
"
use a::{b, c};
-
+
fn f() {
b();
c();
@@ -1,18 +1,18 @@
-use super::{ignore::IgnoreStack, DiagnosticSummary};
-use crate::{copy_recursive, ProjectEntryId, RemoveOptions};
+use crate::{
+ copy_recursive, ignore::IgnoreStack, DiagnosticSummary, ProjectEntryId, RemoveOptions,
+};
use ::ignore::gitignore::{Gitignore, GitignoreBuilder};
use anyhow::{anyhow, Context, Result};
use client::{proto, Client};
use clock::ReplicaId;
use collections::{HashMap, VecDeque};
-use fs::LineEnding;
-use fs::{repository::GitRepository, Fs};
+use fs::{repository::GitRepository, Fs, LineEnding};
use futures::{
channel::{
mpsc::{self, UnboundedSender},
oneshot,
},
- Stream, StreamExt,
+ select_biased, Stream, StreamExt,
};
use fuzzy::CharBag;
use git::{DOT_GIT, GITIGNORE};
@@ -20,20 +20,19 @@ use gpui::{
executor, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext,
Task,
};
-use language::File as _;
use language::{
proto::{
deserialize_fingerprint, deserialize_version, serialize_fingerprint, serialize_line_ending,
serialize_version,
},
- Buffer, DiagnosticEntry, PointUtf16, Rope, RopeFingerprint, Unclipped,
+ Buffer, DiagnosticEntry, File as _, PointUtf16, Rope, RopeFingerprint, Unclipped,
};
use parking_lot::Mutex;
use postage::{
+ barrier,
prelude::{Sink as _, Stream as _},
watch,
};
-
use smol::channel::{self, Sender};
use std::{
any::Any,
@@ -45,18 +44,19 @@ use std::{
mem,
ops::{Deref, DerefMut},
path::{Path, PathBuf},
- sync::{atomic::AtomicUsize, Arc},
+ sync::{
+ atomic::{AtomicUsize, Ordering::SeqCst},
+ Arc,
+ },
task::Poll,
time::{Duration, SystemTime},
};
use sum_tree::{Bias, Edit, SeekTarget, SumTree, TreeMap, TreeSet};
-use util::paths::HOME;
-use util::{ResultExt, TryFutureExt};
+use util::{paths::HOME, ResultExt, TryFutureExt};
#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, PartialOrd, Ord)]
pub struct WorktreeId(usize);
-#[allow(clippy::large_enum_variant)]
pub enum Worktree {
Local(LocalWorktree),
Remote(RemoteWorktree),
@@ -64,10 +64,9 @@ pub enum Worktree {
pub struct LocalWorktree {
snapshot: LocalSnapshot,
- background_snapshot: Arc<Mutex<LocalSnapshot>>,
- last_scan_state_rx: watch::Receiver<ScanState>,
- _background_scanner_task: Option<Task<()>>,
- poll_task: Option<Task<()>>,
+ path_changes_tx: channel::Sender<(Vec<PathBuf>, barrier::Sender)>,
+ is_scanning: (watch::Sender<bool>, watch::Receiver<bool>),
+ _background_scanner_task: Task<()>,
share: Option<ShareState>,
diagnostics: HashMap<Arc<Path>, Vec<DiagnosticEntry<Unclipped<PointUtf16>>>>,
diagnostic_summaries: TreeMap<PathKey, DiagnosticSummary>,
@@ -77,8 +76,8 @@ pub struct LocalWorktree {
}
pub struct RemoteWorktree {
- pub snapshot: Snapshot,
- pub(crate) background_snapshot: Arc<Mutex<Snapshot>>,
+ snapshot: Snapshot,
+ background_snapshot: Arc<Mutex<Snapshot>>,
project_id: u64,
client: Arc<Client>,
updates_tx: Option<UnboundedSender<proto::UpdateWorktree>>,
@@ -118,11 +117,11 @@ impl std::fmt::Debug for GitRepositoryEntry {
f.debug_struct("GitRepositoryEntry")
.field("content_path", &self.content_path)
.field("git_dir_path", &self.git_dir_path)
- .field("libgit_repository", &"LibGitRepository")
.finish()
}
}
+#[derive(Debug)]
pub struct LocalSnapshot {
ignores_by_parent_abs_path: HashMap<Arc<Path>, (Arc<Gitignore>, usize)>,
git_repositories: Vec<GitRepositoryEntry>,
@@ -157,14 +156,22 @@ impl DerefMut for LocalSnapshot {
}
}
-#[derive(Clone, Debug)]
enum ScanState {
- Idle,
/// The worktree is performing its initial scan of the filesystem.
- Initializing,
+ Initializing {
+ snapshot: LocalSnapshot,
+ barrier: Option<barrier::Sender>,
+ },
+ Initialized {
+ snapshot: LocalSnapshot,
+ },
/// The worktree is updating in response to filesystem events.
Updating,
- Err(Arc<anyhow::Error>),
+ Updated {
+ snapshot: LocalSnapshot,
+ changes: HashMap<Arc<Path>, PathChange>,
+ barrier: Option<barrier::Sender>,
+ },
}
struct ShareState {
@@ -175,7 +182,7 @@ struct ShareState {
}
pub enum Event {
- UpdatedEntries,
+ UpdatedEntries(HashMap<Arc<Path>, PathChange>),
UpdatedGitRepositories(Vec<GitRepositoryEntry>),
}
@@ -192,21 +199,87 @@ impl Worktree {
next_entry_id: Arc<AtomicUsize>,
cx: &mut AsyncAppContext,
) -> Result<ModelHandle<Self>> {
- let (tree, scan_states_tx) =
- LocalWorktree::create(client, path, visible, fs.clone(), next_entry_id, cx).await?;
- tree.update(cx, |tree, cx| {
- let tree = tree.as_local_mut().unwrap();
- let abs_path = tree.abs_path().clone();
- let background_snapshot = tree.background_snapshot.clone();
- let background = cx.background().clone();
- tree._background_scanner_task = Some(cx.background().spawn(async move {
- let events = fs.watch(&abs_path, Duration::from_millis(100)).await;
- let scanner =
- BackgroundScanner::new(background_snapshot, scan_states_tx, fs, background);
- scanner.run(events).await;
- }));
- });
- Ok(tree)
+ // After determining whether the root entry is a file or a directory, populate the
+ // snapshot's "root name", which will be used for the purpose of fuzzy matching.
+ let abs_path = path.into();
+ let metadata = fs
+ .metadata(&abs_path)
+ .await
+ .context("failed to stat worktree path")?;
+
+ Ok(cx.add_model(move |cx: &mut ModelContext<Worktree>| {
+ let root_name = abs_path
+ .file_name()
+ .map_or(String::new(), |f| f.to_string_lossy().to_string());
+
+ let mut snapshot = LocalSnapshot {
+ ignores_by_parent_abs_path: Default::default(),
+ git_repositories: Default::default(),
+ removed_entry_ids: Default::default(),
+ next_entry_id,
+ snapshot: Snapshot {
+ id: WorktreeId::from_usize(cx.model_id()),
+ abs_path: abs_path.clone(),
+ root_name: root_name.clone(),
+ root_char_bag: root_name.chars().map(|c| c.to_ascii_lowercase()).collect(),
+ entries_by_path: Default::default(),
+ entries_by_id: Default::default(),
+ scan_id: 0,
+ completed_scan_id: 0,
+ },
+ };
+
+ if let Some(metadata) = metadata {
+ snapshot.insert_entry(
+ Entry::new(
+ Arc::from(Path::new("")),
+ &metadata,
+ &snapshot.next_entry_id,
+ snapshot.root_char_bag,
+ ),
+ fs.as_ref(),
+ );
+ }
+
+ let (path_changes_tx, path_changes_rx) = channel::unbounded();
+ let (scan_states_tx, mut scan_states_rx) = mpsc::unbounded();
+
+ cx.spawn_weak(|this, mut cx| async move {
+ while let Some((state, this)) = scan_states_rx.next().await.zip(this.upgrade(&cx)) {
+ this.update(&mut cx, |this, cx| {
+ this.as_local_mut()
+ .unwrap()
+ .background_scanner_updated(state, cx);
+ });
+ }
+ })
+ .detach();
+
+ let background_scanner_task = cx.background().spawn({
+ let fs = fs.clone();
+ let snapshot = snapshot.clone();
+ let background = cx.background().clone();
+ async move {
+ let events = fs.watch(&abs_path, Duration::from_millis(100)).await;
+ BackgroundScanner::new(snapshot, scan_states_tx, fs, background)
+ .run(events, path_changes_rx)
+ .await;
+ }
+ });
+
+ Worktree::Local(LocalWorktree {
+ snapshot,
+ is_scanning: watch::channel_with(true),
+ share: None,
+ path_changes_tx,
+ _background_scanner_task: background_scanner_task,
+ diagnostics: Default::default(),
+ diagnostic_summaries: Default::default(),
+ client,
+ fs,
+ visible,
+ })
+ }))
}
pub fn remote(
@@ -216,64 +289,50 @@ impl Worktree {
client: Arc<Client>,
cx: &mut MutableAppContext,
) -> ModelHandle<Self> {
- let remote_id = worktree.id;
- let root_char_bag: CharBag = worktree
- .root_name
- .chars()
- .map(|c| c.to_ascii_lowercase())
- .collect();
- let root_name = worktree.root_name.clone();
- let visible = worktree.visible;
-
- let abs_path = PathBuf::from(worktree.abs_path);
- let snapshot = Snapshot {
- id: WorktreeId(remote_id as usize),
- abs_path: Arc::from(abs_path.deref()),
- root_name,
- root_char_bag,
- entries_by_path: Default::default(),
- entries_by_id: Default::default(),
- scan_id: 0,
- completed_scan_id: 0,
- };
-
- let (updates_tx, mut updates_rx) = mpsc::unbounded();
- let background_snapshot = Arc::new(Mutex::new(snapshot.clone()));
- let (mut snapshot_updated_tx, mut snapshot_updated_rx) = watch::channel();
- let worktree_handle = cx.add_model(|_: &mut ModelContext<Worktree>| {
- Worktree::Remote(RemoteWorktree {
- project_id: project_remote_id,
- replica_id,
- snapshot: snapshot.clone(),
- background_snapshot: background_snapshot.clone(),
- updates_tx: Some(updates_tx),
- snapshot_subscriptions: Default::default(),
- client: client.clone(),
- diagnostic_summaries: Default::default(),
- visible,
- disconnected: false,
- })
- });
+ cx.add_model(|cx: &mut ModelContext<Self>| {
+ let snapshot = Snapshot {
+ id: WorktreeId(worktree.id as usize),
+ abs_path: Arc::from(PathBuf::from(worktree.abs_path)),
+ root_name: worktree.root_name.clone(),
+ root_char_bag: worktree
+ .root_name
+ .chars()
+ .map(|c| c.to_ascii_lowercase())
+ .collect(),
+ entries_by_path: Default::default(),
+ entries_by_id: Default::default(),
+ scan_id: 0,
+ completed_scan_id: 0,
+ };
- cx.background()
- .spawn(async move {
- while let Some(update) = updates_rx.next().await {
- if let Err(error) = background_snapshot.lock().apply_remote_update(update) {
- log::error!("error applying worktree update: {}", error);
+ let (updates_tx, mut updates_rx) = mpsc::unbounded();
+ let background_snapshot = Arc::new(Mutex::new(snapshot.clone()));
+ let (mut snapshot_updated_tx, mut snapshot_updated_rx) = watch::channel();
+
+ cx.background()
+ .spawn({
+ let background_snapshot = background_snapshot.clone();
+ async move {
+ while let Some(update) = updates_rx.next().await {
+ if let Err(error) =
+ background_snapshot.lock().apply_remote_update(update)
+ {
+ log::error!("error applying worktree update: {}", error);
+ }
+ snapshot_updated_tx.send(()).await.ok();
+ }
}
- snapshot_updated_tx.send(()).await.ok();
- }
- })
- .detach();
+ })
+ .detach();
- cx.spawn(|mut cx| {
- let this = worktree_handle.downgrade();
- async move {
+ cx.spawn_weak(|this, mut cx| async move {
while (snapshot_updated_rx.recv().await).is_some() {
if let Some(this) = this.upgrade(&cx) {
this.update(&mut cx, |this, cx| {
- this.poll_snapshot(cx);
let this = this.as_remote_mut().unwrap();
+ this.snapshot = this.background_snapshot.lock().clone();
+ cx.emit(Event::UpdatedEntries(Default::default()));
+ cx.notify();
while let Some((scan_id, _)) = this.snapshot_subscriptions.front() {
if this.observed_snapshot(*scan_id) {
let (_, tx) = this.snapshot_subscriptions.pop_front().unwrap();
@@ -287,11 +346,22 @@ impl Worktree {
break;
}
}
- }
- })
- .detach();
+ })
+ .detach();
- worktree_handle
+ Worktree::Remote(RemoteWorktree {
+ project_id: project_remote_id,
+ replica_id,
+ snapshot: snapshot.clone(),
+ background_snapshot,
+ updates_tx: Some(updates_tx),
+ snapshot_subscriptions: Default::default(),
+ client: client.clone(),
+ diagnostic_summaries: Default::default(),
+ visible: worktree.visible,
+ disconnected: false,
+ })
+ })
}
pub fn as_local(&self) -> Option<&LocalWorktree> {
@@ -380,13 +450,6 @@ impl Worktree {
.map(|(path, summary)| (path.0.clone(), *summary))
}
- fn poll_snapshot(&mut self, cx: &mut ModelContext<Self>) {
- match self {
- Self::Local(worktree) => worktree.poll_snapshot(false, cx),
- Self::Remote(worktree) => worktree.poll_snapshot(cx),
- };
- }
-
pub fn abs_path(&self) -> Arc<Path> {
match self {
Worktree::Local(worktree) => worktree.abs_path.clone(),
@@ -396,90 +459,6 @@ impl Worktree {
}
impl LocalWorktree {
- async fn create(
- client: Arc<Client>,
- path: impl Into<Arc<Path>>,
- visible: bool,
- fs: Arc<dyn Fs>,
- next_entry_id: Arc<AtomicUsize>,
- cx: &mut AsyncAppContext,
- ) -> Result<(ModelHandle<Worktree>, UnboundedSender<ScanState>)> {
- let abs_path = path.into();
- let path: Arc<Path> = Arc::from(Path::new(""));
-
- // After determining whether the root entry is a file or a directory, populate the
- // snapshot's "root name", which will be used for the purpose of fuzzy matching.
- let root_name = abs_path
- .file_name()
- .map_or(String::new(), |f| f.to_string_lossy().to_string());
- let root_char_bag = root_name.chars().map(|c| c.to_ascii_lowercase()).collect();
- let metadata = fs
- .metadata(&abs_path)
- .await
- .context("failed to stat worktree path")?;
-
- let (scan_states_tx, mut scan_states_rx) = mpsc::unbounded();
- let (mut last_scan_state_tx, last_scan_state_rx) =
- watch::channel_with(ScanState::Initializing);
- let tree = cx.add_model(move |cx: &mut ModelContext<Worktree>| {
- let mut snapshot = LocalSnapshot {
- ignores_by_parent_abs_path: Default::default(),
- git_repositories: Default::default(),
- removed_entry_ids: Default::default(),
- next_entry_id,
- snapshot: Snapshot {
- id: WorktreeId::from_usize(cx.model_id()),
- abs_path,
- root_name: root_name.clone(),
- root_char_bag,
- entries_by_path: Default::default(),
- entries_by_id: Default::default(),
- scan_id: 0,
- completed_scan_id: 0,
- },
- };
- if let Some(metadata) = metadata {
- let entry = Entry::new(
- path,
- &metadata,
- &snapshot.next_entry_id,
- snapshot.root_char_bag,
- );
- snapshot.insert_entry(entry, fs.as_ref());
- }
-
- let tree = Self {
- snapshot: snapshot.clone(),
- background_snapshot: Arc::new(Mutex::new(snapshot)),
- last_scan_state_rx,
- _background_scanner_task: None,
- share: None,
- poll_task: None,
- diagnostics: Default::default(),
- diagnostic_summaries: Default::default(),
- client,
- fs,
- visible,
- };
-
- cx.spawn_weak(|this, mut cx| async move {
- while let Some(scan_state) = scan_states_rx.next().await {
- if let Some(this) = this.upgrade(&cx) {
- last_scan_state_tx.blocking_send(scan_state).ok();
- this.update(&mut cx, |this, cx| this.poll_snapshot(cx));
- } else {
- break;
- }
- }
- })
- .detach();
-
- Worktree::Local(tree)
- });
-
- Ok((tree, scan_states_tx))
- }
-
pub fn contains_abs_path(&self, path: &Path) -> bool {
path.starts_with(&self.abs_path)
}
@@ -557,68 +536,54 @@ impl LocalWorktree {
Ok(updated)
}
- fn poll_snapshot(&mut self, force: bool, cx: &mut ModelContext<Worktree>) {
- self.poll_task.take();
-
- match self.scan_state() {
- ScanState::Idle => {
- let new_snapshot = self.background_snapshot.lock().clone();
- let updated_repos = Self::changed_repos(
- &self.snapshot.git_repositories,
- &new_snapshot.git_repositories,
- );
- self.snapshot = new_snapshot;
-
- if let Some(share) = self.share.as_mut() {
- *share.snapshots_tx.borrow_mut() = self.snapshot.clone();
- }
-
- cx.emit(Event::UpdatedEntries);
-
- if !updated_repos.is_empty() {
- cx.emit(Event::UpdatedGitRepositories(updated_repos));
- }
+ fn background_scanner_updated(
+ &mut self,
+ scan_state: ScanState,
+ cx: &mut ModelContext<Worktree>,
+ ) {
+ match scan_state {
+ ScanState::Initializing { snapshot, barrier } => {
+ *self.is_scanning.0.borrow_mut() = true;
+ self.set_snapshot(snapshot, cx);
+ drop(barrier);
}
-
- ScanState::Initializing => {
- let is_fake_fs = self.fs.is_fake();
-
- let new_snapshot = self.background_snapshot.lock().clone();
- let updated_repos = Self::changed_repos(
- &self.snapshot.git_repositories,
- &new_snapshot.git_repositories,
- );
- self.snapshot = new_snapshot;
-
- self.poll_task = Some(cx.spawn_weak(|this, mut cx| async move {
- if is_fake_fs {
- #[cfg(any(test, feature = "test-support"))]
- cx.background().simulate_random_delay().await;
- } else {
- smol::Timer::after(Duration::from_millis(100)).await;
- }
- if let Some(this) = this.upgrade(&cx) {
- this.update(&mut cx, |this, cx| this.poll_snapshot(cx));
- }
- }));
-
- cx.emit(Event::UpdatedEntries);
-
- if !updated_repos.is_empty() {
- cx.emit(Event::UpdatedGitRepositories(updated_repos));
- }
+ ScanState::Initialized { snapshot } => {
+ *self.is_scanning.0.borrow_mut() = false;
+ self.set_snapshot(snapshot, cx);
}
-
- _ => {
- if force {
- self.snapshot = self.background_snapshot.lock().clone();
- }
+ ScanState::Updating => {
+ *self.is_scanning.0.borrow_mut() = true;
+ }
+ ScanState::Updated {
+ snapshot,
+ changes,
+ barrier,
+ } => {
+ *self.is_scanning.0.borrow_mut() = false;
+ cx.emit(Event::UpdatedEntries(changes));
+ self.set_snapshot(snapshot, cx);
+ drop(barrier);
}
}
-
cx.notify();
}
+ fn set_snapshot(&mut self, new_snapshot: LocalSnapshot, cx: &mut ModelContext<Worktree>) {
+ let updated_repos = Self::changed_repos(
+ &self.snapshot.git_repositories,
+ &new_snapshot.git_repositories,
+ );
+ self.snapshot = new_snapshot;
+
+ if let Some(share) = self.share.as_mut() {
+ *share.snapshots_tx.borrow_mut() = self.snapshot.clone();
+ }
+
+ if !updated_repos.is_empty() {
+ cx.emit(Event::UpdatedGitRepositories(updated_repos));
+ }
+ }
+
fn changed_repos(
old_repos: &[GitRepositoryEntry],
new_repos: &[GitRepositoryEntry],
@@ -648,19 +613,19 @@ impl LocalWorktree {
}
pub fn scan_complete(&self) -> impl Future<Output = ()> {
- let mut scan_state_rx = self.last_scan_state_rx.clone();
+ let mut is_scanning_rx = self.is_scanning.1.clone();
async move {
- let mut scan_state = Some(scan_state_rx.borrow().clone());
- while let Some(ScanState::Initializing | ScanState::Updating) = scan_state {
- scan_state = scan_state_rx.recv().await;
+ let mut is_scanning = is_scanning_rx.borrow().clone();
+ while is_scanning {
+ if let Some(value) = is_scanning_rx.recv().await {
+ is_scanning = value;
+ } else {
+ break;
+ }
}
}
}
- fn scan_state(&self) -> ScanState {
- self.last_scan_state_rx.borrow().clone()
- }
-
pub fn snapshot(&self) -> LocalSnapshot {
self.snapshot.clone()
}
@@ -704,9 +669,7 @@ impl LocalWorktree {
// Eagerly populate the snapshot with an updated entry for the loaded file
let entry = this
.update(&mut cx, |this, cx| {
- this.as_local()
- .unwrap()
- .refresh_entry(path, abs_path, None, cx)
+ this.as_local().unwrap().refresh_entry(path, None, cx)
})
.await?;
@@ -797,15 +760,25 @@ impl LocalWorktree {
is_dir: bool,
cx: &mut ModelContext<Worktree>,
) -> Task<Result<Entry>> {
- self.write_entry_internal(
- path,
+ let path = path.into();
+ let abs_path = self.absolutize(&path);
+ let fs = self.fs.clone();
+ let write = cx.background().spawn(async move {
if is_dir {
- None
+ fs.create_dir(&abs_path).await
} else {
- Some(Default::default())
- },
- cx,
- )
+ fs.save(&abs_path, &Default::default(), Default::default())
+ .await
+ }
+ });
+
+ cx.spawn(|this, mut cx| async move {
+ write.await?;
+ this.update(&mut cx, |this, cx| {
+ this.as_local_mut().unwrap().refresh_entry(path, None, cx)
+ })
+ .await
+ })
}
pub fn write_file(
@@ -815,7 +788,20 @@ impl LocalWorktree {
line_ending: LineEnding,
cx: &mut ModelContext<Worktree>,
) -> Task<Result<Entry>> {
- self.write_entry_internal(path, Some((text, line_ending)), cx)
+ let path = path.into();
+ let abs_path = self.absolutize(&path);
+ let fs = self.fs.clone();
+ let write = cx
+ .background()
+ .spawn(async move { fs.save(&abs_path, &text, line_ending).await });
+
+ cx.spawn(|this, mut cx| async move {
+ write.await?;
+ this.update(&mut cx, |this, cx| {
+ this.as_local_mut().unwrap().refresh_entry(path, None, cx)
+ })
+ .await
+ })
}
pub fn delete_entry(
@@ -824,36 +810,40 @@ impl LocalWorktree {
cx: &mut ModelContext<Worktree>,
) -> Option<Task<Result<()>>> {
let entry = self.entry_for_id(entry_id)?.clone();
- let abs_path = self.absolutize(&entry.path);
- let delete = cx.background().spawn({
- let fs = self.fs.clone();
- let abs_path = abs_path;
- async move {
- if entry.is_file() {
- fs.remove_file(&abs_path, Default::default()).await
- } else {
- fs.remove_dir(
- &abs_path,
- RemoveOptions {
- recursive: true,
- ignore_if_not_exists: false,
- },
- )
- .await
- }
+ let abs_path = self.abs_path.clone();
+ let fs = self.fs.clone();
+
+ let delete = cx.background().spawn(async move {
+ let mut abs_path = fs.canonicalize(&abs_path).await?;
+ if entry.path.file_name().is_some() {
+ abs_path = abs_path.join(&entry.path);
+ }
+ if entry.is_file() {
+ fs.remove_file(&abs_path, Default::default()).await?;
+ } else {
+ fs.remove_dir(
+ &abs_path,
+ RemoveOptions {
+ recursive: true,
+ ignore_if_not_exists: false,
+ },
+ )
+ .await?;
}
+ anyhow::Ok(abs_path)
});
Some(cx.spawn(|this, mut cx| async move {
- delete.await?;
- this.update(&mut cx, |this, cx| {
- let this = this.as_local_mut().unwrap();
- {
- let mut snapshot = this.background_snapshot.lock();
- snapshot.delete_entry(entry_id);
- }
- this.poll_snapshot(true, cx);
+ let abs_path = delete.await?;
+ let (tx, mut rx) = barrier::channel();
+ this.update(&mut cx, |this, _| {
+ this.as_local_mut()
+ .unwrap()
+ .path_changes_tx
+ .try_send((vec![abs_path], tx))
+ .unwrap();
});
+ rx.recv().await;
Ok(())
}))
}
@@ -867,29 +857,21 @@ impl LocalWorktree {
let old_path = self.entry_for_id(entry_id)?.path.clone();
let new_path = new_path.into();
let abs_old_path = self.absolutize(&old_path);
- let abs_new_path = self.absolutize(new_path.as_ref());
- let rename = cx.background().spawn({
- let fs = self.fs.clone();
- let abs_new_path = abs_new_path.clone();
- async move {
- fs.rename(&abs_old_path, &abs_new_path, Default::default())
- .await
- }
+ let abs_new_path = self.absolutize(&new_path);
+ let fs = self.fs.clone();
+ let rename = cx.background().spawn(async move {
+ fs.rename(&abs_old_path, &abs_new_path, Default::default())
+ .await
});
Some(cx.spawn(|this, mut cx| async move {
rename.await?;
- let entry = this
- .update(&mut cx, |this, cx| {
- this.as_local_mut().unwrap().refresh_entry(
- new_path.clone(),
- abs_new_path,
- Some(old_path),
- cx,
- )
- })
- .await?;
- Ok(entry)
+ this.update(&mut cx, |this, cx| {
+ this.as_local_mut()
+ .unwrap()
+ .refresh_entry(new_path.clone(), Some(old_path), cx)
+ })
+ .await
}))
}
@@ -903,111 +885,63 @@ impl LocalWorktree {
let new_path = new_path.into();
let abs_old_path = self.absolutize(&old_path);
let abs_new_path = self.absolutize(&new_path);
- let copy = cx.background().spawn({
- let fs = self.fs.clone();
- let abs_new_path = abs_new_path.clone();
- async move {
- copy_recursive(
- fs.as_ref(),
- &abs_old_path,
- &abs_new_path,
- Default::default(),
- )
- .await
- }
+ let fs = self.fs.clone();
+ let copy = cx.background().spawn(async move {
+ copy_recursive(
+ fs.as_ref(),
+ &abs_old_path,
+ &abs_new_path,
+ Default::default(),
+ )
+ .await
});
Some(cx.spawn(|this, mut cx| async move {
copy.await?;
- let entry = this
- .update(&mut cx, |this, cx| {
- this.as_local_mut().unwrap().refresh_entry(
- new_path.clone(),
- abs_new_path,
- None,
- cx,
- )
- })
- .await?;
- Ok(entry)
+ this.update(&mut cx, |this, cx| {
+ this.as_local_mut()
+ .unwrap()
+ .refresh_entry(new_path.clone(), None, cx)
+ })
+ .await
}))
}
- fn write_entry_internal(
- &self,
- path: impl Into<Arc<Path>>,
- text_if_file: Option<(Rope, LineEnding)>,
- cx: &mut ModelContext<Worktree>,
- ) -> Task<Result<Entry>> {
- let path = path.into();
- let abs_path = self.absolutize(&path);
- let write = cx.background().spawn({
- let fs = self.fs.clone();
- let abs_path = abs_path.clone();
- async move {
- if let Some((text, line_ending)) = text_if_file {
- fs.save(&abs_path, &text, line_ending).await
- } else {
- fs.create_dir(&abs_path).await
- }
- }
- });
-
- cx.spawn(|this, mut cx| async move {
- write.await?;
- let entry = this
- .update(&mut cx, |this, cx| {
- this.as_local_mut()
- .unwrap()
- .refresh_entry(path, abs_path, None, cx)
- })
- .await?;
- Ok(entry)
- })
- }
-
fn refresh_entry(
&self,
path: Arc<Path>,
- abs_path: PathBuf,
old_path: Option<Arc<Path>>,
cx: &mut ModelContext<Worktree>,
) -> Task<Result<Entry>> {
let fs = self.fs.clone();
- let root_char_bag;
- let next_entry_id;
- {
- let snapshot = self.background_snapshot.lock();
- root_char_bag = snapshot.root_char_bag;
- next_entry_id = snapshot.next_entry_id.clone();
- }
- cx.spawn_weak(|this, mut cx| async move {
- let metadata = fs
- .metadata(&abs_path)
- .await?
- .ok_or_else(|| anyhow!("could not read saved file metadata"))?;
- let this = this
- .upgrade(&cx)
- .ok_or_else(|| anyhow!("worktree was dropped"))?;
- this.update(&mut cx, |this, cx| {
- let this = this.as_local_mut().unwrap();
- let inserted_entry;
- {
- let mut snapshot = this.background_snapshot.lock();
- let mut entry = Entry::new(path, &metadata, &next_entry_id, root_char_bag);
- entry.is_ignored = snapshot
- .ignore_stack_for_abs_path(&abs_path, entry.is_dir())
- .is_abs_path_ignored(&abs_path, entry.is_dir());
- if let Some(old_path) = old_path {
- snapshot.remove_path(&old_path);
- }
- snapshot.scan_started();
- inserted_entry = snapshot.insert_entry(entry, fs.as_ref());
- snapshot.scan_completed();
- }
- this.poll_snapshot(true, cx);
- Ok(inserted_entry)
- })
+ let abs_root_path = self.abs_path.clone();
+ let path_changes_tx = self.path_changes_tx.clone();
+ cx.spawn_weak(move |this, mut cx| async move {
+ let abs_path = fs.canonicalize(&abs_root_path).await?;
+ let mut paths = Vec::with_capacity(2);
+ paths.push(if path.file_name().is_some() {
+ abs_path.join(&path)
+ } else {
+ abs_path.clone()
+ });
+ if let Some(old_path) = old_path {
+ paths.push(if old_path.file_name().is_some() {
+ abs_path.join(&old_path)
+ } else {
+ abs_path.clone()
+ });
+ }
+
+ let (tx, mut rx) = barrier::channel();
+ path_changes_tx.try_send((paths, tx)).unwrap();
+ rx.recv().await;
+ this.upgrade(&cx)
+ .ok_or_else(|| anyhow!("worktree was dropped"))?
+ .update(&mut cx, |this, _| {
+ this.entry_for_path(path)
+ .cloned()
+ .ok_or_else(|| anyhow!("failed to read path after update"))
+ })
})
}
@@ -1109,12 +1043,6 @@ impl RemoteWorktree {
self.snapshot.clone()
}
- fn poll_snapshot(&mut self, cx: &mut ModelContext<Worktree>) {
- self.snapshot = self.background_snapshot.lock().clone();
- cx.emit(Event::UpdatedEntries);
- cx.notify();
- }
-
pub fn disconnected_from_host(&mut self) {
self.updates_tx.take();
self.snapshot_subscriptions.clear();
@@ -1274,28 +1202,25 @@ impl Snapshot {
Ok(entry)
}
- fn delete_entry(&mut self, entry_id: ProjectEntryId) -> bool {
- if let Some(removed_entry) = self.entries_by_id.remove(&entry_id, &()) {
- self.entries_by_path = {
- let mut cursor = self.entries_by_path.cursor();
- let mut new_entries_by_path =
- cursor.slice(&TraversalTarget::Path(&removed_entry.path), Bias::Left, &());
- while let Some(entry) = cursor.item() {
- if entry.path.starts_with(&removed_entry.path) {
- self.entries_by_id.remove(&entry.id, &());
- cursor.next(&());
- } else {
- break;
- }
+ fn delete_entry(&mut self, entry_id: ProjectEntryId) -> Option<Arc<Path>> {
+ let removed_entry = self.entries_by_id.remove(&entry_id, &())?;
+ self.entries_by_path = {
+ let mut cursor = self.entries_by_path.cursor();
+ let mut new_entries_by_path =
+ cursor.slice(&TraversalTarget::Path(&removed_entry.path), Bias::Left, &());
+ while let Some(entry) = cursor.item() {
+ if entry.path.starts_with(&removed_entry.path) {
+ self.entries_by_id.remove(&entry.id, &());
+ cursor.next(&());
+ } else {
+ break;
}
- new_entries_by_path.push_tree(cursor.suffix(&()), &());
- new_entries_by_path
- };
+ }
+ new_entries_by_path.push_tree(cursor.suffix(&()), &());
+ new_entries_by_path
+ };
- true
- } else {
- false
- }
+ Some(removed_entry.path)
}
pub(crate) fn apply_remote_update(&mut self, update: proto::UpdateWorktree) -> Result<()> {
@@ -19,7 +19,7 @@ settings = { path = "../settings" }
theme = { path = "../theme" }
util = { path = "../util" }
workspace = { path = "../workspace" }
-postage = { version = "0.4.1", features = ["futures-traits"] }
+postage = { workspace = true }
futures = "0.3"
unicase = "2.6"
@@ -20,7 +20,7 @@ workspace = { path = "../workspace" }
util = { path = "../util" }
anyhow = "1.0.38"
ordered-float = "2.1.1"
-postage = { version = "0.4", features = ["futures-traits"] }
+postage = { workspace = true }
smol = "1.2"
[dev-dependencies]
@@ -19,5 +19,5 @@ settings = { path = "../settings" }
text = { path = "../text" }
workspace = { path = "../workspace" }
ordered-float = "2.1.1"
-postage = { version = "0.4", features = ["futures-traits"] }
+postage = { workspace = true }
smol = "1.2"
@@ -22,7 +22,7 @@ workspace = { path = "../workspace" }
anyhow = "1.0"
futures = "0.3"
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
-postage = { version = "0.4.1", features = ["futures-traits"] }
+postage = { workspace = true }
serde = { version = "1.0", features = ["derive", "rc"] }
serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
smallvec = { version = "1.6", features = ["union"] }
@@ -22,7 +22,7 @@ futures = "0.3"
theme = { path = "../theme" }
util = { path = "../util" }
json_comments = "0.2"
-postage = { version = "0.4.1", features = ["futures-traits"] }
+postage = { workspace = true }
schemars = "0.8"
serde = { workspace = true }
serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
@@ -22,7 +22,7 @@ digest = { version = "0.9", features = ["std"] }
lazy_static = "1.4"
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
parking_lot = "0.11"
-postage = { version = "0.4.1", features = ["futures-traits"] }
+postage = { workspace = true }
rand = { version = "0.8.3", optional = true }
smallvec = { version = "1.6", features = ["union"] }
util = { path = "../util" }
@@ -19,6 +19,5 @@ workspace = { path = "../workspace" }
util = { path = "../util" }
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
parking_lot = "0.11.1"
-postage = { version = "0.4.1", features = ["futures-traits"] }
+postage = { workspace = true }
smol = "1.2.5"
-
@@ -2,7 +2,7 @@ use std::ops::{Deref, DerefMut};
use collections::{HashMap, HashSet};
use gpui::ContextHandle;
-use language::{OffsetRangeExt, Point};
+use language::OffsetRangeExt;
use util::test::marked_text_offsets;
use super::{neovim_connection::NeovimConnection, NeovimBackedBindingTestContext, VimTestContext};
@@ -108,11 +108,7 @@ impl<'a> NeovimBackedTestContext<'a> {
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::<Point>(cx));
- let text = self.buffer_text();
- self.neovim.set_state(selection, &text).await;
-
+ self.neovim.set_state(marked_text).await;
context_handle
}
@@ -9,7 +9,7 @@ use async_trait::async_trait;
#[cfg(feature = "neovim")]
use gpui::keymap_matcher::Keystroke;
-use language::{Point, Selection};
+use language::Point;
#[cfg(feature = "neovim")]
use lazy_static::lazy_static;
@@ -36,11 +36,11 @@ lazy_static! {
static ref NEOVIM_LOCK: ReentrantMutex<()> = ReentrantMutex::new(());
}
-#[derive(Serialize, Deserialize)]
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub enum NeovimData {
- Text(String),
- Selection { start: (u32, u32), end: (u32, u32) },
- Mode(Option<Mode>),
+ Put { state: String },
+ Key(String),
+ Get { state: String, mode: Option<Mode> },
}
pub struct NeovimConnection {
@@ -117,18 +117,30 @@ impl NeovimConnection {
let key = format!("{start}{shift}{ctrl}{alt}{cmd}{}{end}", keystroke.key);
+ self.data
+ .push_back(NeovimData::Key(keystroke_text.to_string()));
self.nvim
.input(&key)
.await
.expect("Could not input keystroke");
}
- // 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) {}
+ pub async fn send_keystroke(&mut self, keystroke_text: &str) {
+ if matches!(self.data.front(), Some(NeovimData::Get { .. })) {
+ self.data.pop_front();
+ }
+ assert_eq!(
+ self.data.pop_front(),
+ Some(NeovimData::Key(keystroke_text.to_string())),
+ "operation does not match recorded script. re-record with --features=neovim"
+ );
+ }
#[cfg(feature = "neovim")]
- pub async fn set_state(&mut self, selection: Selection<Point>, text: &str) {
+ pub async fn set_state(&mut self, marked_text: &str) {
+ let (text, selection) = parse_state(&marked_text);
+
let nvim_buffer = self
.nvim
.get_current_buf()
@@ -162,18 +174,41 @@ impl NeovimConnection {
if !selection.is_empty() {
panic!("Setting neovim state with non empty selection not yet supported");
}
- let cursor = selection.head();
+ let cursor = selection.start;
nvim_window
.set_cursor((cursor.row as i64 + 1, cursor.column as i64))
.await
.expect("Could not set nvim cursor position");
+
+ if let Some(NeovimData::Get { mode, state }) = self.data.back() {
+ if *mode == Some(Mode::Normal) && *state == marked_text {
+ return;
+ }
+ }
+ self.data.push_back(NeovimData::Put {
+ state: marked_text.to_string(),
+ })
}
#[cfg(not(feature = "neovim"))]
- pub async fn set_state(&mut self, _selection: Selection<Point>, _text: &str) {}
+ pub async fn set_state(&mut self, marked_text: &str) {
+ if let Some(NeovimData::Get { mode, state: text }) = self.data.front() {
+ if *mode == Some(Mode::Normal) && *text == marked_text {
+ return;
+ }
+ self.data.pop_front();
+ }
+ assert_eq!(
+ self.data.pop_front(),
+ Some(NeovimData::Put {
+ state: marked_text.to_string()
+ }),
+ "operation does not match recorded script. re-record with --features=neovim"
+ );
+ }
#[cfg(feature = "neovim")]
- pub async fn text(&mut self) -> String {
+ pub async fn state(&mut self) -> (Option<Mode>, String, Range<Point>) {
let nvim_buffer = self
.nvim
.get_current_buf()
@@ -185,22 +220,6 @@ impl NeovimConnection {
.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('.')")
@@ -218,7 +237,30 @@ impl NeovimConnection {
.unwrap()
- 1; // Neovim columns start at 1
- let (start, end) = if let Some(Mode::Visual { .. }) = self.mode().await {
+ 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,
+ };
+
+ let (start, end) = if let Some(Mode::Visual { .. }) = mode {
self.nvim
.input("<escape>")
.await
@@ -243,72 +285,54 @@ impl NeovimConnection {
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),
+ Point::new(end_row as u32 - 1, end_col as u32),
+ Point::new(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),
+ Point::new(start_row as u32 - 1, start_col as u32),
+ Point::new(end_row as u32 - 1, end_col as u32),
)
}
} else {
- ((cursor_row, cursor_col), (cursor_row, cursor_col))
+ (
+ Point::new(cursor_row, cursor_col),
+ Point::new(cursor_row, cursor_col),
+ )
};
- self.data.push_back(NeovimData::Selection { start, end });
+ let state = NeovimData::Get {
+ mode,
+ state: encode_range(&text, start..end),
+ };
+
+ if self.data.back() != Some(&state) {
+ self.data.push_back(state.clone());
+ }
- Point::new(start.0, start.1)..Point::new(end.0, end.1)
+ (mode, text, start..end)
}
#[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)
+ pub async fn state(&mut self) -> (Option<Mode>, String, Range<Point>) {
+ if let Some(NeovimData::Get { state: text, mode }) = self.data.front() {
+ let (text, range) = parse_state(text);
+ (*mode, text, range)
} else {
- panic!("Invalid test data. Is test deterministic? Try running with '--features neovim' to regenerate");
+ panic!("operation does not match recorded script. re-record with --features=neovim");
}
}
- #[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
+ pub async fn selection(&mut self) -> Range<Point> {
+ self.state().await.2
}
- #[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");
- }
+ self.state().await.0
+ }
+
+ pub async fn text(&mut self) -> String {
+ self.state().await.1
}
fn test_data_path(test_case_id: &str) -> PathBuf {
@@ -325,8 +349,27 @@ impl NeovimConnection {
"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'")
+ let mut result = VecDeque::new();
+ for line in json.lines() {
+ result.push_back(
+ serde_json::from_str(line)
+ .expect("invalid test data. regenerate it with '--features neovim'"),
+ );
+ }
+ result
+ }
+
+ #[cfg(feature = "neovim")]
+ fn write_test_data(test_case_id: &str, data: &VecDeque<NeovimData>) {
+ let path = Self::test_data_path(test_case_id);
+ let mut json = Vec::new();
+ for entry in data {
+ serde_json::to_writer(&mut json, entry).unwrap();
+ json.push(b'\n');
+ }
+ std::fs::create_dir_all(path.parent().unwrap())
+ .expect("could not create test data directory");
+ std::fs::write(path, json).expect("could not write out test data");
}
}
@@ -349,11 +392,7 @@ impl DerefMut for NeovimConnection {
#[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");
+ Self::write_test_data(&self.test_case_id, &self.data);
}
}
@@ -383,3 +422,52 @@ impl Handler for NvimHandler {
) {
}
}
+
+fn parse_state(marked_text: &str) -> (String, Range<Point>) {
+ let (text, ranges) = util::test::marked_text_ranges(marked_text, true);
+ let byte_range = ranges[0].clone();
+ let mut point_range = Point::zero()..Point::zero();
+ let mut ix = 0;
+ let mut position = Point::zero();
+ for c in text.chars().chain(['\0']) {
+ if ix == byte_range.start {
+ point_range.start = position;
+ }
+ if ix == byte_range.end {
+ point_range.end = position;
+ }
+ let len_utf8 = c.len_utf8();
+ ix += len_utf8;
+ if c == '\n' {
+ position.row += 1;
+ position.column = 0;
+ } else {
+ position.column += len_utf8 as u32;
+ }
+ }
+ (text, point_range)
+}
+
+#[cfg(feature = "neovim")]
+fn encode_range(text: &str, range: Range<Point>) -> String {
+ let mut byte_range = 0..0;
+ let mut ix = 0;
+ let mut position = Point::zero();
+ for c in text.chars().chain(['\0']) {
+ if position == range.start {
+ byte_range.start = ix;
+ }
+ if position == range.end {
+ byte_range.end = ix;
+ }
+ let len_utf8 = c.len_utf8();
+ ix += len_utf8;
+ if c == '\n' {
+ position.row += 1;
+ position.column = 0;
+ } else {
+ position.column += len_utf8 as u32;
+ }
+ }
+ util::test::generate_marked_text(text, &[byte_range], true)
+}
@@ -1 +1,3 @@
-[{"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"}]
+{"Get":{"state":"ˇ","mode":"Normal"}}
+{"Put":{"state":"This is a tesˇt"}}
+{"Get":{"state":"This is a tesˇt","mode":"Normal"}}
@@ -1 +1,6 @@
-[{"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"}]
+{"Put":{"state":"The qˇuick"}}
+{"Key":"a"}
+{"Get":{"state":"The quˇick","mode":"Insert"}}
+{"Put":{"state":"The quicˇk"}}
+{"Key":"a"}
+{"Get":{"state":"The quickˇ","mode":"Insert"}}
@@ -1 +1,54 @@
@@ -1 +1,9 @@
-[{"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"}]
+{"Put":{"state":"ˇThe quick\nbrown"}}
+{"Key":"backspace"}
+{"Get":{"state":"ˇThe quick\nbrown","mode":"Normal"}}
+{"Put":{"state":"The qˇuick\nbrown"}}
+{"Key":"backspace"}
+{"Get":{"state":"The ˇquick\nbrown","mode":"Normal"}}
+{"Put":{"state":"The quick\nˇbrown"}}
+{"Key":"backspace"}
+{"Get":{"state":"The quicˇk\nbrown","mode":"Normal"}}
@@ -1 +1,570 @@
@@ -1 +1,24 @@
-[{"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"}]
+{"Put":{"state":"ˇ"}}
+{"Key":"c"}
+{"Key":"c"}
+{"Get":{"state":"ˇ","mode":"Insert"}}
+{"Put":{"state":"The ˇquick"}}
+{"Key":"c"}
+{"Key":"c"}
+{"Get":{"state":"ˇ","mode":"Insert"}}
+{"Put":{"state":"The quˇick\nbrown fox\njumps over"}}
+{"Key":"c"}
+{"Key":"c"}
+{"Get":{"state":"ˇ\nbrown fox\njumps over","mode":"Insert"}}
+{"Put":{"state":"The quick\nbrown ˇfox\njumps over"}}
+{"Key":"c"}
+{"Key":"c"}
+{"Get":{"state":"The quick\nˇ\njumps over","mode":"Insert"}}
+{"Put":{"state":"The quick\nbrown fox\njumps ˇover"}}
+{"Key":"c"}
+{"Key":"c"}
+{"Get":{"state":"The quick\nbrown fox\nˇ","mode":"Insert"}}
+{"Put":{"state":"The quick\nˇ\nbrown fox"}}
+{"Key":"c"}
+{"Key":"c"}
+{"Get":{"state":"The quick\nˇ\nbrown fox","mode":"Insert"}}
@@ -1 +1,8 @@
-[{"Text":"uick\nbrown fox"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"The quick\n\nbrown fox"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"}]
+{"Put":{"state":"The qˇuick\nbrown fox"}}
+{"Key":"c"}
+{"Key":"0"}
+{"Get":{"state":"ˇuick\nbrown fox","mode":"Insert"}}
+{"Put":{"state":"The quick\nˇ\nbrown fox"}}
+{"Key":"c"}
+{"Key":"0"}
+{"Get":{"state":"The quick\nˇ\nbrown fox","mode":"Insert"}}
@@ -1 +1,24 @@
-[{"Text":"st Test"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"test"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"Test1 test3"},{"Mode":"Insert"},{"Selection":{"start":[0,6],"end":[0,6]}},{"Mode":"Insert"},{"Text":"Test \ntest"},{"Mode":"Insert"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Insert"},{"Text":"Test \n\ntest"},{"Mode":"Insert"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Insert"},{"Text":"Test test"},{"Mode":"Insert"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Insert"}]
+{"Put":{"state":"Teˇst Test"}}
+{"Key":"c"}
+{"Key":"b"}
+{"Get":{"state":"ˇst Test","mode":"Insert"}}
+{"Put":{"state":"Test ˇtest"}}
+{"Key":"c"}
+{"Key":"b"}
+{"Get":{"state":"ˇtest","mode":"Insert"}}
+{"Put":{"state":"Test1 test2 ˇtest3"}}
+{"Key":"c"}
+{"Key":"b"}
+{"Get":{"state":"Test1 ˇtest3","mode":"Insert"}}
+{"Put":{"state":"Test test\nˇtest"}}
+{"Key":"c"}
+{"Key":"b"}
+{"Get":{"state":"Test ˇ\ntest","mode":"Insert"}}
+{"Put":{"state":"Test test\nˇ\ntest"}}
+{"Key":"c"}
+{"Key":"b"}
+{"Get":{"state":"Test ˇ\n\ntest","mode":"Insert"}}
+{"Put":{"state":"Test test-test ˇtest"}}
+{"Key":"c"}
+{"Key":"shift-b"}
+{"Get":{"state":"Test ˇtest","mode":"Insert"}}
@@ -1 +1,16 @@
-[{"Text":"Tst"},{"Mode":"Insert"},{"Selection":{"start":[0,1],"end":[0,1]}},{"Mode":"Insert"},{"Text":"est"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"Test"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"Testtest"},{"Mode":"Insert"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Insert"}]
+{"Put":{"state":"Teˇst"}}
+{"Key":"c"}
+{"Key":"backspace"}
+{"Get":{"state":"Tˇst","mode":"Insert"}}
+{"Put":{"state":"Tˇest"}}
+{"Key":"c"}
+{"Key":"backspace"}
+{"Get":{"state":"ˇest","mode":"Insert"}}
+{"Put":{"state":"ˇTest"}}
+{"Key":"c"}
+{"Key":"backspace"}
+{"Get":{"state":"ˇTest","mode":"Insert"}}
+{"Put":{"state":"Test\nˇtest"}}
+{"Key":"c"}
+{"Key":"backspace"}
+{"Get":{"state":"Testˇtest","mode":"Insert"}}
@@ -1 +1,24 @@
-[{"Text":"Te Test"},{"Mode":"Insert"},{"Selection":{"start":[0,2],"end":[0,2]}},{"Mode":"Insert"},{"Text":"T test"},{"Mode":"Insert"},{"Selection":{"start":[0,1],"end":[0,1]}},{"Mode":"Insert"},{"Text":"Test te\ntest"},{"Mode":"Insert"},{"Selection":{"start":[0,7],"end":[0,7]}},{"Mode":"Insert"},{"Text":"Test tes"},{"Mode":"Insert"},{"Selection":{"start":[0,8],"end":[0,8]}},{"Mode":"Insert"},{"Text":"Test test\n"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"Test te test"},{"Mode":"Insert"},{"Selection":{"start":[0,7],"end":[0,7]}},{"Mode":"Insert"}]
+{"Put":{"state":"Teˇst Test"}}
+{"Key":"c"}
+{"Key":"e"}
+{"Get":{"state":"Teˇ Test","mode":"Insert"}}
+{"Put":{"state":"Tˇest test"}}
+{"Key":"c"}
+{"Key":"e"}
+{"Get":{"state":"Tˇ test","mode":"Insert"}}
+{"Put":{"state":"Test teˇst\ntest"}}
+{"Key":"c"}
+{"Key":"e"}
+{"Get":{"state":"Test teˇ\ntest","mode":"Insert"}}
+{"Put":{"state":"Test tesˇt\ntest"}}
+{"Key":"c"}
+{"Key":"e"}
+{"Get":{"state":"Test tesˇ","mode":"Insert"}}
+{"Put":{"state":"Test test\nˇ\ntest"}}
+{"Key":"c"}
+{"Key":"e"}
+{"Get":{"state":"Test test\nˇ","mode":"Insert"}}
+{"Put":{"state":"Test teˇst-test test"}}
+{"Key":"c"}
+{"Key":"shift-e"}
+{"Get":{"state":"Test teˇ test","mode":"Insert"}}
@@ -1 +1,16 @@
-[{"Text":"The quick\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\nbrown fox\njumps over\n"},{"Mode":"Insert"},{"Selection":{"start":[3,0],"end":[3,0]}},{"Mode":"Insert"},{"Text":"The quick\nbrown fox\njumps over\n"},{"Mode":"Insert"},{"Selection":{"start":[3,0],"end":[3,0]}},{"Mode":"Insert"}]
+{"Put":{"state":"The quick\nbrownˇ fox\njumps over\nthe lazy"}}
+{"Key":"c"}
+{"Key":"shift-g"}
+{"Get":{"state":"The quick\nˇ","mode":"Insert"}}
+{"Put":{"state":"The quick\nbrownˇ fox\njumps over\nthe lazy"}}
+{"Key":"c"}
+{"Key":"shift-g"}
+{"Get":{"state":"The quick\nˇ","mode":"Insert"}}
+{"Put":{"state":"The quick\nbrown fox\njumps over\nthe lˇazy"}}
+{"Key":"c"}
+{"Key":"shift-g"}
+{"Get":{"state":"The quick\nbrown fox\njumps over\nˇ","mode":"Insert"}}
+{"Put":{"state":"The quick\nbrown fox\njumps over\nˇ"}}
+{"Key":"c"}
+{"Key":"shift-g"}
+{"Get":{"state":"The quick\nbrown fox\njumps over\nˇ","mode":"Insert"}}
@@ -1 +1,8 @@
-[{"Text":"The q\nbrown fox"},{"Mode":"Insert"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Insert"},{"Text":"The quick\n\nbrown fox"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"}]
+{"Put":{"state":"The qˇuick\nbrown fox"}}
+{"Key":"c"}
+{"Key":"$"}
+{"Get":{"state":"The qˇ\nbrown fox","mode":"Insert"}}
+{"Put":{"state":"The quick\nˇ\nbrown fox"}}
+{"Key":"c"}
+{"Key":"$"}
+{"Get":{"state":"The quick\nˇ\nbrown fox","mode":"Insert"}}
@@ -1 +1,20 @@
-[{"Text":"\njumps over\nthe lazy"},{"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\nthe lazy"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"\nbrown fox\njumps over\nthe lazy"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"}]
+{"Put":{"state":"The quick\nbrownˇ fox\njumps over\nthe lazy"}}
+{"Key":"c"}
+{"Key":"g"}
+{"Key":"g"}
+{"Get":{"state":"ˇ\njumps over\nthe lazy","mode":"Insert"}}
+{"Put":{"state":"The quick\nbrown fox\njumps over\nthe lˇazy"}}
+{"Key":"c"}
+{"Key":"g"}
+{"Key":"g"}
+{"Get":{"state":"ˇ","mode":"Insert"}}
+{"Put":{"state":"The qˇuick\nbrown fox\njumps over\nthe lazy"}}
+{"Key":"c"}
+{"Key":"g"}
+{"Key":"g"}
+{"Get":{"state":"ˇ\nbrown fox\njumps over\nthe lazy","mode":"Insert"}}
+{"Put":{"state":"ˇ\nbrown fox\njumps over\nthe lazy"}}
+{"Key":"c"}
+{"Key":"g"}
+{"Key":"g"}
+{"Get":{"state":"ˇ\nbrown fox\njumps over\nthe lazy","mode":"Insert"}}
@@ -1 +1,16 @@
-[{"Text":"Tst"},{"Mode":"Insert"},{"Selection":{"start":[0,1],"end":[0,1]}},{"Mode":"Insert"},{"Text":"est"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"Test"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"Test\ntest"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"}]
+{"Put":{"state":"Teˇst"}}
+{"Key":"c"}
+{"Key":"h"}
+{"Get":{"state":"Tˇst","mode":"Insert"}}
+{"Put":{"state":"Tˇest"}}
+{"Key":"c"}
+{"Key":"h"}
+{"Get":{"state":"ˇest","mode":"Insert"}}
+{"Put":{"state":"ˇTest"}}
+{"Key":"c"}
+{"Key":"h"}
+{"Get":{"state":"ˇTest","mode":"Insert"}}
+{"Put":{"state":"Test\nˇtest"}}
+{"Key":"c"}
+{"Key":"h"}
+{"Get":{"state":"Test\nˇtest","mode":"Insert"}}
@@ -1 +1,16 @@
-[{"Text":"The quick\n"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick\nbrown fox\njumps over"},{"Mode":"Normal"},{"Selection":{"start":[2,6],"end":[2,6]}},{"Mode":"Normal"},{"Text":"\njumps over"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"The quick\nbrown fox\n"},{"Mode":"Normal"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Normal"}]
+{"Put":{"state":"The quick\nbrown ˇfox\njumps over"}}
+{"Key":"c"}
+{"Key":"j"}
+{"Get":{"state":"The quick\nˇ","mode":"Insert"}}
+{"Put":{"state":"The quick\nbrown fox\njumps ˇover"}}
+{"Key":"c"}
+{"Key":"j"}
+{"Get":{"state":"The quick\nbrown fox\njumps ˇover","mode":"Normal"}}
+{"Put":{"state":"The qˇuick\nbrown fox\njumps over"}}
+{"Key":"c"}
+{"Key":"j"}
+{"Get":{"state":"ˇ\njumps over","mode":"Insert"}}
+{"Put":{"state":"The quick\nbrown fox\nˇ"}}
+{"Key":"c"}
+{"Key":"j"}
+{"Get":{"state":"The quick\nbrown fox\nˇ","mode":"Normal"}}
@@ -1 +1,16 @@
-[{"Text":"\njumps over"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"The quick\n"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick\nbrown fox\njumps over"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":"\nbrown fox\njumps over"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"}]
+{"Put":{"state":"The quick\nbrown ˇfox\njumps over"}}
+{"Key":"c"}
+{"Key":"k"}
+{"Get":{"state":"ˇ\njumps over","mode":"Insert"}}
+{"Put":{"state":"The quick\nbrown fox\njumps ˇover"}}
+{"Key":"c"}
+{"Key":"k"}
+{"Get":{"state":"The quick\nˇ","mode":"Insert"}}
+{"Put":{"state":"The qˇuick\nbrown fox\njumps over"}}
+{"Key":"c"}
+{"Key":"k"}
+{"Get":{"state":"The qˇuick\nbrown fox\njumps over","mode":"Normal"}}
+{"Put":{"state":"ˇ\nbrown fox\njumps over"}}
+{"Key":"c"}
+{"Key":"k"}
+{"Get":{"state":"ˇ\nbrown fox\njumps over","mode":"Normal"}}
@@ -1 +1,8 @@
-[{"Text":"Tet"},{"Mode":"Insert"},{"Selection":{"start":[0,2],"end":[0,2]}},{"Mode":"Insert"},{"Text":"Tes"},{"Mode":"Insert"},{"Selection":{"start":[0,3],"end":[0,3]}},{"Mode":"Insert"}]
+{"Put":{"state":"Teˇst"}}
+{"Key":"c"}
+{"Key":"l"}
+{"Get":{"state":"Teˇt","mode":"Insert"}}
+{"Put":{"state":"Tesˇt"}}
+{"Key":"c"}
+{"Key":"l"}
+{"Get":{"state":"Tesˇ","mode":"Insert"}}
@@ -1 +1,270 @@
@@ -1 +1,1020 @@
@@ -1 +1,28 @@
-[{"Text":"Te"},{"Mode":"Insert"},{"Selection":{"start":[0,2],"end":[0,2]}},{"Mode":"Insert"},{"Text":"T test"},{"Mode":"Insert"},{"Selection":{"start":[0,1],"end":[0,1]}},{"Mode":"Insert"},{"Text":"Testtest"},{"Mode":"Insert"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Insert"},{"Text":"Test te\ntest"},{"Mode":"Insert"},{"Selection":{"start":[0,7],"end":[0,7]}},{"Mode":"Insert"},{"Text":"Test tes\ntest"},{"Mode":"Insert"},{"Selection":{"start":[0,8],"end":[0,8]}},{"Mode":"Insert"},{"Text":"Test test\n\ntest"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"Test te test"},{"Mode":"Insert"},{"Selection":{"start":[0,7],"end":[0,7]}},{"Mode":"Insert"}]
+{"Put":{"state":"Teˇst"}}
+{"Key":"c"}
+{"Key":"w"}
+{"Get":{"state":"Teˇ","mode":"Insert"}}
+{"Put":{"state":"Tˇest test"}}
+{"Key":"c"}
+{"Key":"w"}
+{"Get":{"state":"Tˇ test","mode":"Insert"}}
+{"Put":{"state":"Testˇ test"}}
+{"Key":"c"}
+{"Key":"w"}
+{"Get":{"state":"Testˇtest","mode":"Insert"}}
+{"Put":{"state":"Test teˇst\ntest"}}
+{"Key":"c"}
+{"Key":"w"}
+{"Get":{"state":"Test teˇ\ntest","mode":"Insert"}}
+{"Put":{"state":"Test tesˇt\ntest"}}
+{"Key":"c"}
+{"Key":"w"}
+{"Get":{"state":"Test tesˇ\ntest","mode":"Insert"}}
+{"Put":{"state":"Test test\nˇ\ntest"}}
+{"Key":"c"}
+{"Key":"w"}
+{"Get":{"state":"Test test\nˇ\ntest","mode":"Insert"}}
+{"Put":{"state":"Test teˇst-test test"}}
+{"Key":"c"}
+{"Key":"shift-w"}
+{"Get":{"state":"Test teˇ test","mode":"Insert"}}
@@ -1 +1,460 @@
@@ -1 +1,24 @@
-[{"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"}]
+{"Put":{"state":"ˇ"}}
+{"Key":"d"}
+{"Key":"d"}
+{"Get":{"state":"ˇ","mode":"Normal"}}
+{"Put":{"state":"The ˇquick"}}
+{"Key":"d"}
+{"Key":"d"}
+{"Get":{"state":"ˇ","mode":"Normal"}}
+{"Put":{"state":"The qˇuick\nbrown fox\njumps over"}}
+{"Key":"d"}
+{"Key":"d"}
+{"Get":{"state":"brownˇ fox\njumps over","mode":"Normal"}}
+{"Put":{"state":"The quick\nbrown ˇfox\njumps over"}}
+{"Key":"d"}
+{"Key":"d"}
+{"Get":{"state":"The quick\njumps ˇover","mode":"Normal"}}
+{"Put":{"state":"The quick\nbrown fox\njumps ˇover"}}
+{"Key":"d"}
+{"Key":"d"}
+{"Get":{"state":"The quick\nbrown ˇfox","mode":"Normal"}}
+{"Put":{"state":"The quick\nˇ\nbrown fox"}}
+{"Key":"d"}
+{"Key":"d"}
+{"Get":{"state":"The quick\nˇbrown fox","mode":"Normal"}}
@@ -1 +1,8 @@
-[{"Text":"uick\nbrown fox"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"The quick\n\nbrown fox"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"}]
+{"Put":{"state":"The qˇuick\nbrown fox"}}
+{"Key":"d"}
+{"Key":"0"}
+{"Get":{"state":"ˇuick\nbrown fox","mode":"Normal"}}
+{"Put":{"state":"The quick\nˇ\nbrown fox"}}
+{"Key":"d"}
+{"Key":"0"}
+{"Get":{"state":"The quick\nˇ\nbrown fox","mode":"Normal"}}
@@ -1 +1,24 @@
-[{"Text":"st Test"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"test"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"Test1 test3"},{"Mode":"Normal"},{"Selection":{"start":[0,6],"end":[0,6]}},{"Mode":"Normal"},{"Text":"Test \ntest"},{"Mode":"Normal"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Normal"},{"Text":"Test \n\ntest"},{"Mode":"Normal"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Normal"},{"Text":"Test test"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"}]
+{"Put":{"state":"Teˇst Test"}}
+{"Key":"d"}
+{"Key":"b"}
+{"Get":{"state":"ˇst Test","mode":"Normal"}}
+{"Put":{"state":"Test ˇtest"}}
+{"Key":"d"}
+{"Key":"b"}
+{"Get":{"state":"ˇtest","mode":"Normal"}}
+{"Put":{"state":"Test1 test2 ˇtest3"}}
+{"Key":"d"}
+{"Key":"b"}
+{"Get":{"state":"Test1 ˇtest3","mode":"Normal"}}
+{"Put":{"state":"Test test\nˇtest"}}
+{"Key":"d"}
+{"Key":"b"}
+{"Get":{"state":"Testˇ \ntest","mode":"Normal"}}
+{"Put":{"state":"Test test\nˇ\ntest"}}
+{"Key":"d"}
+{"Key":"b"}
+{"Get":{"state":"Testˇ \n\ntest","mode":"Normal"}}
+{"Put":{"state":"Test test-test ˇtest"}}
+{"Key":"d"}
+{"Key":"shift-b"}
+{"Get":{"state":"Test ˇtest","mode":"Normal"}}
@@ -1 +1,20 @@
-[{"Text":"Te Test"},{"Mode":"Normal"},{"Selection":{"start":[0,2],"end":[0,2]}},{"Mode":"Normal"},{"Text":"T test"},{"Mode":"Normal"},{"Selection":{"start":[0,1],"end":[0,1]}},{"Mode":"Normal"},{"Text":"Test te\ntest"},{"Mode":"Normal"},{"Selection":{"start":[0,6],"end":[0,6]}},{"Mode":"Normal"},{"Text":"Test tes"},{"Mode":"Normal"},{"Selection":{"start":[0,7],"end":[0,7]}},{"Mode":"Normal"},{"Text":"Test te test"},{"Mode":"Normal"},{"Selection":{"start":[0,7],"end":[0,7]}},{"Mode":"Normal"}]
+{"Put":{"state":"Teˇst Test"}}
+{"Key":"d"}
+{"Key":"e"}
+{"Get":{"state":"Teˇ Test","mode":"Normal"}}
+{"Put":{"state":"Tˇest test"}}
+{"Key":"d"}
+{"Key":"e"}
+{"Get":{"state":"Tˇ test","mode":"Normal"}}
+{"Put":{"state":"Test teˇst\ntest"}}
+{"Key":"d"}
+{"Key":"e"}
+{"Get":{"state":"Test tˇe\ntest","mode":"Normal"}}
+{"Put":{"state":"Test tesˇt\ntest"}}
+{"Key":"d"}
+{"Key":"e"}
+{"Get":{"state":"Test teˇs","mode":"Normal"}}
+{"Put":{"state":"Test teˇst-test test"}}
+{"Key":"d"}
+{"Key":"shift-e"}
+{"Get":{"state":"Test teˇ test","mode":"Normal"}}
@@ -1 +1,16 @@
-[{"Text":"The quick"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":"The quick"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":"The quick\nbrown fox\njumps over"},{"Mode":"Normal"},{"Selection":{"start":[2,5],"end":[2,5]}},{"Mode":"Normal"},{"Text":"The quick\nbrown fox\njumps over"},{"Mode":"Normal"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Normal"}]
+{"Put":{"state":"The quick\nbrownˇ fox\njumps over\nthe lazy"}}
+{"Key":"d"}
+{"Key":"shift-g"}
+{"Get":{"state":"The qˇuick","mode":"Normal"}}
+{"Put":{"state":"The quick\nbrownˇ fox\njumps over\nthe lazy"}}
+{"Key":"d"}
+{"Key":"shift-g"}
+{"Get":{"state":"The qˇuick","mode":"Normal"}}
+{"Put":{"state":"The quick\nbrown fox\njumps over\nthe lˇazy"}}
+{"Key":"d"}
+{"Key":"shift-g"}
+{"Get":{"state":"The quick\nbrown fox\njumpsˇ over","mode":"Normal"}}
+{"Put":{"state":"The quick\nbrown fox\njumps over\nˇ"}}
+{"Key":"d"}
+{"Key":"shift-g"}
+{"Get":{"state":"The quick\nbrown fox\nˇjumps over","mode":"Normal"}}
@@ -1 +1,8 @@
-[{"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"}]
+{"Put":{"state":"The qˇuick\nbrown fox"}}
+{"Key":"d"}
+{"Key":"$"}
+{"Get":{"state":"The ˇq\nbrown fox","mode":"Normal"}}
+{"Put":{"state":"The quick\nˇ\nbrown fox"}}
+{"Key":"d"}
+{"Key":"$"}
+{"Get":{"state":"The quick\nˇ\nbrown fox","mode":"Normal"}}
@@ -1 +1,20 @@
-[{"Text":"jumps over\nthe lazy"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":""},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"brown fox\njumps over\nthe lazy"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":"brown fox\njumps over\nthe lazy"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"}]
+{"Put":{"state":"The quick\nbrownˇ fox\njumps over\nthe lazy"}}
+{"Key":"d"}
+{"Key":"g"}
+{"Key":"g"}
+{"Get":{"state":"jumpsˇ over\nthe lazy","mode":"Normal"}}
+{"Put":{"state":"The quick\nbrown fox\njumps over\nthe lˇazy"}}
+{"Key":"d"}
+{"Key":"g"}
+{"Key":"g"}
+{"Get":{"state":"ˇ","mode":"Normal"}}
+{"Put":{"state":"The qˇuick\nbrown fox\njumps over\nthe lazy"}}
+{"Key":"d"}
+{"Key":"g"}
+{"Key":"g"}
+{"Get":{"state":"brownˇ fox\njumps over\nthe lazy","mode":"Normal"}}
+{"Put":{"state":"ˇ\nbrown fox\njumps over\nthe lazy"}}
+{"Key":"d"}
+{"Key":"g"}
+{"Key":"g"}
+{"Get":{"state":"ˇbrown fox\njumps over\nthe lazy","mode":"Normal"}}
@@ -1 +1,16 @@
-[{"Text":"Tst"},{"Mode":"Normal"},{"Selection":{"start":[0,1],"end":[0,1]}},{"Mode":"Normal"},{"Text":"est"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"Test"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"Test\ntest"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"}]
+{"Put":{"state":"Teˇst"}}
+{"Key":"d"}
+{"Key":"h"}
+{"Get":{"state":"Tˇst","mode":"Normal"}}
+{"Put":{"state":"Tˇest"}}
+{"Key":"d"}
+{"Key":"h"}
+{"Get":{"state":"ˇest","mode":"Normal"}}
+{"Put":{"state":"ˇTest"}}
+{"Key":"d"}
+{"Key":"h"}
+{"Get":{"state":"ˇTest","mode":"Normal"}}
+{"Put":{"state":"Test\nˇtest"}}
+{"Key":"d"}
+{"Key":"h"}
+{"Get":{"state":"Test\nˇtest","mode":"Normal"}}
@@ -1 +1,16 @@
-[{"Text":"The quick"},{"Mode":"Normal"},{"Selection":{"start":[0,6],"end":[0,6]}},{"Mode":"Normal"},{"Text":"The quick\nbrown fox\njumps over"},{"Mode":"Normal"},{"Selection":{"start":[2,6],"end":[2,6]}},{"Mode":"Normal"},{"Text":"jumps over"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":"The quick\nbrown fox\n"},{"Mode":"Normal"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Normal"}]
+{"Put":{"state":"The quick\nbrown ˇfox\njumps over"}}
+{"Key":"d"}
+{"Key":"j"}
+{"Get":{"state":"The quˇick","mode":"Normal"}}
+{"Put":{"state":"The quick\nbrown fox\njumps ˇover"}}
+{"Key":"d"}
+{"Key":"j"}
+{"Get":{"state":"The quick\nbrown fox\njumps ˇover","mode":"Normal"}}
+{"Put":{"state":"The qˇuick\nbrown fox\njumps over"}}
+{"Key":"d"}
+{"Key":"j"}
+{"Get":{"state":"jumpsˇ over","mode":"Normal"}}
+{"Put":{"state":"The quick\nbrown fox\nˇ"}}
+{"Key":"d"}
+{"Key":"j"}
+{"Get":{"state":"The quick\nbrown fox\nˇ","mode":"Normal"}}
@@ -1 +1,16 @@
-[{"Text":"jumps over"},{"Mode":"Normal"},{"Selection":{"start":[0,6],"end":[0,6]}},{"Mode":"Normal"},{"Text":"The quick"},{"Mode":"Normal"},{"Selection":{"start":[0,6],"end":[0,6]}},{"Mode":"Normal"},{"Text":"The quick\nbrown fox\njumps over"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":"brown fox\njumps over"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"}]
+{"Put":{"state":"The quick\nbrown ˇfox\njumps over"}}
+{"Key":"d"}
+{"Key":"k"}
+{"Get":{"state":"jumps ˇover","mode":"Normal"}}
+{"Put":{"state":"The quick\nbrown fox\njumps ˇover"}}
+{"Key":"d"}
+{"Key":"k"}
+{"Get":{"state":"The quˇick","mode":"Normal"}}
+{"Put":{"state":"The qˇuick\nbrown fox\njumps over"}}
+{"Key":"d"}
+{"Key":"k"}
+{"Get":{"state":"The qˇuick\nbrown fox\njumps over","mode":"Normal"}}
+{"Put":{"state":"ˇbrown fox\njumps over"}}
+{"Key":"d"}
+{"Key":"k"}
+{"Get":{"state":"ˇbrown fox\njumps over","mode":"Normal"}}
@@ -1 +1,16 @@
-[{"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"}]
+{"Put":{"state":"ˇTest"}}
+{"Key":"d"}
+{"Key":"l"}
+{"Get":{"state":"ˇest","mode":"Normal"}}
+{"Put":{"state":"Teˇst"}}
+{"Key":"d"}
+{"Key":"l"}
+{"Get":{"state":"Teˇt","mode":"Normal"}}
+{"Put":{"state":"Tesˇt"}}
+{"Key":"d"}
+{"Key":"l"}
+{"Get":{"state":"Teˇs","mode":"Normal"}}
+{"Put":{"state":"Tesˇt\ntest"}}
+{"Key":"d"}
+{"Key":"l"}
+{"Get":{"state":"Teˇs\ntest","mode":"Normal"}}
@@ -1 +1,15 @@
-[{"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"}]
+{"Put":{"state":"ˇTest"}}
+{"Key":"shift-x"}
+{"Get":{"state":"ˇTest","mode":"Normal"}}
+{"Put":{"state":"Tˇest"}}
+{"Key":"shift-x"}
+{"Get":{"state":"ˇest","mode":"Normal"}}
+{"Put":{"state":"Teˇst"}}
+{"Key":"shift-x"}
+{"Get":{"state":"Tˇst","mode":"Normal"}}
+{"Put":{"state":"Tesˇt"}}
+{"Key":"shift-x"}
+{"Get":{"state":"Teˇt","mode":"Normal"}}
+{"Put":{"state":"Test\nˇtest"}}
+{"Key":"shift-x"}
+{"Get":{"state":"Test\nˇtest","mode":"Normal"}}
@@ -1 +1,270 @@
@@ -1 +1,1014 @@
@@ -1 +1,6 @@
-[{"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"}]
+{"Put":{"state":"The qˇuick\nbrown fox"}}
+{"Key":"shift-d"}
+{"Get":{"state":"The ˇq\nbrown fox","mode":"Normal"}}
+{"Put":{"state":"The quick\nˇ\nbrown fox"}}
+{"Key":"shift-d"}
+{"Get":{"state":"The quick\nˇ\nbrown fox","mode":"Normal"}}
@@ -1 +1,20 @@
-[{"Text":"Te"},{"Mode":"Normal"},{"Selection":{"start":[0,1],"end":[0,1]}},{"Mode":"Normal"},{"Text":"Ttest"},{"Mode":"Normal"},{"Selection":{"start":[0,1],"end":[0,1]}},{"Mode":"Normal"},{"Text":"Test te\ntest"},{"Mode":"Normal"},{"Selection":{"start":[0,6],"end":[0,6]}},{"Mode":"Normal"},{"Text":"Test tes\ntest"},{"Mode":"Normal"},{"Selection":{"start":[0,7],"end":[0,7]}},{"Mode":"Normal"},{"Text":"Test tetest"},{"Mode":"Normal"},{"Selection":{"start":[0,7],"end":[0,7]}},{"Mode":"Normal"}]
+{"Put":{"state":"Teˇst"}}
+{"Key":"d"}
+{"Key":"w"}
+{"Get":{"state":"Tˇe","mode":"Normal"}}
+{"Put":{"state":"Tˇest test"}}
+{"Key":"d"}
+{"Key":"w"}
+{"Get":{"state":"Tˇtest","mode":"Normal"}}
+{"Put":{"state":"Test teˇst\ntest"}}
+{"Key":"d"}
+{"Key":"w"}
+{"Get":{"state":"Test tˇe\ntest","mode":"Normal"}}
+{"Put":{"state":"Test tesˇt\ntest"}}
+{"Key":"d"}
+{"Key":"w"}
+{"Get":{"state":"Test teˇs\ntest","mode":"Normal"}}
+{"Put":{"state":"Test teˇst-test test"}}
+{"Key":"d"}
+{"Key":"shift-w"}
+{"Get":{"state":"Test teˇtest","mode":"Normal"}}
@@ -1 +1,460 @@
@@ -1 +1,32 @@
@@ -1 +1,15 @@
-[{"Text":"The quick\n\nbrown fox jumps\nover the lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[3,5],"end":[3,5]}},{"Mode":"Normal"},{"Text":"The quick\n\nbrown fox jumps\nover the lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[3,5],"end":[3,5]}},{"Mode":"Normal"},{"Text":"The quick\n\nbrown fox jumps\nover the lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[3,11],"end":[3,11]}},{"Mode":"Normal"},{"Text":"\n\nbrown fox jumps\nover the lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[3,11],"end":[3,11]}},{"Mode":"Normal"},{"Text":"\n\nbrown fox jumps\nover the lazydog"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"}]
+{"Put":{"state":"The qˇuick\n\nbrown fox jumps\nover the lazy dog"}}
+{"Key":"shift-g"}
+{"Get":{"state":"The quick\n\nbrown fox jumps\nover ˇthe lazy dog","mode":"Normal"}}
+{"Key":"shift-g"}
+{"Get":{"state":"The quick\n\nbrown fox jumps\nover ˇthe lazy dog","mode":"Normal"}}
+{"Put":{"state":"The quick\n\nbrown fox jumps\nover the laˇzy dog"}}
+{"Key":"shift-g"}
+{"Get":{"state":"The quick\n\nbrown fox jumps\nover the laˇzy dog","mode":"Normal"}}
+{"Put":{"state":"\n\nbrown fox jumps\nover the laˇzy dog"}}
+{"Key":"shift-g"}
+{"Get":{"state":"\n\nbrown fox jumps\nover the laˇzy dog","mode":"Normal"}}
+{"Put":{"state":"ˇ\n\nbrown fox jumps\nover the lazydog"}}
+{"Key":"2"}
+{"Key":"shift-g"}
+{"Get":{"state":"\nˇ\nbrown fox jumps\nover the lazydog","mode":"Normal"}}
@@ -1 +1,11 @@
-[{"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,0],"end":[1,0]}},{"Mode":"Normal"},{"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,0],"end":[1,0]}},{"Mode":"Normal"}]
+{"Put":{"state":"ˇThe quick brown\nfox jumps"}}
+{"Key":"enter"}
+{"Get":{"state":"The quick brown\nˇfox jumps","mode":"Normal"}}
+{"Put":{"state":"The qˇuick brown\nfox jumps"}}
+{"Key":"enter"}
+{"Get":{"state":"The quick brown\nˇfox jumps","mode":"Normal"}}
+{"Put":{"state":"The quick broˇwn\nfox jumps"}}
+{"Key":"enter"}
+{"Get":{"state":"The quick brown\nˇfox jumps","mode":"Normal"}}
+{"Key":"enter"}
+{"Get":{"state":"The quick brown\nˇfox jumps","mode":"Normal"}}
@@ -1 +1,30 @@
@@ -1 +1,557 @@
@@ -1 +1,21 @@
-[{"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"}]
+{"Put":{"state":"The qˇuick\n\nbrown fox jumps\nover the lazy dog"}}
+{"Key":"g"}
+{"Key":"g"}
+{"Get":{"state":"The qˇuick\n\nbrown fox jumps\nover the lazy dog","mode":"Normal"}}
+{"Put":{"state":"The quick\n\nbrown fox jumps\nover ˇthe lazy dog"}}
+{"Key":"g"}
+{"Key":"g"}
+{"Get":{"state":"The qˇuick\n\nbrown fox jumps\nover the lazy dog","mode":"Normal"}}
+{"Put":{"state":"The quick\n\nbrown fox jumps\nover the laˇzy dog"}}
+{"Key":"g"}
+{"Key":"g"}
+{"Get":{"state":"The quicˇk\n\nbrown fox jumps\nover the lazy dog","mode":"Normal"}}
+{"Put":{"state":"\n\nbrown fox jumps\nover the laˇzy dog"}}
+{"Key":"g"}
+{"Key":"g"}
+{"Get":{"state":"ˇ\n\nbrown fox jumps\nover the lazy dog","mode":"Normal"}}
+{"Put":{"state":"ˇ\n\nbrown fox jumps\nover the lazydog"}}
+{"Key":"2"}
+{"Key":"g"}
+{"Key":"g"}
+{"Get":{"state":"\nˇ\nbrown fox jumps\nover the lazydog","mode":"Normal"}}
@@ -1 +1,9 @@
-[{"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"}]
+{"Put":{"state":"ˇThe quick\nbrown"}}
+{"Key":"h"}
+{"Get":{"state":"ˇThe quick\nbrown","mode":"Normal"}}
+{"Put":{"state":"The qˇuick\nbrown"}}
+{"Key":"h"}
+{"Get":{"state":"The ˇquick\nbrown","mode":"Normal"}}
+{"Put":{"state":"The quick\nˇbrown"}}
+{"Key":"h"}
+{"Get":{"state":"The quick\nˇbrown","mode":"Normal"}}
@@ -1 +1,12 @@
-[{"Text":"Test├──┐Test"},{"Mode":"Normal"},{"Selection":{"start":[0,3],"end":[0,3]}},{"Mode":"Normal"},{"Text":"Test├──┐Test"},{"Mode":"Normal"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Normal"},{"Text":"Test├──┐Test"},{"Mode":"Normal"},{"Selection":{"start":[0,10],"end":[0,10]}},{"Mode":"Normal"},{"Text":"Test├──┐Test"},{"Mode":"Normal"},{"Selection":{"start":[0,13],"end":[0,13]}},{"Mode":"Normal"}]
+{"Put":{"state":"Testˇ├──┐Test"}}
+{"Key":"h"}
+{"Get":{"state":"Tesˇt├──┐Test","mode":"Normal"}}
+{"Put":{"state":"Test├ˇ──┐Test"}}
+{"Key":"h"}
+{"Get":{"state":"Testˇ├──┐Test","mode":"Normal"}}
+{"Put":{"state":"Test├──ˇ┐Test"}}
+{"Key":"h"}
+{"Get":{"state":"Test├─ˇ─┐Test","mode":"Normal"}}
+{"Put":{"state":"Test├──┐ˇTest"}}
+{"Key":"h"}
+{"Get":{"state":"Test├──ˇ┐Test","mode":"Normal"}}
@@ -1 +1,9 @@
-[{"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"}]
+{"Put":{"state":"ˇ\nThe quick\nbrown fox "}}
+{"Key":"shift-a"}
+{"Get":{"state":"ˇ\nThe quick\nbrown fox ","mode":"Insert"}}
+{"Put":{"state":"\nThe qˇuick\nbrown fox "}}
+{"Key":"shift-a"}
+{"Get":{"state":"\nThe quickˇ\nbrown fox ","mode":"Insert"}}
+{"Put":{"state":"\nThe quick\nbrown ˇfox "}}
+{"Key":"shift-a"}
+{"Get":{"state":"\nThe quick\nbrown fox ˇ","mode":"Insert"}}
@@ -1 +1,15 @@
-[{"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"}]
+{"Put":{"state":"The qˇuick"}}
+{"Key":"shift-i"}
+{"Get":{"state":"ˇThe quick","mode":"Insert"}}
+{"Put":{"state":" The qˇuick"}}
+{"Key":"shift-i"}
+{"Get":{"state":" ˇThe quick","mode":"Insert"}}
+{"Put":{"state":"ˇ"}}
+{"Key":"shift-i"}
+{"Get":{"state":"ˇ","mode":"Insert"}}
+{"Put":{"state":"The qˇuick\nbrown fox"}}
+{"Key":"shift-i"}
+{"Get":{"state":"ˇThe quick\nbrown fox","mode":"Insert"}}
+{"Put":{"state":"ˇ\nThe quick"}}
+{"Key":"shift-i"}
+{"Get":{"state":"ˇ\nThe quick","mode":"Insert"}}
@@ -1 +1,18 @@
-[{"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"}]
+{"Put":{"state":"ˇ"}}
+{"Key":"shift-o"}
+{"Get":{"state":"ˇ\n","mode":"Insert"}}
+{"Put":{"state":"The ˇquick"}}
+{"Key":"shift-o"}
+{"Get":{"state":"ˇ\nThe quick","mode":"Insert"}}
+{"Put":{"state":"The qˇuick\nbrown fox\njumps over"}}
+{"Key":"shift-o"}
+{"Get":{"state":"ˇ\nThe quick\nbrown fox\njumps over","mode":"Insert"}}
+{"Put":{"state":"The quick\nbrown ˇfox\njumps over"}}
+{"Key":"shift-o"}
+{"Get":{"state":"The quick\nˇ\nbrown fox\njumps over","mode":"Insert"}}
+{"Put":{"state":"The quick\nbrown fox\njumps ˇover"}}
+{"Key":"shift-o"}
+{"Get":{"state":"The quick\nbrown fox\nˇ\njumps over","mode":"Insert"}}
+{"Put":{"state":"The quick\nˇ\nbrown fox"}}
+{"Key":"shift-o"}
+{"Get":{"state":"The quick\nˇ\n\nbrown fox","mode":"Insert"}}
@@ -1 +1,12 @@
-[{"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"}]
+{"Put":{"state":"ˇThe quick brown\nfox jumps"}}
+{"Key":"j"}
+{"Get":{"state":"The quick brown\nˇfox jumps","mode":"Normal"}}
+{"Put":{"state":"The qˇuick brown\nfox jumps"}}
+{"Key":"j"}
+{"Get":{"state":"The quick brown\nfox jˇumps","mode":"Normal"}}
+{"Put":{"state":"The quick broˇwn\nfox jumps"}}
+{"Key":"j"}
+{"Get":{"state":"The quick brown\nfox jumpˇs","mode":"Normal"}}
+{"Put":{"state":"The quick brown\nˇfox jumps"}}
+{"Key":"j"}
+{"Get":{"state":"The quick brown\nˇfox jumps","mode":"Normal"}}
@@ -1 +1,14 @@
-[{"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"}]
+{"Put":{"state":"The ˇquick\n\nbrown fox jumps\nover the lazy dog"}}
+{"Key":"shift-g"}
+{"Get":{"state":"The quick\n\nbrown fox jumps\noverˇ the lazy dog","mode":"Normal"}}
+{"Key":"shift-g"}
+{"Get":{"state":"The quick\n\nbrown fox jumps\noverˇ the lazy dog","mode":"Normal"}}
+{"Put":{"state":"The quick\n\nbrown fox jumps\nover the lazy doˇg"}}
+{"Key":"shift-g"}
+{"Get":{"state":"The quick\n\nbrown fox jumps\nover the lazy doˇg","mode":"Normal"}}
+{"Put":{"state":"The quiˇck\n\nbrown"}}
+{"Key":"shift-g"}
+{"Get":{"state":"The quick\n\nbrowˇn","mode":"Normal"}}
+{"Put":{"state":"The quiˇck\n\n"}}
+{"Key":"shift-g"}
+{"Get":{"state":"The quick\n\nˇ","mode":"Normal"}}
@@ -1 +1,18 @@
-[{"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"}]
+{"Put":{"state":"The qˇuick"}}
+{"Key":"^"}
+{"Get":{"state":"ˇThe quick","mode":"Normal"}}
+{"Put":{"state":" The qˇuick"}}
+{"Key":"^"}
+{"Get":{"state":" ˇThe quick","mode":"Normal"}}
+{"Put":{"state":"ˇ"}}
+{"Key":"^"}
+{"Get":{"state":"ˇ","mode":"Normal"}}
+{"Put":{"state":"The qˇuick\nbrown fox"}}
+{"Key":"^"}
+{"Get":{"state":"ˇThe quick\nbrown fox","mode":"Normal"}}
+{"Put":{"state":"ˇ\nThe quick"}}
+{"Key":"^"}
+{"Get":{"state":"ˇ\nThe quick","mode":"Normal"}}
+{"Put":{"state":" ˇ \nThe quick"}}
+{"Key":"^"}
+{"Get":{"state":" ˇ \nThe quick","mode":"Normal"}}
@@ -1 +1,28 @@
@@ -1 +1,15 @@
-[{"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"}]
+{"Put":{"state":"ˇThe quick\nbrown fox jumps"}}
+{"Key":"k"}
+{"Get":{"state":"ˇThe quick\nbrown fox jumps","mode":"Normal"}}
+{"Put":{"state":"The qˇuick\nbrown fox jumps"}}
+{"Key":"k"}
+{"Get":{"state":"The qˇuick\nbrown fox jumps","mode":"Normal"}}
+{"Put":{"state":"The quick\nˇbrown fox jumps"}}
+{"Key":"k"}
+{"Get":{"state":"ˇThe quick\nbrown fox jumps","mode":"Normal"}}
+{"Put":{"state":"The quick\nbrown fˇox jumps"}}
+{"Key":"k"}
+{"Get":{"state":"The quiˇck\nbrown fox jumps","mode":"Normal"}}
+{"Put":{"state":"The quick\nbrown fox jumˇps"}}
+{"Key":"k"}
+{"Get":{"state":"The quicˇk\nbrown fox jumps","mode":"Normal"}}
@@ -1 +1,15 @@
-[{"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"}]
+{"Put":{"state":"ˇThe quick\nbrown"}}
+{"Key":"l"}
+{"Get":{"state":"Tˇhe quick\nbrown","mode":"Normal"}}
+{"Put":{"state":"The qˇuick\nbrown"}}
+{"Key":"l"}
+{"Get":{"state":"The quˇick\nbrown","mode":"Normal"}}
+{"Put":{"state":"The quicˇk\nbrown"}}
+{"Key":"l"}
+{"Get":{"state":"The quicˇk\nbrown","mode":"Normal"}}
+{"Put":{"state":"The quick\nˇbrown"}}
+{"Key":"l"}
+{"Get":{"state":"The quick\nbˇrown","mode":"Normal"}}
+{"Put":{"state":"The quick\nbrowˇn"}}
+{"Key":"l"}
+{"Get":{"state":"The quick\nbrowˇn","mode":"Normal"}}
@@ -1 +1,16 @@
-[{"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"}]
+{"Key":"i"}
+{"Get":{"state":"ˇ","mode":"Insert"}}
+{"Key":"shift-T"}
+{"Key":"e"}
+{"Key":"s"}
+{"Key":"t"}
+{"Key":" "}
+{"Key":"t"}
+{"Key":"e"}
+{"Key":"s"}
+{"Key":"t"}
+{"Key":"escape"}
+{"Key":"0"}
+{"Key":"d"}
+{"Key":"w"}
+{"Get":{"state":"ˇtest","mode":"Normal"}}
@@ -1 +1,18 @@
-[{"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"}]
+{"Put":{"state":"ˇ"}}
+{"Key":"o"}
+{"Get":{"state":"\nˇ","mode":"Insert"}}
+{"Put":{"state":"The ˇquick"}}
+{"Key":"o"}
+{"Get":{"state":"The quick\nˇ","mode":"Insert"}}
+{"Put":{"state":"The qˇuick\nbrown fox\njumps over"}}
+{"Key":"o"}
+{"Get":{"state":"The quick\nˇ\nbrown fox\njumps over","mode":"Insert"}}
+{"Put":{"state":"The quick\nbrown ˇfox\njumps over"}}
+{"Key":"o"}
+{"Get":{"state":"The quick\nbrown fox\nˇ\njumps over","mode":"Insert"}}
+{"Put":{"state":"The quick\nbrown fox\njumps ˇover"}}
+{"Key":"o"}
+{"Get":{"state":"The quick\nbrown fox\njumps over\nˇ","mode":"Insert"}}
+{"Put":{"state":"The quick\nˇ\nbrown fox"}}
+{"Key":"o"}
+{"Get":{"state":"The quick\n\nˇ\nbrown fox","mode":"Insert"}}
@@ -1 +1,13 @@
-[{"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"}]
+{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog"}}
+{"Key":"d"}
+{"Key":"d"}
+{"Get":{"state":"The quick brown\nthe laˇzy dog","mode":"Normal"}}
+{"Key":"p"}
+{"Get":{"state":"The quick brown\nthe lazy dog\nˇfox jumps over","mode":"Normal"}}
+{"Put":{"state":"The quick brown\nfox ˇjumps over\nthe lazy dog"}}
+{"Key":"v"}
+{"Key":"w"}
+{"Key":"y"}
+{"Put":{"state":"The quick brown\nfox jumps oveˇr\nthe lazy dog"}}
+{"Key":"p"}
+{"Get":{"state":"The quick brown\nfox jumps overjumps ˇo\nthe lazy dog","mode":"Normal"}}
@@ -1 +1,58 @@
@@ -1 +1,275 @@
@@ -1 +1,275 @@
@@ -1 +1,275 @@
@@ -1 +1,275 @@
@@ -1 +1,214 @@
@@ -1 +1,41 @@
-[{"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"}]
+{"Put":{"state":"The quick ˇbrown"}}
+{"Key":"v"}
+{"Key":"w"}
+{"Key":"c"}
+{"Get":{"state":"The quick ˇ","mode":"Insert"}}
+{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}}
+{"Key":"v"}
+{"Key":"w"}
+{"Key":"j"}
+{"Key":"c"}
+{"Get":{"state":"The ˇver\nthe lazy dog","mode":"Insert"}}
+{"Put":{"state":"The quick brown\nfox jumps ˇover\nthe lazy dog"}}
+{"Key":"v"}
+{"Key":"w"}
+{"Key":"j"}
+{"Key":"c"}
+{"Get":{"state":"The quick brown\nfox jumps ˇhe lazy dog","mode":"Insert"}}
+{"Put":{"state":"The quick brown\nfox jumps over\nthe ˇlazy dog"}}
+{"Key":"v"}
+{"Key":"w"}
+{"Key":"j"}
+{"Key":"c"}
+{"Get":{"state":"The quick brown\nfox jumps over\nthe ˇog","mode":"Insert"}}
+{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}}
+{"Key":"v"}
+{"Key":"b"}
+{"Key":"k"}
+{"Key":"c"}
+{"Get":{"state":"ˇuick brown\nfox jumps over\nthe lazy dog","mode":"Insert"}}
+{"Put":{"state":"The quick brown\nfox jumps ˇover\nthe lazy dog"}}
+{"Key":"v"}
+{"Key":"b"}
+{"Key":"k"}
+{"Key":"c"}
+{"Get":{"state":"The ˇver\nthe lazy dog","mode":"Insert"}}
+{"Put":{"state":"The quick brown\nfox jumps over\nthe ˇlazy dog"}}
+{"Key":"v"}
+{"Key":"b"}
+{"Key":"k"}
+{"Key":"c"}
+{"Get":{"state":"The quick brown\nˇazy dog","mode":"Insert"}}
@@ -1 +1,44 @@
-[{"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"}]
+{"Put":{"state":"The quick ˇbrown"}}
+{"Key":"v"}
+{"Key":"w"}
+{"Key":"x"}
+{"Get":{"state":"The quickˇ ","mode":"Normal"}}
+{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}}
+{"Key":"v"}
+{"Key":"w"}
+{"Key":"j"}
+{"Key":"x"}
+{"Get":{"state":"The ˇver\nthe lazy dog","mode":"Normal"}}
+{"Key":"j"}
+{"Key":"p"}
+{"Get":{"state":"The ver\nthe lˇquick brown\nfox jumps oazy dog","mode":"Normal"}}
+{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}}
+{"Key":"v"}
+{"Key":"w"}
+{"Key":"j"}
+{"Key":"x"}
+{"Get":{"state":"The ˇver\nthe lazy dog","mode":"Normal"}}
+{"Put":{"state":"The quick brown\nfox jumps over\nthe ˇlazy dog"}}
+{"Key":"v"}
+{"Key":"w"}
+{"Key":"j"}
+{"Key":"x"}
+{"Get":{"state":"The quick brown\nfox jumps over\nthe ˇog","mode":"Normal"}}
+{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}}
+{"Key":"v"}
+{"Key":"b"}
+{"Key":"k"}
+{"Key":"x"}
+{"Get":{"state":"ˇuick brown\nfox jumps over\nthe lazy dog","mode":"Normal"}}
+{"Put":{"state":"The quick brown\nfox jumps ˇover\nthe lazy dog"}}
+{"Key":"v"}
+{"Key":"b"}
+{"Key":"k"}
+{"Key":"x"}
+{"Get":{"state":"The ˇver\nthe lazy dog","mode":"Normal"}}
+{"Put":{"state":"The quick brown\nfox jumps over\nthe ˇlazy dog"}}
+{"Key":"v"}
+{"Key":"b"}
+{"Key":"k"}
+{"Key":"x"}
+{"Get":{"state":"The quick brown\nˇazy dog","mode":"Normal"}}
@@ -1 +1,35 @@
-[{"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"}]
+{"Put":{"state":"The quˇick brown\nfox jumps over\nthe lazy dog"}}
+{"Key":"shift-v"}
+{"Key":"c"}
+{"Get":{"state":"ˇ\nfox jumps over\nthe lazy dog","mode":"Insert"}}
+{"Key":"escape"}
+{"Key":"j"}
+{"Key":"p"}
+{"Get":{"state":"\nfox jumps over\nˇThe quick brown\nthe lazy dog","mode":"Normal"}}
+{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog"}}
+{"Key":"shift-v"}
+{"Key":"c"}
+{"Get":{"state":"The quick brown\nˇ\nthe lazy dog","mode":"Insert"}}
+{"Put":{"state":"The quick brown\nfox jumps over\nthe laˇzy dog"}}
+{"Key":"shift-v"}
+{"Key":"c"}
+{"Get":{"state":"The quick brown\nfox jumps over\nˇ","mode":"Insert"}}
+{"Put":{"state":"The quˇick brown\nfox jumps over\nthe lazy dog"}}
+{"Key":"shift-v"}
+{"Key":"j"}
+{"Key":"c"}
+{"Get":{"state":"ˇ\nthe lazy dog","mode":"Insert"}}
+{"Key":"escape"}
+{"Key":"j"}
+{"Key":"p"}
+{"Get":{"state":"\nthe lazy dog\nˇThe quick brown\nfox jumps over","mode":"Normal"}}
+{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog"}}
+{"Key":"shift-v"}
+{"Key":"j"}
+{"Key":"c"}
+{"Get":{"state":"The quick brown\nˇ","mode":"Insert"}}
+{"Put":{"state":"The quick brown\nfox jumps over\nthe laˇzy dog"}}
+{"Key":"shift-v"}
+{"Key":"j"}
+{"Key":"c"}
+{"Get":{"state":"The quick brown\nfox jumps over\nˇ","mode":"Insert"}}
@@ -1 +1,31 @@
-[{"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"}]
+{"Put":{"state":"The quˇick brown\nfox jumps over\nthe lazy dog"}}
+{"Key":"shift-v"}
+{"Key":"x"}
+{"Get":{"state":"fox juˇmps over\nthe lazy dog","mode":"Normal"}}
+{"Key":"p"}
+{"Get":{"state":"fox jumps over\nˇThe quick brown\nthe lazy dog","mode":"Normal"}}
+{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog"}}
+{"Key":"shift-v"}
+{"Key":"x"}
+{"Get":{"state":"The quick brown\nthe laˇzy dog","mode":"Normal"}}
+{"Put":{"state":"The quick brown\nfox jumps over\nthe laˇzy dog"}}
+{"Key":"shift-v"}
+{"Key":"x"}
+{"Get":{"state":"The quick brown\nfox juˇmps over","mode":"Normal"}}
+{"Put":{"state":"The quˇick brown\nfox jumps over\nthe lazy dog"}}
+{"Key":"shift-v"}
+{"Key":"j"}
+{"Key":"x"}
+{"Get":{"state":"the laˇzy dog","mode":"Normal"}}
+{"Key":"p"}
+{"Get":{"state":"the lazy dog\nˇThe quick brown\nfox jumps over","mode":"Normal"}}
+{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog"}}
+{"Key":"shift-v"}
+{"Key":"j"}
+{"Key":"x"}
+{"Get":{"state":"The quˇick brown","mode":"Normal"}}
+{"Put":{"state":"The quick brown\nfox jumps over\nthe laˇzy dog"}}
+{"Key":"shift-v"}
+{"Key":"j"}
+{"Key":"x"}
+{"Get":{"state":"The quick brown\nfox juˇmps over","mode":"Normal"}}
@@ -1 +0,0 @@
-[]
@@ -1 +1,230 @@
@@ -1 +1,40 @@
@@ -1 +1,12 @@
-[{"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"}]
+{"Put":{"state":"ˇTest"}}
+{"Key":"x"}
+{"Get":{"state":"ˇest","mode":"Normal"}}
+{"Put":{"state":"Teˇst"}}
+{"Key":"x"}
+{"Get":{"state":"Teˇt","mode":"Normal"}}
+{"Put":{"state":"Tesˇt"}}
+{"Key":"x"}
+{"Get":{"state":"Teˇs","mode":"Normal"}}
+{"Put":{"state":"Tesˇt\ntest"}}
+{"Key":"x"}
+{"Get":{"state":"Teˇs\ntest","mode":"Normal"}}
@@ -43,7 +43,7 @@ lazy_static = "1.4"
env_logger = "0.9.1"
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
parking_lot = "0.11.1"
-postage = { version = "0.4.1", features = ["futures-traits"] }
+postage = { workspace = true }
serde = { version = "1.0", features = ["derive", "rc"] }
serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
serde_json = { version = "1.0", features = ["preserve_order"] }
@@ -82,7 +82,7 @@ libc = "0.2"
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
num_cpus = "1.13.0"
parking_lot = "0.11.1"
-postage = { version = "0.4.1", features = ["futures-traits"] }
+postage = { workspace = true }
rand = "0.8.3"
regex = "1.5"
rsa = "0.4"