diff --git a/Cargo.lock b/Cargo.lock index e817fed0db..c7b2d51b5d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1575,6 +1575,7 @@ dependencies = [ "serde", "serde_derive", "settings", + "smallvec", "theme", "theme_selector", "time", @@ -8590,8 +8591,8 @@ dependencies = [ [[package]] name = "tree-sitter-bash" -version = "0.19.0" -source = "git+https://github.com/tree-sitter/tree-sitter-bash?rev=1b0321ee85701d5036c334a6f04761cdc672e64c#1b0321ee85701d5036c334a6f04761cdc672e64c" +version = "0.20.4" +source = "git+https://github.com/tree-sitter/tree-sitter-bash?rev=7331995b19b8f8aba2d5e26deb51d2195c18bc94#7331995b19b8f8aba2d5e26deb51d2195c18bc94" dependencies = [ "cc", "tree-sitter", diff --git a/Cargo.toml b/Cargo.toml index cf977b8fe6..836a0bd6b2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -125,7 +125,7 @@ pretty_assertions = "1.3.0" git2 = { version = "0.15", default-features = false} uuid = { version = "1.1.2", features = ["v4"] } -tree-sitter-bash = { git = "https://github.com/tree-sitter/tree-sitter-bash", rev = "1b0321ee85701d5036c334a6f04761cdc672e64c" } +tree-sitter-bash = { git = "https://github.com/tree-sitter/tree-sitter-bash", rev = "7331995b19b8f8aba2d5e26deb51d2195c18bc94" } tree-sitter-c = "0.20.1" tree-sitter-cpp = { git = "https://github.com/tree-sitter/tree-sitter-cpp", rev="f44509141e7e483323d2ec178f2d2e6c0fc041c1" } tree-sitter-css = { git = "https://github.com/tree-sitter/tree-sitter-css", rev = "769203d0f9abe1a9a691ac2b9fe4bb4397a73c51" } diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 8422d53abc..ef6a655bdc 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -370,42 +370,15 @@ { "context": "Pane", "bindings": { - "ctrl-1": [ - "pane::ActivateItem", - 0 - ], - "ctrl-2": [ - "pane::ActivateItem", - 1 - ], - "ctrl-3": [ - "pane::ActivateItem", - 2 - ], - "ctrl-4": [ - "pane::ActivateItem", - 3 - ], - "ctrl-5": [ - "pane::ActivateItem", - 4 - ], - "ctrl-6": [ - "pane::ActivateItem", - 5 - ], - "ctrl-7": [ - "pane::ActivateItem", - 6 - ], - "ctrl-8": [ - "pane::ActivateItem", - 7 - ], - "ctrl-9": [ - "pane::ActivateItem", - 8 - ], + "ctrl-1": ["pane::ActivateItem", 0], + "ctrl-2": ["pane::ActivateItem", 1], + "ctrl-3": ["pane::ActivateItem", 2], + "ctrl-4": ["pane::ActivateItem", 3], + "ctrl-5": ["pane::ActivateItem", 4], + "ctrl-6": ["pane::ActivateItem", 5], + "ctrl-7": ["pane::ActivateItem", 6], + "ctrl-8": ["pane::ActivateItem", 7], + "ctrl-9": ["pane::ActivateItem", 8], "ctrl-0": "pane::ActivateLastItem", "ctrl--": "pane::GoBack", "ctrl-_": "pane::GoForward", @@ -416,42 +389,15 @@ { "context": "Workspace", "bindings": { - "cmd-1": [ - "workspace::ActivatePane", - 0 - ], - "cmd-2": [ - "workspace::ActivatePane", - 1 - ], - "cmd-3": [ - "workspace::ActivatePane", - 2 - ], - "cmd-4": [ - "workspace::ActivatePane", - 3 - ], - "cmd-5": [ - "workspace::ActivatePane", - 4 - ], - "cmd-6": [ - "workspace::ActivatePane", - 5 - ], - "cmd-7": [ - "workspace::ActivatePane", - 6 - ], - "cmd-8": [ - "workspace::ActivatePane", - 7 - ], - "cmd-9": [ - "workspace::ActivatePane", - 8 - ], + "cmd-1": ["workspace::ActivatePane", 0], + "cmd-2": ["workspace::ActivatePane", 1], + "cmd-3": ["workspace::ActivatePane", 2], + "cmd-4": ["workspace::ActivatePane", 3], + "cmd-5": ["workspace::ActivatePane", 4], + "cmd-6": ["workspace::ActivatePane", 5], + "cmd-7": ["workspace::ActivatePane", 6], + "cmd-8": ["workspace::ActivatePane", 7], + "cmd-9": ["workspace::ActivatePane", 8], "cmd-b": "workspace::ToggleLeftDock", "cmd-r": "workspace::ToggleRightDock", "cmd-j": "workspace::ToggleBottomDock", @@ -494,38 +440,14 @@ }, { "bindings": { - "cmd-k cmd-left": [ - "workspace::ActivatePaneInDirection", - "Left" - ], - "cmd-k cmd-right": [ - "workspace::ActivatePaneInDirection", - "Right" - ], - "cmd-k cmd-up": [ - "workspace::ActivatePaneInDirection", - "Up" - ], - "cmd-k cmd-down": [ - "workspace::ActivatePaneInDirection", - "Down" - ], - "cmd-k shift-left": [ - "workspace::SwapPaneInDirection", - "Left" - ], - "cmd-k shift-right": [ - "workspace::SwapPaneInDirection", - "Right" - ], - "cmd-k shift-up": [ - "workspace::SwapPaneInDirection", - "Up" - ], - "cmd-k shift-down": [ - "workspace::SwapPaneInDirection", - "Down" - ] + "cmd-k cmd-left": ["workspace::ActivatePaneInDirection", "Left"], + "cmd-k cmd-right": ["workspace::ActivatePaneInDirection", "Right"], + "cmd-k cmd-up": ["workspace::ActivatePaneInDirection", "Up"], + "cmd-k cmd-down": ["workspace::ActivatePaneInDirection", "Down"], + "cmd-k shift-left": ["workspace::SwapPaneInDirection", "Left"], + "cmd-k shift-right": ["workspace::SwapPaneInDirection", "Right"], + "cmd-k shift-up": ["workspace::SwapPaneInDirection", "Up"], + "cmd-k shift-down": ["workspace::SwapPaneInDirection", "Down"] } }, // Bindings from Atom @@ -627,14 +549,6 @@ "space": "collab_panel::InsertSpace" } }, - { - "context": "(CollabPanel && not_editing) > Editor", - "bindings": { - "cmd-c": "collab_panel::StartLinkChannel", - "cmd-x": "collab_panel::StartMoveChannel", - "cmd-v": "collab_panel::MoveOrLinkToSelected" - } - }, { "context": "ChannelModal", "bindings": { @@ -655,57 +569,21 @@ "cmd-v": "terminal::Paste", "cmd-k": "terminal::Clear", // Some nice conveniences - "cmd-backspace": [ - "terminal::SendText", - "\u0015" - ], - "cmd-right": [ - "terminal::SendText", - "\u0005" - ], - "cmd-left": [ - "terminal::SendText", - "\u0001" - ], + "cmd-backspace": ["terminal::SendText", "\u0015"], + "cmd-right": ["terminal::SendText", "\u0005"], + "cmd-left": ["terminal::SendText", "\u0001"], // Terminal.app compatibility - "alt-left": [ - "terminal::SendText", - "\u001bb" - ], - "alt-right": [ - "terminal::SendText", - "\u001bf" - ], + "alt-left": ["terminal::SendText", "\u001bb"], + "alt-right": ["terminal::SendText", "\u001bf"], // There are conflicting bindings for these keys in the global context. // these bindings override them, remove at your own risk: - "up": [ - "terminal::SendKeystroke", - "up" - ], - "pageup": [ - "terminal::SendKeystroke", - "pageup" - ], - "down": [ - "terminal::SendKeystroke", - "down" - ], - "pagedown": [ - "terminal::SendKeystroke", - "pagedown" - ], - "escape": [ - "terminal::SendKeystroke", - "escape" - ], - "enter": [ - "terminal::SendKeystroke", - "enter" - ], - "ctrl-c": [ - "terminal::SendKeystroke", - "ctrl-c" - ] + "up": ["terminal::SendKeystroke", "up"], + "pageup": ["terminal::SendKeystroke", "pageup"], + "down": ["terminal::SendKeystroke", "down"], + "pagedown": ["terminal::SendKeystroke", "pagedown"], + "escape": ["terminal::SendKeystroke", "escape"], + "enter": ["terminal::SendKeystroke", "enter"], + "ctrl-c": ["terminal::SendKeystroke", "ctrl-c"] } } ] diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index ea025747d8..81235bb72a 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -39,6 +39,7 @@ "w": "vim::NextWordStart", "{": "vim::StartOfParagraph", "}": "vim::EndOfParagraph", + "|": "vim::GoToColumn", "shift-w": [ "vim::NextWordStart", { @@ -97,14 +98,8 @@ "ctrl-o": "pane::GoBack", "ctrl-i": "pane::GoForward", "ctrl-]": "editor::GoToDefinition", - "escape": [ - "vim::SwitchMode", - "Normal" - ], - "ctrl+[": [ - "vim::SwitchMode", - "Normal" - ], + "escape": ["vim::SwitchMode", "Normal"], + "ctrl+[": ["vim::SwitchMode", "Normal"], "v": "vim::ToggleVisual", "shift-v": "vim::ToggleVisualLine", "ctrl-v": "vim::ToggleVisualBlock", @@ -233,123 +228,36 @@ } ], // Count support - "1": [ - "vim::Number", - 1 - ], - "2": [ - "vim::Number", - 2 - ], - "3": [ - "vim::Number", - 3 - ], - "4": [ - "vim::Number", - 4 - ], - "5": [ - "vim::Number", - 5 - ], - "6": [ - "vim::Number", - 6 - ], - "7": [ - "vim::Number", - 7 - ], - "8": [ - "vim::Number", - 8 - ], - "9": [ - "vim::Number", - 9 - ], + "1": ["vim::Number", 1], + "2": ["vim::Number", 2], + "3": ["vim::Number", 3], + "4": ["vim::Number", 4], + "5": ["vim::Number", 5], + "6": ["vim::Number", 6], + "7": ["vim::Number", 7], + "8": ["vim::Number", 8], + "9": ["vim::Number", 9], // window related commands (ctrl-w X) - "ctrl-w left": [ - "workspace::ActivatePaneInDirection", - "Left" - ], - "ctrl-w right": [ - "workspace::ActivatePaneInDirection", - "Right" - ], - "ctrl-w up": [ - "workspace::ActivatePaneInDirection", - "Up" - ], - "ctrl-w down": [ - "workspace::ActivatePaneInDirection", - "Down" - ], - "ctrl-w h": [ - "workspace::ActivatePaneInDirection", - "Left" - ], - "ctrl-w l": [ - "workspace::ActivatePaneInDirection", - "Right" - ], - "ctrl-w k": [ - "workspace::ActivatePaneInDirection", - "Up" - ], - "ctrl-w j": [ - "workspace::ActivatePaneInDirection", - "Down" - ], - "ctrl-w ctrl-h": [ - "workspace::ActivatePaneInDirection", - "Left" - ], - "ctrl-w ctrl-l": [ - "workspace::ActivatePaneInDirection", - "Right" - ], - "ctrl-w ctrl-k": [ - "workspace::ActivatePaneInDirection", - "Up" - ], - "ctrl-w ctrl-j": [ - "workspace::ActivatePaneInDirection", - "Down" - ], - "ctrl-w shift-left": [ - "workspace::SwapPaneInDirection", - "Left" - ], - "ctrl-w shift-right": [ - "workspace::SwapPaneInDirection", - "Right" - ], - "ctrl-w shift-up": [ - "workspace::SwapPaneInDirection", - "Up" - ], - "ctrl-w shift-down": [ - "workspace::SwapPaneInDirection", - "Down" - ], - "ctrl-w shift-h": [ - "workspace::SwapPaneInDirection", - "Left" - ], - "ctrl-w shift-l": [ - "workspace::SwapPaneInDirection", - "Right" - ], - "ctrl-w shift-k": [ - "workspace::SwapPaneInDirection", - "Up" - ], - "ctrl-w shift-j": [ - "workspace::SwapPaneInDirection", - "Down" - ], + "ctrl-w left": ["workspace::ActivatePaneInDirection", "Left"], + "ctrl-w right": ["workspace::ActivatePaneInDirection", "Right"], + "ctrl-w up": ["workspace::ActivatePaneInDirection", "Up"], + "ctrl-w down": ["workspace::ActivatePaneInDirection", "Down"], + "ctrl-w h": ["workspace::ActivatePaneInDirection", "Left"], + "ctrl-w l": ["workspace::ActivatePaneInDirection", "Right"], + "ctrl-w k": ["workspace::ActivatePaneInDirection", "Up"], + "ctrl-w j": ["workspace::ActivatePaneInDirection", "Down"], + "ctrl-w ctrl-h": ["workspace::ActivatePaneInDirection", "Left"], + "ctrl-w ctrl-l": ["workspace::ActivatePaneInDirection", "Right"], + "ctrl-w ctrl-k": ["workspace::ActivatePaneInDirection", "Up"], + "ctrl-w ctrl-j": ["workspace::ActivatePaneInDirection", "Down"], + "ctrl-w shift-left": ["workspace::SwapPaneInDirection", "Left"], + "ctrl-w shift-right": ["workspace::SwapPaneInDirection", "Right"], + "ctrl-w shift-up": ["workspace::SwapPaneInDirection", "Up"], + "ctrl-w shift-down": ["workspace::SwapPaneInDirection", "Down"], + "ctrl-w shift-h": ["workspace::SwapPaneInDirection", "Left"], + "ctrl-w shift-l": ["workspace::SwapPaneInDirection", "Right"], + "ctrl-w shift-k": ["workspace::SwapPaneInDirection", "Up"], + "ctrl-w shift-j": ["workspace::SwapPaneInDirection", "Down"], "ctrl-w g t": "pane::ActivateNextItem", "ctrl-w ctrl-g t": "pane::ActivateNextItem", "ctrl-w g shift-t": "pane::ActivatePrevItem", @@ -371,14 +279,8 @@ "ctrl-w ctrl-q": "pane::CloseAllItems", "ctrl-w o": "workspace::CloseInactiveTabsAndPanes", "ctrl-w ctrl-o": "workspace::CloseInactiveTabsAndPanes", - "ctrl-w n": [ - "workspace::NewFileInDirection", - "Up" - ], - "ctrl-w ctrl-n": [ - "workspace::NewFileInDirection", - "Up" - ] + "ctrl-w n": ["workspace::NewFileInDirection", "Up"], + "ctrl-w ctrl-n": ["workspace::NewFileInDirection", "Up"] } }, { @@ -393,21 +295,12 @@ "context": "Editor && vim_mode == normal && vim_operator == none && !VimWaiting", "bindings": { ".": "vim::Repeat", - "c": [ - "vim::PushOperator", - "Change" - ], + "c": ["vim::PushOperator", "Change"], "shift-c": "vim::ChangeToEndOfLine", - "d": [ - "vim::PushOperator", - "Delete" - ], + "d": ["vim::PushOperator", "Delete"], "shift-d": "vim::DeleteToEndOfLine", "shift-j": "vim::JoinLines", - "y": [ - "vim::PushOperator", - "Yank" - ], + "y": ["vim::PushOperator", "Yank"], "shift-y": "vim::YankLine", "i": "vim::InsertBefore", "shift-i": "vim::InsertFirstNonWhitespace", @@ -443,10 +336,7 @@ "backwards": true } ], - "r": [ - "vim::PushOperator", - "Replace" - ], + "r": ["vim::PushOperator", "Replace"], "s": "vim::Substitute", "shift-s": "vim::SubstituteLine", "> >": "editor::Indent", @@ -458,10 +348,7 @@ { "context": "Editor && VimCount", "bindings": { - "0": [ - "vim::Number", - 0 - ] + "0": ["vim::Number", 0] } }, { @@ -497,12 +384,15 @@ "'": "vim::Quotes", "`": "vim::BackQuotes", "\"": "vim::DoubleQuotes", + "|": "vim::VerticalBars", "(": "vim::Parentheses", ")": "vim::Parentheses", + "b": "vim::Parentheses", "[": "vim::SquareBrackets", "]": "vim::SquareBrackets", "{": "vim::CurlyBrackets", "}": "vim::CurlyBrackets", + "shift-b": "vim::CurlyBrackets", "<": "vim::AngleBrackets", ">": "vim::AngleBrackets" } @@ -548,22 +438,10 @@ "shift-i": "vim::InsertBefore", "shift-a": "vim::InsertAfter", "shift-j": "vim::JoinLines", - "r": [ - "vim::PushOperator", - "Replace" - ], - "ctrl-c": [ - "vim::SwitchMode", - "Normal" - ], - "escape": [ - "vim::SwitchMode", - "Normal" - ], - "ctrl+[": [ - "vim::SwitchMode", - "Normal" - ], + "r": ["vim::PushOperator", "Replace"], + "ctrl-c": ["vim::SwitchMode", "Normal"], + "escape": ["vim::SwitchMode", "Normal"], + "ctrl+[": ["vim::SwitchMode", "Normal"], ">": "editor::Indent", "<": "editor::Outdent", "i": [ @@ -602,14 +480,8 @@ "bindings": { "tab": "vim::Tab", "enter": "vim::Enter", - "escape": [ - "vim::SwitchMode", - "Normal" - ], - "ctrl+[": [ - "vim::SwitchMode", - "Normal" - ] + "escape": ["vim::SwitchMode", "Normal"], + "ctrl+[": ["vim::SwitchMode", "Normal"] } }, { diff --git a/assets/settings/default.json b/assets/settings/default.json index e70b563359..19c73ca021 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -148,7 +148,7 @@ // Where to dock channels panel. Can be 'left' or 'right'. "dock": "right", // Default width of the channels panel. - "default_width": 240 + "default_width": 380 }, "assistant": { // Whether to show the assistant panel button in the status bar. diff --git a/crates/ai/src/embedding.rs b/crates/ai/src/embedding.rs index 0f895156f7..8cfc901525 100644 --- a/crates/ai/src/embedding.rs +++ b/crates/ai/src/embedding.rs @@ -2,6 +2,7 @@ use std::time::Instant; use anyhow::Result; use async_trait::async_trait; +use gpui::AppContext; use ordered_float::OrderedFloat; use rusqlite::types::{FromSql, FromSqlResult, ToSqlOutput, ValueRef}; use rusqlite::ToSql; @@ -70,8 +71,12 @@ impl Embedding { #[async_trait] pub trait EmbeddingProvider: Sync + Send { fn base_model(&self) -> Box; - fn is_authenticated(&self) -> bool; - async fn embed_batch(&self, spans: Vec) -> Result>; + fn retrieve_credentials(&self, cx: &AppContext) -> Option; + async fn embed_batch( + &self, + spans: Vec, + api_key: Option, + ) -> Result>; fn max_tokens_per_batch(&self) -> usize; fn rate_limit_expiration(&self) -> Option; } diff --git a/crates/ai/src/providers/dummy.rs b/crates/ai/src/providers/dummy.rs index 7eef16111d..2ee26488bd 100644 --- a/crates/ai/src/providers/dummy.rs +++ b/crates/ai/src/providers/dummy.rs @@ -6,6 +6,7 @@ use crate::{ models::{LanguageModel, TruncationDirection}, }; use async_trait::async_trait; +use gpui::AppContext; use serde::Serialize; pub struct DummyLanguageModel {} @@ -58,16 +59,20 @@ pub struct DummyEmbeddingProvider {} #[async_trait] impl EmbeddingProvider for DummyEmbeddingProvider { + fn retrieve_credentials(&self, _cx: &AppContext) -> Option { + Some("Dummy Credentials".to_string()) + } fn base_model(&self) -> Box { Box::new(DummyLanguageModel {}) } - fn is_authenticated(&self) -> bool { - true - } fn rate_limit_expiration(&self) -> Option { None } - async fn embed_batch(&self, spans: Vec) -> anyhow::Result> { + async fn embed_batch( + &self, + spans: Vec, + api_key: Option, + ) -> anyhow::Result> { // 1024 is the OpenAI Embeddings size for ada models. // the model we will likely be starting with. let dummy_vec = Embedding::from(vec![0.32 as f32; 1536]); diff --git a/crates/ai/src/providers/open_ai/embedding.rs b/crates/ai/src/providers/open_ai/embedding.rs index 3689cb36f4..805a906dda 100644 --- a/crates/ai/src/providers/open_ai/embedding.rs +++ b/crates/ai/src/providers/open_ai/embedding.rs @@ -2,7 +2,7 @@ use anyhow::{anyhow, Result}; use async_trait::async_trait; use futures::AsyncReadExt; use gpui::executor::Background; -use gpui::serde_json; +use gpui::{serde_json, AppContext}; use isahc::http::StatusCode; use isahc::prelude::Configurable; use isahc::{AsyncBody, Response}; @@ -17,11 +17,14 @@ use std::sync::Arc; use std::time::{Duration, Instant}; use tiktoken_rs::{cl100k_base, CoreBPE}; use util::http::{HttpClient, Request}; +use util::ResultExt; use crate::embedding::{Embedding, EmbeddingProvider}; use crate::models::LanguageModel; use crate::providers::open_ai::OpenAILanguageModel; +use super::OPENAI_API_URL; + lazy_static! { static ref OPENAI_API_KEY: Option = env::var("OPENAI_API_KEY").ok(); static ref OPENAI_BPE_TOKENIZER: CoreBPE = cl100k_base().unwrap(); @@ -135,13 +138,25 @@ impl OpenAIEmbeddingProvider { #[async_trait] impl EmbeddingProvider for OpenAIEmbeddingProvider { + fn retrieve_credentials(&self, cx: &AppContext) -> Option { + let api_key = if let Ok(api_key) = env::var("OPENAI_API_KEY") { + Some(api_key) + } else if let Some((_, api_key)) = cx + .platform() + .read_credentials(OPENAI_API_URL) + .log_err() + .flatten() + { + String::from_utf8(api_key).log_err() + } else { + None + }; + api_key + } fn base_model(&self) -> Box { let model: Box = Box::new(self.model.clone()); model } - fn is_authenticated(&self) -> bool { - OPENAI_API_KEY.as_ref().is_some() - } fn max_tokens_per_batch(&self) -> usize { 50000 } @@ -164,7 +179,11 @@ impl EmbeddingProvider for OpenAIEmbeddingProvider { // (output, tokens.len()) // } - async fn embed_batch(&self, spans: Vec) -> Result> { + async fn embed_batch( + &self, + spans: Vec, + api_key: Option, + ) -> Result> { const BACKOFF_SECONDS: [usize; 4] = [3, 5, 15, 45]; const MAX_RETRIES: usize = 4; diff --git a/crates/call/src/call.rs b/crates/call/src/call.rs index 0846341325..ca1a60bd63 100644 --- a/crates/call/src/call.rs +++ b/crates/call/src/call.rs @@ -10,7 +10,7 @@ use client::{ ZED_ALWAYS_ACTIVE, }; use collections::HashSet; -use futures::{future::Shared, FutureExt}; +use futures::{channel::oneshot, future::Shared, Future, FutureExt}; use gpui::{ AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Subscription, Task, WeakModelHandle, @@ -37,10 +37,42 @@ pub struct IncomingCall { pub initial_project: Option, } +pub struct OneAtATime { + cancel: Option>, +} + +impl OneAtATime { + /// spawn a task in the given context. + /// if another task is spawned before that resolves, or if the OneAtATime itself is dropped, the first task will be cancelled and return Ok(None) + /// otherwise you'll see the result of the task. + fn spawn(&mut self, cx: &mut AppContext, f: F) -> Task>> + where + F: 'static + FnOnce(AsyncAppContext) -> Fut, + Fut: Future>, + R: 'static, + { + let (tx, rx) = oneshot::channel(); + self.cancel.replace(tx); + cx.spawn(|cx| async move { + futures::select_biased! { + _ = rx.fuse() => Ok(None), + result = f(cx).fuse() => result.map(Some), + } + }) + } + + fn running(&self) -> bool { + self.cancel + .as_ref() + .is_some_and(|cancel| !cancel.is_canceled()) + } +} + /// Singleton global maintaining the user's participation in a room across workspaces. pub struct ActiveCall { room: Option<(ModelHandle, Vec)>, pending_room_creation: Option, Arc>>>>, + _join_debouncer: OneAtATime, location: Option>, pending_invites: HashSet, incoming_call: ( @@ -69,6 +101,7 @@ impl ActiveCall { pending_invites: Default::default(), incoming_call: watch::channel(), + _join_debouncer: OneAtATime { cancel: None }, _subscriptions: vec![ client.add_request_handler(cx.handle(), Self::handle_incoming_call), client.add_message_handler(cx.handle(), Self::handle_call_canceled), @@ -143,6 +176,10 @@ impl ActiveCall { } cx.notify(); + if self._join_debouncer.running() { + return Task::ready(Ok(())); + } + let room = if let Some(room) = self.room().cloned() { Some(Task::ready(Ok(room)).shared()) } else { @@ -259,11 +296,20 @@ impl ActiveCall { return Task::ready(Err(anyhow!("no incoming call"))); }; - let join = Room::join(&call, self.client.clone(), self.user_store.clone(), cx); + if self.pending_room_creation.is_some() { + return Task::ready(Ok(())); + } + + let room_id = call.room_id.clone(); + let client = self.client.clone(); + let user_store = self.user_store.clone(); + let join = self + ._join_debouncer + .spawn(cx, move |cx| Room::join(room_id, client, user_store, cx)); cx.spawn(|this, mut cx| async move { let room = join.await?; - this.update(&mut cx, |this, cx| this.set_room(Some(room.clone()), cx)) + this.update(&mut cx, |this, cx| this.set_room(room.clone(), cx)) .await?; this.update(&mut cx, |this, cx| { this.report_call_event("accept incoming", cx) @@ -290,20 +336,28 @@ impl ActiveCall { &mut self, channel_id: u64, cx: &mut ModelContext, - ) -> Task>> { + ) -> Task>>> { if let Some(room) = self.room().cloned() { if room.read(cx).channel_id() == Some(channel_id) { - return Task::ready(Ok(room)); + return Task::ready(Ok(Some(room))); } else { room.update(cx, |room, cx| room.clear_state(cx)); } } - let join = Room::join_channel(channel_id, self.client.clone(), self.user_store.clone(), cx); + if self.pending_room_creation.is_some() { + return Task::ready(Ok(None)); + } - cx.spawn(|this, mut cx| async move { + let client = self.client.clone(); + let user_store = self.user_store.clone(); + let join = self._join_debouncer.spawn(cx, move |cx| async move { + Room::join_channel(channel_id, client, user_store, cx).await + }); + + cx.spawn(move |this, mut cx| async move { let room = join.await?; - this.update(&mut cx, |this, cx| this.set_room(Some(room.clone()), cx)) + this.update(&mut cx, |this, cx| this.set_room(room.clone(), cx)) .await?; this.update(&mut cx, |this, cx| { this.report_call_event("join channel", cx) @@ -457,3 +511,40 @@ pub fn report_call_event_for_channel( }; telemetry.report_clickhouse_event(event, telemetry_settings); } + +#[cfg(test)] +mod test { + use gpui::TestAppContext; + + use crate::OneAtATime; + + #[gpui::test] + async fn test_one_at_a_time(cx: &mut TestAppContext) { + let mut one_at_a_time = OneAtATime { cancel: None }; + + assert_eq!( + cx.update(|cx| one_at_a_time.spawn(cx, |_| async { Ok(1) })) + .await + .unwrap(), + Some(1) + ); + + let (a, b) = cx.update(|cx| { + ( + one_at_a_time.spawn(cx, |_| async { + assert!(false); + Ok(2) + }), + one_at_a_time.spawn(cx, |_| async { Ok(3) }), + ) + }); + + assert_eq!(a.await.unwrap(), None); + assert_eq!(b.await.unwrap(), Some(3)); + + let promise = cx.update(|cx| one_at_a_time.spawn(cx, |_| async { Ok(4) })); + drop(one_at_a_time); + + assert_eq!(promise.await.unwrap(), None); + } +} diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs index 4e52f57f60..3ff5a34901 100644 --- a/crates/call/src/room.rs +++ b/crates/call/src/room.rs @@ -1,7 +1,6 @@ use crate::{ call_settings::CallSettings, participant::{LocalParticipant, ParticipantLocation, RemoteParticipant, RemoteVideoTrack}, - IncomingCall, }; use anyhow::{anyhow, Result}; use audio::{Audio, Sound}; @@ -55,7 +54,7 @@ pub enum Event { pub struct Room { id: u64, - channel_id: Option, + pub channel_id: Option, live_kit: Option, status: RoomStatus, shared_projects: HashSet>, @@ -122,6 +121,10 @@ impl Room { } } + pub fn can_publish(&self) -> bool { + self.live_kit.as_ref().is_some_and(|room| room.can_publish) + } + fn new( id: u64, channel_id: Option, @@ -181,20 +184,23 @@ impl Room { }); let connect = room.connect(&connection_info.server_url, &connection_info.token); - cx.spawn(|this, mut cx| async move { - connect.await?; + if connection_info.can_publish { + cx.spawn(|this, mut cx| async move { + connect.await?; - if !cx.read(Self::mute_on_join) { - this.update(&mut cx, |this, cx| this.share_microphone(cx)) - .await?; - } + if !cx.read(Self::mute_on_join) { + this.update(&mut cx, |this, cx| this.share_microphone(cx)) + .await?; + } - anyhow::Ok(()) - }) - .detach_and_log_err(cx); + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + } Some(LiveKitRoom { room, + can_publish: connection_info.can_publish, screen_track: LocalTrack::None, microphone_track: LocalTrack::None, next_publish_id: 0, @@ -284,37 +290,32 @@ impl Room { }) } - pub(crate) fn join_channel( + pub(crate) async fn join_channel( channel_id: u64, client: Arc, user_store: ModelHandle, - cx: &mut AppContext, - ) -> Task>> { - cx.spawn(|cx| async move { - Self::from_join_response( - client.request(proto::JoinChannel { channel_id }).await?, - client, - user_store, - cx, - ) - }) + cx: AsyncAppContext, + ) -> Result> { + Self::from_join_response( + client.request(proto::JoinChannel { channel_id }).await?, + client, + user_store, + cx, + ) } - pub(crate) fn join( - call: &IncomingCall, + pub(crate) async fn join( + room_id: u64, client: Arc, user_store: ModelHandle, - cx: &mut AppContext, - ) -> Task>> { - let id = call.room_id; - cx.spawn(|cx| async move { - Self::from_join_response( - client.request(proto::JoinRoom { id }).await?, - client, - user_store, - cx, - ) - }) + cx: AsyncAppContext, + ) -> Result> { + Self::from_join_response( + client.request(proto::JoinRoom { id: room_id }).await?, + client, + user_store, + cx, + ) } pub fn mute_on_join(cx: &AppContext) -> bool { @@ -1498,6 +1499,7 @@ struct LiveKitRoom { deafened: bool, speaking: bool, next_publish_id: usize, + can_publish: bool, _maintain_room: Task<()>, _maintain_tracks: [Task<()>; 2], } diff --git a/crates/channel/src/channel.rs b/crates/channel/src/channel.rs index b6db304a70..d0a32e16ff 100644 --- a/crates/channel/src/channel.rs +++ b/crates/channel/src/channel.rs @@ -11,9 +11,7 @@ pub use channel_chat::{ mentions_to_proto, ChannelChat, ChannelChatEvent, ChannelMessage, ChannelMessageId, MessageParams, }; -pub use channel_store::{ - Channel, ChannelData, ChannelEvent, ChannelId, ChannelMembership, ChannelPath, ChannelStore, -}; +pub use channel_store::{Channel, ChannelEvent, ChannelId, ChannelMembership, ChannelStore}; #[cfg(test)] mod channel_store_tests; diff --git a/crates/channel/src/channel_buffer.rs b/crates/channel/src/channel_buffer.rs index ab7ea78ac1..9089973d32 100644 --- a/crates/channel/src/channel_buffer.rs +++ b/crates/channel/src/channel_buffer.rs @@ -1,4 +1,4 @@ -use crate::Channel; +use crate::{Channel, ChannelId, ChannelStore}; use anyhow::Result; use client::{Client, Collaborator, UserStore}; use collections::HashMap; @@ -19,10 +19,11 @@ pub(crate) fn init(client: &Arc) { } pub struct ChannelBuffer { - pub(crate) channel: Arc, + pub channel_id: ChannelId, connected: bool, collaborators: HashMap, user_store: ModelHandle, + channel_store: ModelHandle, buffer: ModelHandle, buffer_epoch: u64, client: Arc, @@ -34,6 +35,7 @@ pub enum ChannelBufferEvent { CollaboratorsChanged, Disconnected, BufferEdited, + ChannelChanged, } impl Entity for ChannelBuffer { @@ -46,7 +48,7 @@ impl Entity for ChannelBuffer { } self.client .send(proto::LeaveChannelBuffer { - channel_id: self.channel.id, + channel_id: self.channel_id, }) .log_err(); } @@ -58,6 +60,7 @@ impl ChannelBuffer { channel: Arc, client: Arc, user_store: ModelHandle, + channel_store: ModelHandle, mut cx: AsyncAppContext, ) -> Result> { let response = client @@ -90,9 +93,10 @@ impl ChannelBuffer { connected: true, collaborators: Default::default(), acknowledge_task: None, - channel, + channel_id: channel.id, subscription: Some(subscription.set_model(&cx.handle(), &mut cx.to_async())), user_store, + channel_store, }; this.replace_collaborators(response.collaborators, cx); this @@ -179,7 +183,7 @@ impl ChannelBuffer { let operation = language::proto::serialize_operation(operation); self.client .send(proto::UpdateChannelBuffer { - channel_id: self.channel.id, + channel_id: self.channel_id, operations: vec![operation], }) .log_err(); @@ -223,12 +227,15 @@ impl ChannelBuffer { &self.collaborators } - pub fn channel(&self) -> Arc { - self.channel.clone() + pub fn channel(&self, cx: &AppContext) -> Option> { + self.channel_store + .read(cx) + .channel_for_id(self.channel_id) + .cloned() } pub(crate) fn disconnect(&mut self, cx: &mut ModelContext) { - log::info!("channel buffer {} disconnected", self.channel.id); + log::info!("channel buffer {} disconnected", self.channel_id); if self.connected { self.connected = false; self.subscription.take(); @@ -237,6 +244,11 @@ impl ChannelBuffer { } } + pub(crate) fn channel_changed(&mut self, cx: &mut ModelContext) { + cx.emit(ChannelBufferEvent::ChannelChanged); + cx.notify() + } + pub fn is_connected(&self) -> bool { self.connected } diff --git a/crates/channel/src/channel_chat.rs b/crates/channel/src/channel_chat.rs index ca344c409f..ef11d96424 100644 --- a/crates/channel/src/channel_chat.rs +++ b/crates/channel/src/channel_chat.rs @@ -19,7 +19,7 @@ use time::OffsetDateTime; use util::{post_inc, ResultExt as _, TryFutureExt}; pub struct ChannelChat { - channel: Arc, + pub channel_id: ChannelId, messages: SumTree, acknowledged_message_ids: HashSet, channel_store: ModelHandle, @@ -87,7 +87,7 @@ impl Entity for ChannelChat { fn release(&mut self, _: &mut AppContext) { self.rpc .send(proto::LeaveChannelChat { - channel_id: self.channel.id, + channel_id: self.channel_id, }) .log_err(); } @@ -112,7 +112,7 @@ impl ChannelChat { Ok(cx.add_model(|cx| { let mut this = Self { - channel, + channel_id: channel.id, user_store, channel_store, rpc: client, @@ -130,8 +130,11 @@ impl ChannelChat { })) } - pub fn channel(&self) -> &Arc { - &self.channel + pub fn channel(&self, cx: &AppContext) -> Option> { + self.channel_store + .read(cx) + .channel_for_id(self.channel_id) + .cloned() } pub fn client(&self) -> &Arc { @@ -153,7 +156,7 @@ impl ChannelChat { .current_user() .ok_or_else(|| anyhow!("current_user is not present"))?; - let channel_id = self.channel.id; + let channel_id = self.channel_id; let pending_id = ChannelMessageId::Pending(post_inc(&mut self.next_pending_message_id)); let nonce = self.rng.gen(); self.insert_messages( @@ -195,7 +198,7 @@ impl ChannelChat { pub fn remove_message(&mut self, id: u64, cx: &mut ModelContext) -> Task> { let response = self.rpc.request(proto::RemoveChannelMessage { - channel_id: self.channel.id, + channel_id: self.channel_id, message_id: id, }); cx.spawn(|this, mut cx| async move { @@ -215,7 +218,7 @@ impl ChannelChat { let rpc = self.rpc.clone(); let user_store = self.user_store.clone(); - let channel_id = self.channel.id; + let channel_id = self.channel_id; let before_message_id = self.first_loaded_message_id()?; Some(cx.spawn(|this, mut cx| { async move { @@ -288,13 +291,13 @@ impl ChannelChat { { self.rpc .send(proto::AckChannelMessage { - channel_id: self.channel.id, + channel_id: self.channel_id, message_id: latest_message_id, }) .ok(); self.last_acknowledged_id = Some(latest_message_id); self.channel_store.update(cx, |store, cx| { - store.acknowledge_message_id(self.channel.id, latest_message_id, cx); + store.acknowledge_message_id(self.channel_id, latest_message_id, cx); }); } } @@ -303,7 +306,7 @@ impl ChannelChat { pub fn rejoin(&mut self, cx: &mut ModelContext) { let user_store = self.user_store.clone(); let rpc = self.rpc.clone(); - let channel_id = self.channel.id; + let channel_id = self.channel_id; cx.spawn(|this, mut cx| { async move { let response = rpc.request(proto::JoinChannelChat { channel_id }).await?; @@ -376,7 +379,7 @@ impl ChannelChat { if self.acknowledged_message_ids.insert(id) { self.rpc .send(proto::AckChannelMessage { - channel_id: self.channel.id, + channel_id: self.channel_id, message_id: id, }) .ok(); @@ -412,7 +415,7 @@ impl ChannelChat { this.update(&mut cx, |this, cx| { this.insert_messages(SumTree::from_item(message, &()), cx); cx.emit(ChannelChatEvent::NewMessage { - channel_id: this.channel.id, + channel_id: this.channel_id, message_id, }) }); diff --git a/crates/channel/src/channel_store.rs b/crates/channel/src/channel_store.rs index 221b845297..efa05d51a9 100644 --- a/crates/channel/src/channel_store.rs +++ b/crates/channel/src/channel_store.rs @@ -9,11 +9,10 @@ use db::RELEASE_CHANNEL; use futures::{channel::mpsc, future::Shared, Future, FutureExt, StreamExt}; use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle}; use rpc::{ - proto::{self, ChannelEdge, ChannelPermission, ChannelRole, ChannelVisibility}, + proto::{self, ChannelVisibility}, TypedEnvelope, }; -use serde_derive::{Deserialize, Serialize}; -use std::{borrow::Cow, hash::Hash, mem, ops::Deref, sync::Arc, time::Duration}; +use std::{mem, sync::Arc, time::Duration}; use util::ResultExt; pub fn init(client: &Arc, user_store: ModelHandle, cx: &mut AppContext) { @@ -27,10 +26,9 @@ pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30); pub type ChannelId = u64; pub struct ChannelStore { - channel_index: ChannelIndex, + pub channel_index: ChannelIndex, channel_invitations: Vec>, channel_participants: HashMap>>, - channels_with_admin_privileges: HashSet, outgoing_invites: HashSet<(ChannelId, UserId)>, update_channels_tx: mpsc::UnboundedSender, opened_buffers: HashMap>, @@ -43,15 +41,15 @@ pub struct ChannelStore { _update_channels: Task<()>, } -pub type ChannelData = (Channel, ChannelPath); - #[derive(Clone, Debug, PartialEq)] pub struct Channel { pub id: ChannelId, pub name: String, pub visibility: proto::ChannelVisibility, + pub role: proto::ChannelRole, pub unseen_note_version: Option<(u64, clock::Global)>, pub unseen_message_id: Option, + pub parent_path: Vec, } impl Channel { @@ -72,10 +70,11 @@ impl Channel { slug.trim_matches(|c| c == '-').to_string() } -} -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Serialize, Deserialize)] -pub struct ChannelPath(Arc<[ChannelId]>); + pub fn can_edit_notes(&self) -> bool { + self.role == proto::ChannelRole::Member || self.role == proto::ChannelRole::Admin + } +} pub struct ChannelMembership { pub user: Arc, @@ -161,7 +160,6 @@ impl ChannelStore { channel_invitations: Vec::default(), channel_index: ChannelIndex::default(), channel_participants: Default::default(), - channels_with_admin_privileges: Default::default(), outgoing_invites: Default::default(), opened_buffers: Default::default(), opened_chats: Default::default(), @@ -190,16 +188,6 @@ impl ChannelStore { self.client.clone() } - pub fn has_children(&self, channel_id: ChannelId) -> bool { - self.channel_index.iter().any(|path| { - if let Some(ix) = path.iter().position(|id| *id == channel_id) { - path.len() > ix + 1 - } else { - false - } - }) - } - /// Returns the number of unique channels in the store pub fn channel_count(&self) -> usize { self.channel_index.by_id().len() @@ -219,20 +207,19 @@ impl ChannelStore { } /// Iterate over all entries in the channel DAG - pub fn channel_dag_entries(&self) -> impl '_ + Iterator)> { - self.channel_index.iter().map(move |path| { - let id = path.last().unwrap(); - let channel = self.channel_for_id(*id).unwrap(); - (path.len() - 1, channel) - }) + pub fn ordered_channels(&self) -> impl '_ + Iterator)> { + self.channel_index + .ordered_channels() + .iter() + .filter_map(move |id| { + let channel = self.channel_index.by_id().get(id)?; + Some((channel.parent_path.len(), channel)) + }) } - pub fn channel_dag_entry_at(&self, ix: usize) -> Option<(&Arc, &ChannelPath)> { - let path = self.channel_index.get(ix)?; - let id = path.last().unwrap(); - let channel = self.channel_for_id(*id).unwrap(); - - Some((channel, path)) + pub fn channel_at_index(&self, ix: usize) -> Option<&Arc> { + let channel_id = self.channel_index.ordered_channels().get(ix)?; + self.channel_index.by_id().get(channel_id) } pub fn channel_at(&self, ix: usize) -> Option<&Arc> { @@ -269,10 +256,11 @@ impl ChannelStore { ) -> Task>> { let client = self.client.clone(); let user_store = self.user_store.clone(); + let channel_store = cx.handle(); self.open_channel_resource( channel_id, |this| &mut this.opened_buffers, - |channel, cx| ChannelBuffer::new(channel, client, user_store, cx), + |channel, cx| ChannelBuffer::new(channel, client, user_store, channel_store, cx), cx, ) } @@ -449,16 +437,11 @@ impl ChannelStore { .spawn(async move { task.await.map_err(|error| anyhow!("{}", error)) }) } - pub fn is_user_admin(&self, channel_id: ChannelId) -> bool { - self.channel_index.iter().any(|path| { - if let Some(ix) = path.iter().position(|id| *id == channel_id) { - path[..=ix] - .iter() - .any(|id| self.channels_with_admin_privileges.contains(id)) - } else { - false - } - }) + pub fn is_channel_admin(&self, channel_id: ChannelId) -> bool { + let Some(channel) = self.channel_for_id(channel_id) else { + return false; + }; + channel.role == proto::ChannelRole::Admin } pub fn channel_participants(&self, channel_id: ChannelId) -> &[Arc] { @@ -485,24 +468,19 @@ impl ChannelStore { .ok_or_else(|| anyhow!("missing channel in response"))?; let channel_id = channel.id; - let parent_edge = if let Some(parent_id) = parent_id { - vec![ChannelEdge { - channel_id: channel.id, - parent_id, - }] - } else { - vec![] - }; + // let parent_edge = if let Some(parent_id) = parent_id { + // vec![ChannelEdge { + // channel_id: channel.id, + // parent_id, + // }] + // } else { + // vec![] + // }; this.update(&mut cx, |this, cx| { let task = this.update_channels( proto::UpdateChannels { channels: vec![channel], - insert_edge: parent_edge, - channel_permissions: vec![ChannelPermission { - channel_id, - role: ChannelRole::Admin.into(), - }], ..Default::default() }, cx, @@ -520,53 +498,16 @@ impl ChannelStore { }) } - pub fn link_channel( - &mut self, - channel_id: ChannelId, - to: ChannelId, - cx: &mut ModelContext, - ) -> Task> { - let client = self.client.clone(); - cx.spawn(|_, _| async move { - let _ = client - .request(proto::LinkChannel { channel_id, to }) - .await?; - - Ok(()) - }) - } - - pub fn unlink_channel( - &mut self, - channel_id: ChannelId, - from: ChannelId, - cx: &mut ModelContext, - ) -> Task> { - let client = self.client.clone(); - cx.spawn(|_, _| async move { - let _ = client - .request(proto::UnlinkChannel { channel_id, from }) - .await?; - - Ok(()) - }) - } - pub fn move_channel( &mut self, channel_id: ChannelId, - from: ChannelId, - to: ChannelId, + to: Option, cx: &mut ModelContext, ) -> Task> { let client = self.client.clone(); cx.spawn(|_, _| async move { let _ = client - .request(proto::MoveChannel { - channel_id, - from, - to, - }) + .request(proto::MoveChannel { channel_id, to }) .await?; Ok(()) @@ -800,6 +741,11 @@ impl ChannelStore { } fn handle_connect(&mut self, cx: &mut ModelContext) -> Task> { + self.channel_index.clear(); + self.channel_invitations.clear(); + self.channel_participants.clear(); + self.channel_index.clear(); + self.outgoing_invites.clear(); self.disconnect_channel_buffers_task.take(); for chat in self.opened_chats.values() { @@ -819,7 +765,7 @@ impl ChannelStore { let channel_buffer = buffer.read(cx); let buffer = channel_buffer.buffer().read(cx); buffer_versions.push(proto::ChannelBufferVersion { - channel_id: channel_buffer.channel().id, + channel_id: channel_buffer.channel_id, epoch: channel_buffer.epoch(), version: language::proto::serialize_version(&buffer.version()), }); @@ -846,13 +792,13 @@ impl ChannelStore { }; channel_buffer.update(cx, |channel_buffer, cx| { - let channel_id = channel_buffer.channel().id; + let channel_id = channel_buffer.channel_id; if let Some(remote_buffer) = response .buffers .iter_mut() .find(|buffer| buffer.channel_id == channel_id) { - let channel_id = channel_buffer.channel().id; + let channel_id = channel_buffer.channel_id; let remote_version = language::proto::deserialize_version(&remote_buffer.version); @@ -909,12 +855,6 @@ impl ChannelStore { } fn handle_disconnect(&mut self, wait_for_reconnect: bool, cx: &mut ModelContext) { - self.channel_index.clear(); - self.channel_invitations.clear(); - self.channel_participants.clear(); - self.channels_with_admin_privileges.clear(); - self.channel_index.clear(); - self.outgoing_invites.clear(); cx.notify(); self.disconnect_channel_buffers_task.get_or_insert_with(|| { @@ -958,9 +898,11 @@ impl ChannelStore { Arc::new(Channel { id: channel.id, visibility: channel.visibility(), + role: channel.role(), name: channel.name, unseen_note_version: None, unseen_message_id: None, + parent_path: channel.parent_path, }), ), } @@ -968,8 +910,6 @@ impl ChannelStore { let channels_changed = !payload.channels.is_empty() || !payload.delete_channels.is_empty() - || !payload.insert_edge.is_empty() - || !payload.delete_edge.is_empty() || !payload.unseen_channel_messages.is_empty() || !payload.unseen_channel_buffer_changes.is_empty(); @@ -977,12 +917,17 @@ impl ChannelStore { if !payload.delete_channels.is_empty() { self.channel_index.delete_channels(&payload.delete_channels); self.channel_participants - .retain(|channel_id, _| !payload.delete_channels.contains(channel_id)); - self.channels_with_admin_privileges - .retain(|channel_id| !payload.delete_channels.contains(channel_id)); + .retain(|channel_id, _| !&payload.delete_channels.contains(channel_id)); for channel_id in &payload.delete_channels { let channel_id = *channel_id; + if payload + .channels + .iter() + .any(|channel| channel.id == channel_id) + { + continue; + } if let Some(OpenedModelHandle::Open(buffer)) = self.opened_buffers.remove(&channel_id) { @@ -995,7 +940,16 @@ impl ChannelStore { let mut index = self.channel_index.bulk_insert(); for channel in payload.channels { - index.insert(channel) + let id = channel.id; + let channel_changed = index.insert(channel); + + if channel_changed { + if let Some(OpenedModelHandle::Open(buffer)) = self.opened_buffers.get(&id) { + if let Some(buffer) = buffer.upgrade(cx) { + buffer.update(cx, ChannelBuffer::channel_changed); + } + } + } } for unseen_buffer_change in payload.unseen_channel_buffer_changes { @@ -1013,24 +967,6 @@ impl ChannelStore { unseen_channel_message.message_id, ); } - - for edge in payload.insert_edge { - index.insert_edge(edge.channel_id, edge.parent_id); - } - - for edge in payload.delete_edge { - index.delete_edge(edge.parent_id, edge.channel_id); - } - } - - for permission in payload.channel_permissions { - if permission.role() == proto::ChannelRole::Admin { - self.channels_with_admin_privileges - .insert(permission.channel_id); - } else { - self.channels_with_admin_privileges - .remove(&permission.channel_id); - } } cx.notify(); @@ -1079,44 +1015,3 @@ impl ChannelStore { })) } } - -impl Deref for ChannelPath { - type Target = [ChannelId]; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl ChannelPath { - pub fn new(path: Arc<[ChannelId]>) -> Self { - debug_assert!(path.len() >= 1); - Self(path) - } - - pub fn parent_id(&self) -> Option { - self.0.len().checked_sub(2).map(|i| self.0[i]) - } - - pub fn channel_id(&self) -> ChannelId { - self.0[self.0.len() - 1] - } -} - -impl From for Cow<'static, ChannelPath> { - fn from(value: ChannelPath) -> Self { - Cow::Owned(value) - } -} - -impl<'a> From<&'a ChannelPath> for Cow<'a, ChannelPath> { - fn from(value: &'a ChannelPath) -> Self { - Cow::Borrowed(value) - } -} - -impl Default for ChannelPath { - fn default() -> Self { - ChannelPath(Arc::from([])) - } -} diff --git a/crates/channel/src/channel_store/channel_index.rs b/crates/channel/src/channel_store/channel_index.rs index 36379a3942..97b2ab6318 100644 --- a/crates/channel/src/channel_store/channel_index.rs +++ b/crates/channel/src/channel_store/channel_index.rs @@ -1,14 +1,11 @@ -use std::{ops::Deref, sync::Arc}; - use crate::{Channel, ChannelId}; use collections::BTreeMap; use rpc::proto; - -use super::ChannelPath; +use std::sync::Arc; #[derive(Default, Debug)] pub struct ChannelIndex { - paths: Vec, + channels_ordered: Vec, channels_by_id: BTreeMap>, } @@ -17,8 +14,12 @@ impl ChannelIndex { &self.channels_by_id } + pub fn ordered_channels(&self) -> &[ChannelId] { + &self.channels_ordered + } + pub fn clear(&mut self) { - self.paths.clear(); + self.channels_ordered.clear(); self.channels_by_id.clear(); } @@ -26,15 +27,13 @@ impl ChannelIndex { pub fn delete_channels(&mut self, channels: &[ChannelId]) { self.channels_by_id .retain(|channel_id, _| !channels.contains(channel_id)); - self.paths.retain(|path| { - path.iter() - .all(|channel_id| self.channels_by_id.contains_key(channel_id)) - }); + self.channels_ordered + .retain(|channel_id| !channels.contains(channel_id)); } pub fn bulk_insert(&mut self) -> ChannelPathsInsertGuard { ChannelPathsInsertGuard { - paths: &mut self.paths, + channels_ordered: &mut self.channels_ordered, channels_by_id: &mut self.channels_by_id, } } @@ -77,42 +76,15 @@ impl ChannelIndex { } } -impl Deref for ChannelIndex { - type Target = [ChannelPath]; - - fn deref(&self) -> &Self::Target { - &self.paths - } -} - /// A guard for ensuring that the paths index maintains its sort and uniqueness /// invariants after a series of insertions #[derive(Debug)] pub struct ChannelPathsInsertGuard<'a> { - paths: &'a mut Vec, + channels_ordered: &'a mut Vec, channels_by_id: &'a mut BTreeMap>, } impl<'a> ChannelPathsInsertGuard<'a> { - /// Remove the given edge from this index. This will not remove the channel. - /// If this operation would result in a dangling edge, re-insert it. - pub fn delete_edge(&mut self, parent_id: ChannelId, channel_id: ChannelId) { - self.paths.retain(|path| { - !path - .windows(2) - .any(|window| window == [parent_id, channel_id]) - }); - - // Ensure that there is at least one channel path in the index - if !self - .paths - .iter() - .any(|path| path.iter().any(|id| id == &channel_id)) - { - self.insert_root(channel_id); - } - } - pub fn note_changed(&mut self, channel_id: ChannelId, epoch: u64, version: &clock::Global) { insert_note_changed(&mut self.channels_by_id, channel_id, epoch, &version); } @@ -121,10 +93,17 @@ impl<'a> ChannelPathsInsertGuard<'a> { insert_new_message(&mut self.channels_by_id, channel_id, message_id) } - pub fn insert(&mut self, channel_proto: proto::Channel) { + pub fn insert(&mut self, channel_proto: proto::Channel) -> bool { + let mut ret = false; if let Some(existing_channel) = self.channels_by_id.get_mut(&channel_proto.id) { let existing_channel = Arc::make_mut(existing_channel); + + ret = existing_channel.visibility != channel_proto.visibility() + || existing_channel.role != channel_proto.role() + || existing_channel.name != channel_proto.name; + existing_channel.visibility = channel_proto.visibility(); + existing_channel.role = channel_proto.role(); existing_channel.name = channel_proto.name; } else { self.channels_by_id.insert( @@ -132,83 +111,47 @@ impl<'a> ChannelPathsInsertGuard<'a> { Arc::new(Channel { id: channel_proto.id, visibility: channel_proto.visibility(), + role: channel_proto.role(), name: channel_proto.name, unseen_note_version: None, unseen_message_id: None, + parent_path: channel_proto.parent_path, }), ); self.insert_root(channel_proto.id); } - } - - pub fn insert_edge(&mut self, channel_id: ChannelId, parent_id: ChannelId) { - let mut parents = Vec::new(); - let mut descendants = Vec::new(); - let mut ixs_to_remove = Vec::new(); - - for (ix, path) in self.paths.iter().enumerate() { - if path - .windows(2) - .any(|window| window[0] == parent_id && window[1] == channel_id) - { - // We already have this edge in the index - return; - } - if path.ends_with(&[parent_id]) { - parents.push(path); - } else if let Some(position) = path.iter().position(|id| id == &channel_id) { - if position == 0 { - ixs_to_remove.push(ix); - } - descendants.push(path.split_at(position).1); - } - } - - let mut new_paths = Vec::new(); - for parent in parents.iter() { - if descendants.is_empty() { - let mut new_path = Vec::with_capacity(parent.len() + 1); - new_path.extend_from_slice(parent); - new_path.push(channel_id); - new_paths.push(ChannelPath::new(new_path.into())); - } else { - for descendant in descendants.iter() { - let mut new_path = Vec::with_capacity(parent.len() + descendant.len()); - new_path.extend_from_slice(parent); - new_path.extend_from_slice(descendant); - new_paths.push(ChannelPath::new(new_path.into())); - } - } - } - - for ix in ixs_to_remove.into_iter().rev() { - self.paths.swap_remove(ix); - } - self.paths.extend(new_paths) + ret } fn insert_root(&mut self, channel_id: ChannelId) { - self.paths.push(ChannelPath::new(Arc::from([channel_id]))); + self.channels_ordered.push(channel_id); } } impl<'a> Drop for ChannelPathsInsertGuard<'a> { fn drop(&mut self) { - self.paths.sort_by(|a, b| { - let a = channel_path_sorting_key(a, &self.channels_by_id); - let b = channel_path_sorting_key(b, &self.channels_by_id); + self.channels_ordered.sort_by(|a, b| { + let a = channel_path_sorting_key(*a, &self.channels_by_id); + let b = channel_path_sorting_key(*b, &self.channels_by_id); a.cmp(b) }); - self.paths.dedup(); + self.channels_ordered.dedup(); } } fn channel_path_sorting_key<'a>( - path: &'a [ChannelId], + id: ChannelId, channels_by_id: &'a BTreeMap>, -) -> impl 'a + Iterator> { - path.iter() - .map(|id| Some(channels_by_id.get(id)?.name.as_str())) +) -> impl Iterator { + let (parent_path, name) = channels_by_id + .get(&id) + .map_or((&[] as &[_], None), |channel| { + (channel.parent_path.as_slice(), Some(channel.name.as_str())) + }); + parent_path + .iter() + .filter_map(|id| Some(channels_by_id.get(id)?.name.as_str())) + .chain(name) } fn insert_note_changed( diff --git a/crates/channel/src/channel_store_tests.rs b/crates/channel/src/channel_store_tests.rs index 8cc9cb73da..ff8761ee91 100644 --- a/crates/channel/src/channel_store_tests.rs +++ b/crates/channel/src/channel_store_tests.rs @@ -19,17 +19,17 @@ fn test_update_channels(cx: &mut AppContext) { id: 1, name: "b".to_string(), visibility: proto::ChannelVisibility::Members as i32, + role: proto::ChannelRole::Admin.into(), + parent_path: Vec::new(), }, proto::Channel { id: 2, name: "a".to_string(), visibility: proto::ChannelVisibility::Members as i32, + role: proto::ChannelRole::Member.into(), + parent_path: Vec::new(), }, ], - channel_permissions: vec![proto::ChannelPermission { - channel_id: 1, - role: proto::ChannelRole::Admin.into(), - }], ..Default::default() }, cx, @@ -38,8 +38,8 @@ fn test_update_channels(cx: &mut AppContext) { &channel_store, &[ // - (0, "a".to_string(), false), - (0, "b".to_string(), true), + (0, "a".to_string(), proto::ChannelRole::Member), + (0, "b".to_string(), proto::ChannelRole::Admin), ], cx, ); @@ -52,21 +52,15 @@ fn test_update_channels(cx: &mut AppContext) { id: 3, name: "x".to_string(), visibility: proto::ChannelVisibility::Members as i32, + role: proto::ChannelRole::Admin.into(), + parent_path: vec![1], }, proto::Channel { id: 4, name: "y".to_string(), visibility: proto::ChannelVisibility::Members as i32, - }, - ], - insert_edge: vec![ - proto::ChannelEdge { - parent_id: 1, - channel_id: 3, - }, - proto::ChannelEdge { - parent_id: 2, - channel_id: 4, + role: proto::ChannelRole::Member.into(), + parent_path: vec![2], }, ], ..Default::default() @@ -76,10 +70,10 @@ fn test_update_channels(cx: &mut AppContext) { assert_channels( &channel_store, &[ - (0, "a".to_string(), false), - (1, "y".to_string(), false), - (0, "b".to_string(), true), - (1, "x".to_string(), true), + (0, "a".to_string(), proto::ChannelRole::Member), + (1, "y".to_string(), proto::ChannelRole::Member), + (0, "b".to_string(), proto::ChannelRole::Admin), + (1, "x".to_string(), proto::ChannelRole::Admin), ], cx, ); @@ -97,32 +91,24 @@ fn test_dangling_channel_paths(cx: &mut AppContext) { id: 0, name: "a".to_string(), visibility: proto::ChannelVisibility::Members as i32, + role: proto::ChannelRole::Admin.into(), + parent_path: vec![], }, proto::Channel { id: 1, name: "b".to_string(), visibility: proto::ChannelVisibility::Members as i32, + role: proto::ChannelRole::Admin.into(), + parent_path: vec![0], }, proto::Channel { id: 2, name: "c".to_string(), visibility: proto::ChannelVisibility::Members as i32, + role: proto::ChannelRole::Admin.into(), + parent_path: vec![0, 1], }, ], - insert_edge: vec![ - proto::ChannelEdge { - parent_id: 0, - channel_id: 1, - }, - proto::ChannelEdge { - parent_id: 1, - channel_id: 2, - }, - ], - channel_permissions: vec![proto::ChannelPermission { - channel_id: 0, - role: proto::ChannelRole::Admin.into(), - }], ..Default::default() }, cx, @@ -132,9 +118,9 @@ fn test_dangling_channel_paths(cx: &mut AppContext) { &channel_store, &[ // - (0, "a".to_string(), true), - (1, "b".to_string(), true), - (2, "c".to_string(), true), + (0, "a".to_string(), proto::ChannelRole::Admin), + (1, "b".to_string(), proto::ChannelRole::Admin), + (2, "c".to_string(), proto::ChannelRole::Admin), ], cx, ); @@ -149,7 +135,11 @@ fn test_dangling_channel_paths(cx: &mut AppContext) { ); // Make sure that the 1/2/3 path is gone - assert_channels(&channel_store, &[(0, "a".to_string(), true)], cx); + assert_channels( + &channel_store, + &[(0, "a".to_string(), proto::ChannelRole::Admin)], + cx, + ); } #[gpui::test] @@ -166,12 +156,18 @@ async fn test_channel_messages(cx: &mut TestAppContext) { id: channel_id, name: "the-channel".to_string(), visibility: proto::ChannelVisibility::Members as i32, + role: proto::ChannelRole::Member.into(), + parent_path: vec![], }], ..Default::default() }); cx.foreground().run_until_parked(); cx.read(|cx| { - assert_channels(&channel_store, &[(0, "the-channel".to_string(), false)], cx); + assert_channels( + &channel_store, + &[(0, "the-channel".to_string(), proto::ChannelRole::Member)], + cx, + ); }); let get_users = server.receive::().await.unwrap(); @@ -189,7 +185,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) { // Join a channel and populate its existing messages. let channel = channel_store.update(cx, |store, cx| { - let channel_id = store.channel_dag_entries().next().unwrap().1.id; + let channel_id = store.ordered_channels().next().unwrap().1.id; store.open_channel_chat(channel_id, cx) }); let join_channel = server.receive::().await.unwrap(); @@ -371,19 +367,13 @@ fn update_channels( #[track_caller] fn assert_channels( channel_store: &ModelHandle, - expected_channels: &[(usize, String, bool)], + expected_channels: &[(usize, String, proto::ChannelRole)], cx: &AppContext, ) { let actual = channel_store.read_with(cx, |store, _| { store - .channel_dag_entries() - .map(|(depth, channel)| { - ( - depth, - channel.name.to_string(), - store.is_user_admin(channel.id), - ) - }) + .ordered_channels() + .map(|(depth, channel)| (depth, channel.name.to_string(), channel.role)) .collect::>() }); assert_eq!(actual, expected_channels); diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index 7fa808b498..775a4c1bbe 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -193,9 +193,12 @@ CREATE TABLE "channels" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT, "name" VARCHAR NOT NULL, "created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - "visibility" VARCHAR NOT NULL + "visibility" VARCHAR NOT NULL, + "parent_path" TEXT ); +CREATE INDEX "index_channels_on_parent_path" ON "channels" ("parent_path"); + CREATE TABLE IF NOT EXISTS "channel_chat_participants" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT, "user_id" INTEGER NOT NULL REFERENCES users (id), @@ -224,12 +227,6 @@ CREATE TABLE "channel_message_mentions" ( PRIMARY KEY(message_id, start_offset) ); -CREATE TABLE "channel_paths" ( - "id_path" TEXT NOT NULL PRIMARY KEY, - "channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE -); -CREATE INDEX "index_channel_paths_on_channel_id" ON "channel_paths" ("channel_id"); - CREATE TABLE "channel_members" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT, "channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE, diff --git a/crates/collab/migrations/20231024085546_move_channel_paths_to_channels_table.sql b/crates/collab/migrations/20231024085546_move_channel_paths_to_channels_table.sql new file mode 100644 index 0000000000..d9fc6c8722 --- /dev/null +++ b/crates/collab/migrations/20231024085546_move_channel_paths_to_channels_table.sql @@ -0,0 +1,12 @@ +ALTER TABLE channels ADD COLUMN parent_path TEXT; + +UPDATE channels +SET parent_path = substr( + channel_paths.id_path, + 2, + length(channel_paths.id_path) - length('/' || channel_paths.channel_id::text || '/') +) +FROM channel_paths +WHERE channel_paths.channel_id = channels.id; + +CREATE INDEX "index_channels_on_parent_path" ON "channels" ("parent_path"); diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index 5f3d0fc0c7..df33416a46 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -13,7 +13,6 @@ use anyhow::anyhow; use collections::{BTreeMap, HashMap, HashSet}; use dashmap::DashMap; use futures::StreamExt; -use queries::channels::ChannelGraph; use rand::{prelude::StdRng, Rng, SeedableRng}; use rpc::{ proto::{self}, @@ -435,18 +434,115 @@ pub struct NewUserResult { pub signup_device_id: Option, } -#[derive(FromQueryResult, Debug, PartialEq, Eq, Hash)] +#[derive(Debug)] +pub struct MoveChannelResult { + pub participants_to_update: HashMap, + pub participants_to_remove: HashSet, + pub moved_channels: HashSet, +} + +#[derive(Debug)] +pub struct RenameChannelResult { + pub channel: Channel, + pub participants_to_update: HashMap, +} + +#[derive(Debug)] +pub struct CreateChannelResult { + pub channel: Channel, + pub participants_to_update: Vec<(UserId, ChannelsForUser)>, +} + +#[derive(Debug)] +pub struct SetChannelVisibilityResult { + pub participants_to_update: HashMap, + pub participants_to_remove: HashSet, + pub channels_to_remove: Vec, +} + +#[derive(Debug)] +pub struct MembershipUpdated { + pub channel_id: ChannelId, + pub new_channels: ChannelsForUser, + pub removed_channels: Vec, +} + +#[derive(Debug)] +pub enum SetMemberRoleResult { + InviteUpdated(Channel), + MembershipUpdated(MembershipUpdated), +} + +#[derive(Debug)] +pub struct InviteMemberResult { + pub channel: Channel, + pub notifications: NotificationBatch, +} + +#[derive(Debug)] +pub struct RespondToChannelInvite { + pub membership_update: Option, + pub notifications: NotificationBatch, +} + +#[derive(Debug)] +pub struct RemoveChannelMemberResult { + pub membership_update: MembershipUpdated, + pub notification_id: Option, +} + +#[derive(Debug, PartialEq, Eq, Hash)] pub struct Channel { pub id: ChannelId, pub name: String, pub visibility: ChannelVisibility, + pub role: ChannelRole, + pub parent_path: Vec, +} + +impl Channel { + fn from_model(value: channel::Model, role: ChannelRole) -> Self { + Channel { + id: value.id, + visibility: value.visibility, + name: value.clone().name, + role, + parent_path: value.ancestors().collect(), + } + } + + pub fn to_proto(&self) -> proto::Channel { + proto::Channel { + id: self.id.to_proto(), + name: self.name.clone(), + visibility: self.visibility.into(), + role: self.role.into(), + parent_path: self.parent_path.iter().map(|c| c.to_proto()).collect(), + } + } +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct ChannelMember { + pub role: ChannelRole, + pub user_id: UserId, + pub kind: proto::channel_member::Kind, +} + +impl ChannelMember { + pub fn to_proto(&self) -> proto::ChannelMember { + proto::ChannelMember { + role: self.role.into(), + user_id: self.user_id.to_proto(), + kind: self.kind.into(), + } + } } #[derive(Debug, PartialEq)] pub struct ChannelsForUser { - pub channels: ChannelGraph, + pub channels: Vec, pub channel_participants: HashMap>, - pub channels_with_admin_privileges: HashSet, pub unseen_buffer_changes: Vec, pub channel_messages: Vec, } diff --git a/crates/collab/src/db/ids.rs b/crates/collab/src/db/ids.rs index 433444de67..5f0df90811 100644 --- a/crates/collab/src/db/ids.rs +++ b/crates/collab/src/db/ids.rs @@ -84,7 +84,7 @@ id_type!(FlagId); id_type!(NotificationId); id_type!(NotificationKindId); -#[derive(Eq, PartialEq, Copy, Clone, Debug, EnumIter, DeriveActiveEnum, Default)] +#[derive(Eq, PartialEq, Copy, Clone, Debug, EnumIter, DeriveActiveEnum, Default, Hash)] #[sea_orm(rs_type = "String", db_type = "String(None)")] pub enum ChannelRole { #[sea_orm(string_value = "admin")] @@ -116,6 +116,22 @@ impl ChannelRole { other } } + + pub fn can_see_all_descendants(&self) -> bool { + use ChannelRole::*; + match self { + Admin | Member => true, + Guest | Banned => false, + } + } + + pub fn can_only_see_public_descendants(&self) -> bool { + use ChannelRole::*; + match self { + Guest => true, + Admin | Member | Banned => false, + } + } } impl From for ChannelRole { diff --git a/crates/collab/src/db/queries/buffers.rs b/crates/collab/src/db/queries/buffers.rs index 69f100e6b8..9eddb1f618 100644 --- a/crates/collab/src/db/queries/buffers.rs +++ b/crates/collab/src/db/queries/buffers.rs @@ -16,7 +16,8 @@ impl Database { connection: ConnectionId, ) -> Result { self.transaction(|tx| async move { - self.check_user_is_channel_member(channel_id, user_id, &tx) + let channel = self.get_channel_internal(channel_id, &*tx).await?; + self.check_user_is_channel_participant(&channel, user_id, &tx) .await?; let buffer = channel::Model { @@ -129,9 +130,11 @@ impl Database { self.transaction(|tx| async move { let mut results = Vec::new(); for client_buffer in buffers { - let channel_id = ChannelId::from_proto(client_buffer.channel_id); + let channel = self + .get_channel_internal(ChannelId::from_proto(client_buffer.channel_id), &*tx) + .await?; if self - .check_user_is_channel_member(channel_id, user_id, &*tx) + .check_user_is_channel_participant(&channel, user_id, &*tx) .await .is_err() { @@ -139,9 +142,9 @@ impl Database { continue; } - let buffer = self.get_channel_buffer(channel_id, &*tx).await?; + let buffer = self.get_channel_buffer(channel.id, &*tx).await?; let mut collaborators = channel_buffer_collaborator::Entity::find() - .filter(channel_buffer_collaborator::Column::ChannelId.eq(channel_id)) + .filter(channel_buffer_collaborator::Column::ChannelId.eq(channel.id)) .all(&*tx) .await?; @@ -439,7 +442,8 @@ impl Database { Vec, )> { self.transaction(move |tx| async move { - self.check_user_is_channel_member(channel_id, user, &*tx) + let channel = self.get_channel_internal(channel_id, &*tx).await?; + self.check_user_is_channel_member(&channel, user, &*tx) .await?; let buffer = buffer::Entity::find() @@ -482,9 +486,7 @@ impl Database { ) .await?; - channel_members = self - .get_channel_participants_internal(channel_id, &*tx) - .await?; + channel_members = self.get_channel_participants(&channel, &*tx).await?; let collaborators = self .get_channel_buffer_collaborators_internal(channel_id, &*tx) .await?; diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs index 4ee7625afd..68b06e435d 100644 --- a/crates/collab/src/db/queries/channels.rs +++ b/crates/collab/src/db/queries/channels.rs @@ -1,5 +1,6 @@ use super::*; -use rpc::proto::{channel_member::Kind, ChannelEdge}; +use rpc::proto::channel_member::Kind; +use sea_orm::TryGetableMany; impl Database { #[cfg(test)] @@ -16,72 +17,82 @@ impl Database { .await } + #[cfg(test)] pub async fn create_root_channel(&self, name: &str, creator_id: UserId) -> Result { - self.create_channel(name, None, creator_id).await + Ok(self + .create_channel(name, None, creator_id) + .await? + .channel + .id) + } + + #[cfg(test)] + pub async fn create_sub_channel( + &self, + name: &str, + parent: ChannelId, + creator_id: UserId, + ) -> Result { + Ok(self + .create_channel(name, Some(parent), creator_id) + .await? + .channel + .id) } pub async fn create_channel( &self, name: &str, - parent: Option, - creator_id: UserId, - ) -> Result { + parent_channel_id: Option, + admin_id: UserId, + ) -> Result { let name = Self::sanitize_channel_name(name)?; self.transaction(move |tx| async move { - if let Some(parent) = parent { - self.check_user_is_channel_admin(parent, creator_id, &*tx) + let mut parent = None; + + if let Some(parent_channel_id) = parent_channel_id { + let parent_channel = self.get_channel_internal(parent_channel_id, &*tx).await?; + self.check_user_is_channel_admin(&parent_channel, admin_id, &*tx) .await?; + parent = Some(parent_channel); } let channel = channel::ActiveModel { id: ActiveValue::NotSet, name: ActiveValue::Set(name.to_string()), visibility: ActiveValue::Set(ChannelVisibility::Members), + parent_path: ActiveValue::Set( + parent + .as_ref() + .map_or(String::new(), |parent| parent.path()), + ), } .insert(&*tx) .await?; - if let Some(parent) = parent { - let sql = r#" - INSERT INTO channel_paths - (id_path, channel_id) - SELECT - id_path || $1 || '/', $2 - FROM - channel_paths - WHERE - channel_id = $3 - "#; - let channel_paths_stmt = Statement::from_sql_and_values( - self.pool.get_database_backend(), - sql, - [ - channel.id.to_proto().into(), - channel.id.to_proto().into(), - parent.to_proto().into(), - ], - ); - tx.execute(channel_paths_stmt).await?; + let participants_to_update; + if let Some(parent) = &parent { + participants_to_update = self + .participants_to_notify_for_channel_change(parent, &*tx) + .await?; } else { - channel_path::Entity::insert(channel_path::ActiveModel { + participants_to_update = vec![]; + + channel_member::ActiveModel { + id: ActiveValue::NotSet, channel_id: ActiveValue::Set(channel.id), - id_path: ActiveValue::Set(format!("/{}/", channel.id)), - }) - .exec(&*tx) + user_id: ActiveValue::Set(admin_id), + accepted: ActiveValue::Set(true), + role: ActiveValue::Set(ChannelRole::Admin), + } + .insert(&*tx) .await?; - } + }; - channel_member::ActiveModel { - id: ActiveValue::NotSet, - channel_id: ActiveValue::Set(channel.id), - user_id: ActiveValue::Set(creator_id), - accepted: ActiveValue::Set(true), - role: ActiveValue::Set(ChannelRole::Admin), - } - .insert(&*tx) - .await?; - - Ok(channel.id) + Ok(CreateChannelResult { + channel: Channel::from_model(channel, ChannelRole::Admin), + participants_to_update, + }) }) .await } @@ -92,28 +103,20 @@ impl Database { user_id: UserId, connection: ConnectionId, environment: &str, - ) -> Result<(JoinRoom, Option)> { + ) -> Result<(JoinRoom, Option, ChannelRole)> { self.transaction(move |tx| async move { - let mut joined_channel_id = None; + let channel = self.get_channel_internal(channel_id, &*tx).await?; + let mut role = self.channel_role_for_user(&channel, user_id, &*tx).await?; - let channel = channel::Entity::find() - .filter(channel::Column::Id.eq(channel_id)) - .one(&*tx) - .await?; + let mut accept_invite_result = None; - let mut role = self - .channel_role_for_user(channel_id, user_id, &*tx) - .await?; - - if role.is_none() && channel.is_some() { + if role.is_none() { if let Some(invitation) = self - .pending_invite_for_channel(channel_id, user_id, &*tx) + .pending_invite_for_channel(&channel, user_id, &*tx) .await? { // note, this may be a parent channel - joined_channel_id = Some(invitation.channel_id); role = Some(invitation.role); - channel_member::Entity::update(channel_member::ActiveModel { accepted: ActiveValue::Set(true), ..invitation.into_active_model() @@ -121,44 +124,46 @@ impl Database { .exec(&*tx) .await?; + accept_invite_result = Some( + self.calculate_membership_updated(&channel, user_id, &*tx) + .await?, + ); + debug_assert!( - self.channel_role_for_user(channel_id, user_id, &*tx) - .await? - == role + self.channel_role_for_user(&channel, user_id, &*tx).await? == role ); } } - if role.is_none() - && channel.as_ref().map(|c| c.visibility) == Some(ChannelVisibility::Public) - { - let channel_id_to_join = self - .most_public_ancestor_for_channel(channel_id, &*tx) + + if channel.visibility == ChannelVisibility::Public { + role = Some(ChannelRole::Guest); + let channel_to_join = self + .public_ancestors_including_self(&channel, &*tx) .await? - .unwrap_or(channel_id); - // TODO: change this back to Guest. - role = Some(ChannelRole::Member); - joined_channel_id = Some(channel_id_to_join); + .first() + .cloned() + .unwrap_or(channel.clone()); channel_member::Entity::insert(channel_member::ActiveModel { id: ActiveValue::NotSet, - channel_id: ActiveValue::Set(channel_id_to_join), + channel_id: ActiveValue::Set(channel_to_join.id), user_id: ActiveValue::Set(user_id), accepted: ActiveValue::Set(true), - // TODO: change this back to Guest. - role: ActiveValue::Set(ChannelRole::Member), + role: ActiveValue::Set(ChannelRole::Guest), }) .exec(&*tx) .await?; - debug_assert!( - self.channel_role_for_user(channel_id, user_id, &*tx) - .await? - == role + accept_invite_result = Some( + self.calculate_membership_updated(&channel_to_join, user_id, &*tx) + .await?, ); + + debug_assert!(self.channel_role_for_user(&channel, user_id, &*tx).await? == role); } - if channel.is_none() || role.is_none() || role == Some(ChannelRole::Banned) { - Err(anyhow!("no such channel, or not allowed"))? + if role.is_none() || role == Some(ChannelRole::Banned) { + Err(anyhow!("not allowed"))? } let live_kit_room = format!("channel-{}", nanoid::nanoid!(30)); @@ -166,9 +171,9 @@ impl Database { .get_or_create_channel_room(channel_id, &live_kit_room, environment, &*tx) .await?; - self.join_channel_room_internal(channel_id, room_id, user_id, connection, &*tx) + self.join_channel_room_internal(room_id, user_id, connection, &*tx) .await - .map(|jr| (jr, joined_channel_id)) + .map(|jr| (jr, accept_invite_result, role.unwrap())) }) .await } @@ -177,21 +182,77 @@ impl Database { &self, channel_id: ChannelId, visibility: ChannelVisibility, - user_id: UserId, - ) -> Result { + admin_id: UserId, + ) -> Result { self.transaction(move |tx| async move { - self.check_user_is_channel_admin(channel_id, user_id, &*tx) + let channel = self.get_channel_internal(channel_id, &*tx).await?; + + self.check_user_is_channel_admin(&channel, admin_id, &*tx) .await?; - let channel = channel::ActiveModel { - id: ActiveValue::Unchanged(channel_id), - visibility: ActiveValue::Set(visibility), - ..Default::default() - } - .update(&*tx) - .await?; + let previous_members = self + .get_channel_participant_details_internal(&channel, &*tx) + .await?; - Ok(channel) + let mut model = channel.into_active_model(); + model.visibility = ActiveValue::Set(visibility); + let channel = model.update(&*tx).await?; + + let mut participants_to_update: HashMap = self + .participants_to_notify_for_channel_change(&channel, &*tx) + .await? + .into_iter() + .collect(); + + let mut channels_to_remove: Vec = vec![]; + let mut participants_to_remove: HashSet = HashSet::default(); + match visibility { + ChannelVisibility::Members => { + let all_descendents: Vec = self + .get_channel_descendants_including_self(vec![channel_id], &*tx) + .await? + .into_iter() + .map(|channel| channel.id) + .collect(); + + channels_to_remove = channel::Entity::find() + .filter( + channel::Column::Id + .is_in(all_descendents) + .and(channel::Column::Visibility.eq(ChannelVisibility::Public)), + ) + .all(&*tx) + .await? + .into_iter() + .map(|channel| channel.id) + .collect(); + + channels_to_remove.push(channel_id); + + for member in previous_members { + if member.role.can_only_see_public_descendants() { + participants_to_remove.insert(member.user_id); + } + } + } + ChannelVisibility::Public => { + if let Some(public_parent) = self.public_parent_channel(&channel, &*tx).await? { + let parent_updates = self + .participants_to_notify_for_channel_change(&public_parent, &*tx) + .await?; + + for (user_id, channels) in parent_updates { + participants_to_update.insert(user_id, channels); + } + } + } + } + + Ok(SetChannelVisibilityResult { + participants_to_update, + participants_to_remove, + channels_to_remove, + }) }) .await } @@ -202,39 +263,12 @@ impl Database { user_id: UserId, ) -> Result<(Vec, Vec)> { self.transaction(move |tx| async move { - self.check_user_is_channel_admin(channel_id, user_id, &*tx) + let channel = self.get_channel_internal(channel_id, &*tx).await?; + self.check_user_is_channel_admin(&channel, user_id, &*tx) .await?; - // Don't remove descendant channels that have additional parents. - let mut channels_to_remove: HashSet = HashSet::default(); - channels_to_remove.insert(channel_id); - - let graph = self.get_channel_descendants([channel_id], &*tx).await?; - for edge in graph.iter() { - channels_to_remove.insert(ChannelId::from_proto(edge.channel_id)); - } - - { - let mut channels_to_keep = channel_path::Entity::find() - .filter( - channel_path::Column::ChannelId - .is_in(channels_to_remove.iter().copied()) - .and( - channel_path::Column::IdPath - .not_like(&format!("%/{}/%", channel_id)), - ), - ) - .stream(&*tx) - .await?; - while let Some(row) = channels_to_keep.next().await { - let row = row?; - channels_to_remove.remove(&row.channel_id); - } - } - - let channel_ancestors = self.get_channel_ancestors(channel_id, &*tx).await?; let members_to_notify: Vec = channel_member::Entity::find() - .filter(channel_member::Column::ChannelId.is_in(channel_ancestors)) + .filter(channel_member::Column::ChannelId.is_in(channel.ancestors_including_self())) .select_only() .column(channel_member::Column::UserId) .distinct() @@ -242,25 +276,19 @@ impl Database { .all(&*tx) .await?; + let channels_to_remove = self + .get_channel_descendants_including_self(vec![channel.id], &*tx) + .await? + .into_iter() + .map(|channel| channel.id) + .collect::>(); + channel::Entity::delete_many() .filter(channel::Column::Id.is_in(channels_to_remove.iter().copied())) .exec(&*tx) .await?; - // Delete any other paths that include this channel - let sql = r#" - DELETE FROM channel_paths - WHERE - id_path LIKE '%' || $1 || '%' - "#; - let channel_paths_stmt = Statement::from_sql_and_values( - self.pool.get_database_backend(), - sql, - [channel_id.to_proto().into()], - ); - tx.execute(channel_paths_stmt).await?; - - Ok((channels_to_remove.into_iter().collect(), members_to_notify)) + Ok((channels_to_remove, members_to_notify)) }) .await } @@ -271,16 +299,12 @@ impl Database { invitee_id: UserId, inviter_id: UserId, role: ChannelRole, - ) -> Result { + ) -> Result { self.transaction(move |tx| async move { - self.check_user_is_channel_admin(channel_id, inviter_id, &*tx) + let channel = self.get_channel_internal(channel_id, &*tx).await?; + self.check_user_is_channel_admin(&channel, inviter_id, &*tx) .await?; - let channel = channel::Entity::find_by_id(channel_id) - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("no such channel"))?; - channel_member::ActiveModel { id: ActiveValue::NotSet, channel_id: ActiveValue::Set(channel_id), @@ -291,12 +315,14 @@ impl Database { .insert(&*tx) .await?; - Ok(self + let channel = Channel::from_model(channel, role); + + let notifications = self .create_notification( invitee_id, rpc::Notification::ChannelInvitation { channel_id: channel_id.to_proto(), - channel_name: channel.name, + channel_name: channel.name.clone(), inviter_id: inviter_id.to_proto(), }, true, @@ -304,7 +330,12 @@ impl Database { ) .await? .into_iter() - .collect()) + .collect(); + + Ok(InviteMemberResult { + channel, + notifications, + }) }) .await } @@ -320,27 +351,36 @@ impl Database { pub async fn rename_channel( &self, channel_id: ChannelId, - user_id: UserId, + admin_id: UserId, new_name: &str, - ) -> Result { + ) -> Result { self.transaction(move |tx| async move { let new_name = Self::sanitize_channel_name(new_name)?.to_string(); - self.check_user_is_channel_admin(channel_id, user_id, &*tx) + let channel = self.get_channel_internal(channel_id, &*tx).await?; + let role = self + .check_user_is_channel_admin(&channel, admin_id, &*tx) .await?; - let channel = channel::ActiveModel { - id: ActiveValue::Unchanged(channel_id), - name: ActiveValue::Set(new_name.clone()), - ..Default::default() - } - .update(&*tx) - .await?; + let mut model = channel.into_active_model(); + model.name = ActiveValue::Set(new_name.clone()); + let channel = model.update(&*tx).await?; - Ok(Channel { - id: channel.id, - name: channel.name, - visibility: channel.visibility, + let participants = self + .get_channel_participant_details_internal(&channel, &*tx) + .await?; + + Ok(RenameChannelResult { + channel: Channel::from_model(channel.clone(), role), + participants_to_update: participants + .iter() + .map(|participant| { + ( + participant.user_id, + Channel::from_model(channel.clone(), participant.role), + ) + }) + .collect(), }) }) .await @@ -351,10 +391,12 @@ impl Database { channel_id: ChannelId, user_id: UserId, accept: bool, - ) -> Result { + ) -> Result { self.transaction(move |tx| async move { - let rows_affected = if accept { - channel_member::Entity::update_many() + let channel = self.get_channel_internal(channel_id, &*tx).await?; + + let membership_update = if accept { + let rows_affected = channel_member::Entity::update_many() .set(channel_member::ActiveModel { accepted: ActiveValue::Set(accept), ..Default::default() @@ -367,9 +409,18 @@ impl Database { ) .exec(&*tx) .await? - .rows_affected + .rows_affected; + + if rows_affected == 0 { + Err(anyhow!("no such invitation"))?; + } + + Some( + self.calculate_membership_updated(&channel, user_id, &*tx) + .await?, + ) } else { - channel_member::Entity::delete_many() + let rows_affected = channel_member::Entity::delete_many() .filter( channel_member::Column::ChannelId .eq(channel_id) @@ -378,39 +429,71 @@ impl Database { ) .exec(&*tx) .await? - .rows_affected + .rows_affected; + if rows_affected == 0 { + Err(anyhow!("no such invitation"))?; + } + + None }; - if rows_affected == 0 { - Err(anyhow!("no such invitation"))?; - } - - Ok(self - .mark_notification_as_read_with_response( - user_id, - &rpc::Notification::ChannelInvitation { - channel_id: channel_id.to_proto(), - channel_name: Default::default(), - inviter_id: Default::default(), - }, - accept, - &*tx, - ) - .await? - .into_iter() - .collect()) + Ok(RespondToChannelInvite { + membership_update, + notifications: self + .mark_notification_as_read_with_response( + user_id, + &rpc::Notification::ChannelInvitation { + channel_id: channel_id.to_proto(), + channel_name: Default::default(), + inviter_id: Default::default(), + }, + accept, + &*tx, + ) + .await? + .into_iter() + .collect(), + }) }) .await } + async fn calculate_membership_updated( + &self, + channel: &channel::Model, + user_id: UserId, + tx: &DatabaseTransaction, + ) -> Result { + let new_channels = self.get_user_channels(user_id, Some(channel), &*tx).await?; + let removed_channels = self + .get_channel_descendants_including_self(vec![channel.id], &*tx) + .await? + .into_iter() + .filter_map(|channel| { + if !new_channels.channels.iter().any(|c| c.id == channel.id) { + Some(channel.id) + } else { + None + } + }) + .collect::>(); + + Ok(MembershipUpdated { + channel_id: channel.id, + new_channels, + removed_channels, + }) + } + pub async fn remove_channel_member( &self, channel_id: ChannelId, member_id: UserId, admin_id: UserId, - ) -> Result> { + ) -> Result { self.transaction(|tx| async move { - self.check_user_is_channel_admin(channel_id, admin_id, &*tx) + let channel = self.get_channel_internal(channel_id, &*tx).await?; + self.check_user_is_channel_admin(&channel, admin_id, &*tx) .await?; let result = channel_member::Entity::delete_many() @@ -426,23 +509,30 @@ impl Database { Err(anyhow!("no such member"))?; } - Ok(self - .remove_notification( - member_id, - rpc::Notification::ChannelInvitation { - channel_id: channel_id.to_proto(), - channel_name: Default::default(), - inviter_id: Default::default(), - }, - &*tx, - ) - .await?) + Ok(RemoveChannelMemberResult { + membership_update: self + .calculate_membership_updated(&channel, member_id, &*tx) + .await?, + notification_id: self + .remove_notification( + member_id, + rpc::Notification::ChannelInvitation { + channel_id: channel_id.to_proto(), + channel_name: Default::default(), + inviter_id: Default::default(), + }, + &*tx, + ) + .await?, + }) }) .await } pub async fn get_channel_invites_for_user(&self, user_id: UserId) -> Result> { self.transaction(|tx| async move { + let mut role_for_channel: HashMap = HashMap::default(); + let channel_invites = channel_member::Entity::find() .filter( channel_member::Column::UserId @@ -452,23 +542,20 @@ impl Database { .all(&*tx) .await?; + for invite in channel_invites { + role_for_channel.insert(invite.channel_id, invite.role); + } + let channels = channel::Entity::find() - .filter( - channel::Column::Id.is_in( - channel_invites - .into_iter() - .map(|channel_member| channel_member.channel_id), - ), - ) + .filter(channel::Column::Id.is_in(role_for_channel.keys().copied())) .all(&*tx) .await?; let channels = channels .into_iter() - .map(|channel| Channel { - id: channel.id, - name: channel.name, - visibility: channel.visibility, + .filter_map(|channel| { + let role = *role_for_channel.get(&channel.id)?; + Some(Channel::from_model(channel, role)) }) .collect(); @@ -481,41 +568,7 @@ impl Database { self.transaction(|tx| async move { let tx = tx; - let channel_memberships = channel_member::Entity::find() - .filter( - channel_member::Column::UserId - .eq(user_id) - .and(channel_member::Column::Accepted.eq(true)), - ) - .all(&*tx) - .await?; - - self.get_user_channels(user_id, channel_memberships, &tx) - .await - }) - .await - } - - pub async fn get_channel_for_user( - &self, - channel_id: ChannelId, - user_id: UserId, - ) -> Result { - self.transaction(|tx| async move { - let tx = tx; - - let channel_membership = channel_member::Entity::find() - .filter( - channel_member::Column::UserId - .eq(user_id) - .and(channel_member::Column::ChannelId.eq(channel_id)) - .and(channel_member::Column::Accepted.eq(true)), - ) - .all(&*tx) - .await?; - - self.get_user_channels(user_id, channel_membership, &tx) - .await + self.get_user_channels(user_id, None, &tx).await }) .await } @@ -523,110 +576,77 @@ impl Database { pub async fn get_user_channels( &self, user_id: UserId, - channel_memberships: Vec, + ancestor_channel: Option<&channel::Model>, tx: &DatabaseTransaction, ) -> Result { - let mut edges = self - .get_channel_descendants(channel_memberships.iter().map(|m| m.channel_id), &*tx) + let channel_memberships = channel_member::Entity::find() + .filter( + channel_member::Column::UserId + .eq(user_id) + .and(channel_member::Column::Accepted.eq(true)), + ) + .all(&*tx) .await?; - let mut role_for_channel: HashMap = HashMap::default(); + let descendants = self + .get_channel_descendants_including_self( + channel_memberships.iter().map(|m| m.channel_id), + &*tx, + ) + .await?; + let mut roles_by_channel_id: HashMap = HashMap::default(); for membership in channel_memberships.iter() { - role_for_channel.insert(membership.channel_id, membership.role); + roles_by_channel_id.insert(membership.channel_id, membership.role); } - for ChannelEdge { - parent_id, - channel_id, - } in edges.iter() - { - let parent_id = ChannelId::from_proto(*parent_id); - let channel_id = ChannelId::from_proto(*channel_id); - debug_assert!(role_for_channel.get(&parent_id).is_some()); - let parent_role = role_for_channel[&parent_id]; - if let Some(existing_role) = role_for_channel.get(&channel_id) { - if existing_role.should_override(parent_role) { - continue; - } - } - role_for_channel.insert(channel_id, parent_role); - } + let mut visible_channel_ids: HashSet = HashSet::default(); - let mut channels: Vec = Vec::new(); - let mut channels_with_admin_privileges: HashSet = HashSet::default(); - let mut channels_to_remove: HashSet = HashSet::default(); + let channels: Vec = descendants + .into_iter() + .filter_map(|channel| { + let parent_role = channel + .parent_id() + .and_then(|parent_id| roles_by_channel_id.get(&parent_id)); - let mut rows = channel::Entity::find() - .filter(channel::Column::Id.is_in(role_for_channel.keys().copied())) - .stream(&*tx) - .await?; - - while let Some(row) = rows.next().await { - let channel = row?; - let role = role_for_channel[&channel.id]; - - if role == ChannelRole::Banned - || role == ChannelRole::Guest && channel.visibility != ChannelVisibility::Public - { - channels_to_remove.insert(channel.id.0 as u64); - continue; - } - - channels.push(Channel { - id: channel.id, - name: channel.name, - visibility: channel.visibility, - }); - - if role == ChannelRole::Admin { - channels_with_admin_privileges.insert(channel.id); - } - } - drop(rows); - - if !channels_to_remove.is_empty() { - // Note: this code assumes each channel has one parent. - // If there are multiple valid public paths to a channel, - // e.g. - // If both of these paths are present (* indicating public): - // - zed* -> projects -> vim* - // - zed* -> conrad -> public-projects* -> vim* - // Users would only see one of them (based on edge sort order) - let mut replacement_parent: HashMap = HashMap::default(); - for ChannelEdge { - parent_id, - channel_id, - } in edges.iter() - { - if channels_to_remove.contains(channel_id) { - replacement_parent.insert(*channel_id, *parent_id); - } - } - - let mut new_edges: Vec = Vec::new(); - 'outer: for ChannelEdge { - mut parent_id, - channel_id, - } in edges.iter() - { - if channels_to_remove.contains(channel_id) { - continue; - } - while channels_to_remove.contains(&parent_id) { - if let Some(new_parent_id) = replacement_parent.get(&parent_id) { - parent_id = *new_parent_id; + let role = if let Some(parent_role) = parent_role { + let role = if let Some(existing_role) = roles_by_channel_id.get(&channel.id) { + existing_role.max(*parent_role) } else { - continue 'outer; + *parent_role + }; + roles_by_channel_id.insert(channel.id, role); + role + } else { + *roles_by_channel_id.get(&channel.id)? + }; + + let can_see_parent_paths = role.can_see_all_descendants() + || role.can_only_see_public_descendants() + && channel.visibility == ChannelVisibility::Public; + if !can_see_parent_paths { + return None; + } + + visible_channel_ids.insert(channel.id); + + if let Some(ancestor) = ancestor_channel { + if !channel + .ancestors_including_self() + .any(|id| id == ancestor.id) + { + return None; } } - new_edges.push(ChannelEdge { - parent_id, - channel_id: *channel_id, - }) - } - edges = new_edges; - } + + let mut channel = Channel::from_model(channel, role); + channel + .parent_path + .retain(|id| visible_channel_ids.contains(&id)); + + Some(channel) + }) + .collect(); #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] enum QueryUserIdsAndChannelIds { @@ -661,17 +681,65 @@ impl Database { .await?; Ok(ChannelsForUser { - channels: ChannelGraph { channels, edges }, + channels, channel_participants, - channels_with_admin_privileges, unseen_buffer_changes: channel_buffer_changes, channel_messages: unseen_messages, }) } - pub async fn get_channel_members(&self, id: ChannelId) -> Result> { - self.transaction(|tx| async move { self.get_channel_participants_internal(id, &*tx).await }) - .await + async fn participants_to_notify_for_channel_change( + &self, + new_parent: &channel::Model, + tx: &DatabaseTransaction, + ) -> Result> { + let mut results: Vec<(UserId, ChannelsForUser)> = Vec::new(); + + let members = self + .get_channel_participant_details_internal(new_parent, &*tx) + .await?; + + for member in members.iter() { + if !member.role.can_see_all_descendants() { + continue; + } + results.push(( + member.user_id, + self.get_user_channels(member.user_id, Some(new_parent), &*tx) + .await?, + )) + } + + let public_parents = self + .public_ancestors_including_self(new_parent, &*tx) + .await?; + let public_parent = public_parents.last(); + + let Some(public_parent) = public_parent else { + return Ok(results); + }; + + // could save some time in the common case by skipping this if the + // new channel is not public and has no public descendants. + let public_members = if public_parent == new_parent { + members + } else { + self.get_channel_participant_details_internal(public_parent, &*tx) + .await? + }; + + for member in public_members { + if !member.role.can_only_see_public_descendants() { + continue; + }; + results.push(( + member.user_id, + self.get_user_channels(member.user_id, Some(public_parent), &*tx) + .await?, + )) + } + + Ok(results) } pub async fn set_channel_member_role( @@ -680,9 +748,10 @@ impl Database { admin_id: UserId, for_user: UserId, role: ChannelRole, - ) -> Result { + ) -> Result { self.transaction(|tx| async move { - self.check_user_is_channel_admin(channel_id, admin_id, &*tx) + let channel = self.get_channel_internal(channel_id, &*tx).await?; + self.check_user_is_channel_admin(&channel, admin_id, &*tx) .await?; let membership = channel_member::Entity::find() @@ -702,7 +771,16 @@ impl Database { update.role = ActiveValue::Set(role); let updated = channel_member::Entity::update(update).exec(&*tx).await?; - Ok(updated) + if updated.accepted { + Ok(SetMemberRoleResult::MembershipUpdated( + self.calculate_membership_updated(&channel, for_user, &*tx) + .await?, + )) + } else { + Ok(SetMemberRoleResult::InviteUpdated(Channel::from_model( + channel, role, + ))) + } }) .await } @@ -712,146 +790,149 @@ impl Database { channel_id: ChannelId, user_id: UserId, ) -> Result> { - self.transaction(|tx| async move { - let user_role = self - .check_user_is_channel_member(channel_id, user_id, &*tx) - .await?; + let (role, members) = self + .transaction(move |tx| async move { + let channel = self.get_channel_internal(channel_id, &*tx).await?; + let role = self + .check_user_is_channel_participant(&channel, user_id, &*tx) + .await?; + Ok(( + role, + self.get_channel_participant_details_internal(&channel, &*tx) + .await?, + )) + }) + .await?; - let channel_visibility = channel::Entity::find() - .filter(channel::Column::Id.eq(channel_id)) - .one(&*tx) - .await? - .map(|channel| channel.visibility) - .unwrap_or(ChannelVisibility::Members); - - #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] - enum QueryMemberDetails { - UserId, - Role, - IsDirectMember, - Accepted, - Visibility, - } - - let tx = tx; - let ancestor_ids = self.get_channel_ancestors(channel_id, &*tx).await?; - let mut stream = channel_member::Entity::find() - .left_join(channel::Entity) - .filter(channel_member::Column::ChannelId.is_in(ancestor_ids.iter().copied())) - .select_only() - .column(channel_member::Column::UserId) - .column(channel_member::Column::Role) - .column_as( - channel_member::Column::ChannelId.eq(channel_id), - QueryMemberDetails::IsDirectMember, - ) - .column(channel_member::Column::Accepted) - .column(channel::Column::Visibility) - .into_values::<_, QueryMemberDetails>() - .stream(&*tx) - .await?; - - struct UserDetail { - kind: Kind, - channel_role: ChannelRole, - } - let mut user_details: HashMap = HashMap::default(); - - while let Some(user_membership) = stream.next().await { - let (user_id, channel_role, is_direct_member, is_invite_accepted, visibility): ( - UserId, - ChannelRole, - bool, - bool, - ChannelVisibility, - ) = user_membership?; - let kind = match (is_direct_member, is_invite_accepted) { - (true, true) => proto::channel_member::Kind::Member, - (true, false) => proto::channel_member::Kind::Invitee, - (false, true) => proto::channel_member::Kind::AncestorMember, - (false, false) => continue, - }; - - if channel_role == ChannelRole::Guest - && visibility != ChannelVisibility::Public - && channel_visibility != ChannelVisibility::Public - { - continue; - } - - if let Some(details_mut) = user_details.get_mut(&user_id) { - if channel_role.should_override(details_mut.channel_role) { - details_mut.channel_role = channel_role; - } - if kind == Kind::Member { - details_mut.kind = kind; - // the UI is going to be a bit confusing if you already have permissions - // that are greater than or equal to the ones you're being invited to. - } else if kind == Kind::Invitee && details_mut.kind == Kind::AncestorMember { - details_mut.kind = kind; - } - } else { - user_details.insert(user_id, UserDetail { kind, channel_role }); - } - } - - Ok(user_details + if role == ChannelRole::Admin { + Ok(members .into_iter() - .filter_map(|(user_id, mut details)| { - // If the user is not an admin, don't give them as much - // information about the other members. - if user_role != ChannelRole::Admin { - if details.kind == Kind::Invitee - || details.channel_role == ChannelRole::Banned - { - return None; - } - - if details.channel_role == ChannelRole::Admin { - details.channel_role = ChannelRole::Member; - } + .map(|channel_member| channel_member.to_proto()) + .collect()) + } else { + return Ok(members + .into_iter() + .filter_map(|member| { + if member.kind == proto::channel_member::Kind::Invitee { + return None; } - - Some(proto::ChannelMember { - user_id: user_id.to_proto(), - kind: details.kind.into(), - role: details.channel_role.into(), + Some(ChannelMember { + role: member.role, + user_id: member.user_id, + kind: proto::channel_member::Kind::Member, }) }) - .collect()) - }) - .await + .map(|channel_member| channel_member.to_proto()) + .collect()); + } } - pub async fn get_channel_participants_internal( + async fn get_channel_participant_details_internal( &self, - id: ChannelId, + channel: &channel::Model, tx: &DatabaseTransaction, - ) -> Result> { - let ancestor_ids = self.get_channel_ancestors(id, tx).await?; - let user_ids = channel_member::Entity::find() - .distinct() - .filter( - channel_member::Column::ChannelId - .is_in(ancestor_ids.iter().copied()) - .and(channel_member::Column::Accepted.eq(true)), - ) + ) -> Result> { + #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] + enum QueryMemberDetails { + UserId, + Role, + IsDirectMember, + Accepted, + Visibility, + } + + let mut stream = channel_member::Entity::find() + .left_join(channel::Entity) + .filter(channel_member::Column::ChannelId.is_in(channel.ancestors_including_self())) .select_only() .column(channel_member::Column::UserId) - .into_values::<_, QueryUserIds>() - .all(&*tx) + .column(channel_member::Column::Role) + .column_as( + channel_member::Column::ChannelId.eq(channel.id), + QueryMemberDetails::IsDirectMember, + ) + .column(channel_member::Column::Accepted) + .column(channel::Column::Visibility) + .into_values::<_, QueryMemberDetails>() + .stream(&*tx) .await?; - Ok(user_ids) + + let mut user_details: HashMap = HashMap::default(); + + while let Some(user_membership) = stream.next().await { + let (user_id, channel_role, is_direct_member, is_invite_accepted, visibility): ( + UserId, + ChannelRole, + bool, + bool, + ChannelVisibility, + ) = user_membership?; + let kind = match (is_direct_member, is_invite_accepted) { + (true, true) => proto::channel_member::Kind::Member, + (true, false) => proto::channel_member::Kind::Invitee, + (false, true) => proto::channel_member::Kind::AncestorMember, + (false, false) => continue, + }; + + if channel_role == ChannelRole::Guest + && visibility != ChannelVisibility::Public + && channel.visibility != ChannelVisibility::Public + { + continue; + } + + if let Some(details_mut) = user_details.get_mut(&user_id) { + if channel_role.should_override(details_mut.role) { + details_mut.role = channel_role; + } + if kind == Kind::Member { + details_mut.kind = kind; + // the UI is going to be a bit confusing if you already have permissions + // that are greater than or equal to the ones you're being invited to. + } else if kind == Kind::Invitee && details_mut.kind == Kind::AncestorMember { + details_mut.kind = kind; + } + } else { + user_details.insert( + user_id, + ChannelMember { + user_id, + kind, + role: channel_role, + }, + ); + } + } + + Ok(user_details + .into_iter() + .map(|(_, details)| details) + .collect()) + } + + pub async fn get_channel_participants( + &self, + channel: &channel::Model, + tx: &DatabaseTransaction, + ) -> Result> { + let participants = self + .get_channel_participant_details_internal(channel, &*tx) + .await?; + Ok(participants + .into_iter() + .map(|member| member.user_id) + .collect()) } pub async fn check_user_is_channel_admin( &self, - channel_id: ChannelId, + channel: &channel::Model, user_id: UserId, tx: &DatabaseTransaction, - ) -> Result<()> { - match self.channel_role_for_user(channel_id, user_id, tx).await? { - Some(ChannelRole::Admin) => Ok(()), + ) -> Result { + let role = self.channel_role_for_user(channel, user_id, tx).await?; + match role { + Some(ChannelRole::Admin) => Ok(role.unwrap()), Some(ChannelRole::Member) | Some(ChannelRole::Banned) | Some(ChannelRole::Guest) @@ -863,11 +944,11 @@ impl Database { pub async fn check_user_is_channel_member( &self, - channel_id: ChannelId, + channel: &channel::Model, user_id: UserId, tx: &DatabaseTransaction, ) -> Result { - let channel_role = self.channel_role_for_user(channel_id, user_id, tx).await?; + let channel_role = self.channel_role_for_user(channel, user_id, tx).await?; match channel_role { Some(ChannelRole::Admin) | Some(ChannelRole::Member) => Ok(channel_role.unwrap()), Some(ChannelRole::Banned) | Some(ChannelRole::Guest) | None => Err(anyhow!( @@ -878,13 +959,14 @@ impl Database { pub async fn check_user_is_channel_participant( &self, - channel_id: ChannelId, + channel: &channel::Model, user_id: UserId, tx: &DatabaseTransaction, - ) -> Result<()> { - match self.channel_role_for_user(channel_id, user_id, tx).await? { + ) -> Result { + let role = self.channel_role_for_user(channel, user_id, tx).await?; + match role { Some(ChannelRole::Admin) | Some(ChannelRole::Member) | Some(ChannelRole::Guest) => { - Ok(()) + Ok(role.unwrap()) } Some(ChannelRole::Banned) | None => Err(anyhow!( "user is not a channel participant or channel does not exist" @@ -894,14 +976,12 @@ impl Database { pub async fn pending_invite_for_channel( &self, - channel_id: ChannelId, + channel: &channel::Model, user_id: UserId, tx: &DatabaseTransaction, ) -> Result> { - let channel_ids = self.get_channel_ancestors(channel_id, tx).await?; - let row = channel_member::Entity::find() - .filter(channel_member::Column::ChannelId.is_in(channel_ids)) + .filter(channel_member::Column::ChannelId.is_in(channel.ancestors_including_self())) .filter(channel_member::Column::UserId.eq(user_id)) .filter(channel_member::Column::Accepted.eq(false)) .one(&*tx) @@ -910,58 +990,39 @@ impl Database { Ok(row) } - pub async fn most_public_ancestor_for_channel( + pub async fn public_parent_channel( &self, - channel_id: ChannelId, + channel: &channel::Model, tx: &DatabaseTransaction, - ) -> Result> { - // Note: if there are many paths to a channel, this will return just one - let arbitary_path = channel_path::Entity::find() - .filter(channel_path::Column::ChannelId.eq(channel_id)) - .order_by(channel_path::Column::IdPath, sea_orm::Order::Desc) - .one(tx) - .await?; + ) -> Result> { + let mut path = self.public_ancestors_including_self(channel, &*tx).await?; + if path.last().unwrap().id == channel.id { + path.pop(); + } + Ok(path.pop()) + } - let Some(path) = arbitary_path else { - return Ok(None); - }; - - let ancestor_ids: Vec = path - .id_path - .trim_matches('/') - .split('/') - .map(|id| ChannelId::from_proto(id.parse().unwrap())) - .collect(); - - let rows = channel::Entity::find() - .filter(channel::Column::Id.is_in(ancestor_ids.iter().copied())) + pub async fn public_ancestors_including_self( + &self, + channel: &channel::Model, + tx: &DatabaseTransaction, + ) -> Result> { + let visible_channels = channel::Entity::find() + .filter(channel::Column::Id.is_in(channel.ancestors_including_self())) .filter(channel::Column::Visibility.eq(ChannelVisibility::Public)) + .order_by_asc(channel::Column::ParentPath) .all(&*tx) .await?; - let mut visible_channels: HashSet = HashSet::default(); - - for row in rows { - visible_channels.insert(row.id); - } - - for ancestor in ancestor_ids { - if visible_channels.contains(&ancestor) { - return Ok(Some(ancestor)); - } - } - - Ok(None) + Ok(visible_channels) } pub async fn channel_role_for_user( &self, - channel_id: ChannelId, + channel: &channel::Model, user_id: UserId, tx: &DatabaseTransaction, ) -> Result> { - let channel_ids = self.get_channel_ancestors(channel_id, tx).await?; - #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] enum QueryChannelMembership { ChannelId, @@ -973,7 +1034,7 @@ impl Database { .left_join(channel::Entity) .filter( channel_member::Column::ChannelId - .is_in(channel_ids) + .is_in(channel.ancestors_including_self()) .and(channel_member::Column::UserId.eq(user_id)) .and(channel_member::Column::Accepted.eq(true)), ) @@ -1012,7 +1073,7 @@ impl Database { } ChannelRole::Guest => {} } - if channel_id == membership_channel { + if channel.id == membership_channel { current_channel_visibility = Some(visibility); } } @@ -1022,7 +1083,7 @@ impl Database { if is_participant && user_role.is_none() { if current_channel_visibility.is_none() { current_channel_visibility = channel::Entity::find() - .filter(channel::Column::Id.eq(channel_id)) + .filter(channel::Column::Id.eq(channel.id)) .one(&*tx) .await? .map(|channel| channel.visibility); @@ -1035,39 +1096,13 @@ impl Database { Ok(user_role) } - /// Returns the channel ancestors in arbitrary order - pub async fn get_channel_ancestors( - &self, - channel_id: ChannelId, - tx: &DatabaseTransaction, - ) -> Result> { - let paths = channel_path::Entity::find() - .filter(channel_path::Column::ChannelId.eq(channel_id)) - .order_by(channel_path::Column::IdPath, sea_orm::Order::Desc) - .all(tx) - .await?; - let mut channel_ids = Vec::new(); - for path in paths { - for id in path.id_path.trim_matches('/').split('/') { - if let Ok(id) = id.parse() { - let id = ChannelId::from_proto(id); - if let Err(ix) = channel_ids.binary_search(&id) { - channel_ids.insert(ix, id); - } - } - } - } - Ok(channel_ids) - } - - // Returns the channel desendants as a sorted list of edges for further processing. - // The edges are sorted such that you will see unknown channel ids as children - // before you see them as parents. - async fn get_channel_descendants( + // Get the descendants of the given set if channels, ordered by their + // path. + async fn get_channel_descendants_including_self( &self, channel_ids: impl IntoIterator, tx: &DatabaseTransaction, - ) -> Result> { + ) -> Result> { let mut values = String::new(); for id in channel_ids { if !values.is_empty() { @@ -1082,63 +1117,55 @@ impl Database { let sql = format!( r#" - SELECT - descendant_paths.* + SELECT DISTINCT + descendant_channels.*, + descendant_channels.parent_path || descendant_channels.id as full_path FROM - channel_paths parent_paths, channel_paths descendant_paths + channels parent_channels, channels descendant_channels WHERE - parent_paths.channel_id IN ({values}) AND - descendant_paths.id_path != parent_paths.id_path AND - descendant_paths.id_path LIKE (parent_paths.id_path || '%') + descendant_channels.id IN ({values}) OR + ( + parent_channels.id IN ({values}) AND + descendant_channels.parent_path LIKE (parent_channels.parent_path || parent_channels.id || '/%') + ) ORDER BY - descendant_paths.id_path - "# + full_path ASC + "# ); - let stmt = Statement::from_string(self.pool.get_database_backend(), sql); - - let mut paths = channel_path::Entity::find() - .from_raw_sql(stmt) - .stream(tx) - .await?; - - let mut results: Vec = Vec::new(); - while let Some(path) = paths.next().await { - let path = path?; - let ids: Vec<&str> = path.id_path.trim_matches('/').split('/').collect(); - - debug_assert!(ids.len() >= 2); - debug_assert!(ids[ids.len() - 1] == path.channel_id.to_string()); - - results.push(ChannelEdge { - parent_id: ids[ids.len() - 2].parse().unwrap(), - channel_id: ids[ids.len() - 1].parse().unwrap(), - }) - } - - Ok(results) + Ok(channel::Entity::find() + .from_raw_sql(Statement::from_string( + self.pool.get_database_backend(), + sql, + )) + .all(tx) + .await?) } /// Returns the channel with the given ID pub async fn get_channel(&self, channel_id: ChannelId, user_id: UserId) -> Result { self.transaction(|tx| async move { - self.check_user_is_channel_participant(channel_id, user_id, &*tx) + let channel = self.get_channel_internal(channel_id, &*tx).await?; + let role = self + .check_user_is_channel_participant(&channel, user_id, &*tx) .await?; - let channel = channel::Entity::find_by_id(channel_id).one(&*tx).await?; - let Some(channel) = channel else { - Err(anyhow!("no such channel"))? - }; - - Ok(Channel { - id: channel.id, - visibility: channel.visibility, - name: channel.name, - }) + Ok(Channel::from_model(channel, role)) }) .await } + pub async fn get_channel_internal( + &self, + channel_id: ChannelId, + tx: &DatabaseTransaction, + ) -> Result { + Ok(channel::Entity::find_by_id(channel_id) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("no such channel"))?) + } + pub(crate) async fn get_or_create_channel_room( &self, channel_id: ChannelId, @@ -1174,246 +1201,112 @@ impl Database { Ok(room_id) } - // Insert an edge from the given channel to the given other channel. - pub async fn link_channel( - &self, - user: UserId, - channel: ChannelId, - to: ChannelId, - ) -> Result { - self.transaction(|tx| async move { - // Note that even with these maxed permissions, this linking operation - // is still insecure because you can't remove someone's permissions to a - // channel if they've linked the channel to one where they're an admin. - self.check_user_is_channel_admin(channel, user, &*tx) - .await?; - - self.link_channel_internal(user, channel, to, &*tx).await - }) - .await - } - - pub async fn link_channel_internal( - &self, - user: UserId, - channel: ChannelId, - new_parent: ChannelId, - tx: &DatabaseTransaction, - ) -> Result { - self.check_user_is_channel_admin(new_parent, user, &*tx) - .await?; - - let paths = channel_path::Entity::find() - .filter(channel_path::Column::IdPath.like(&format!("%/{}/%", channel))) - .all(tx) - .await?; - - let mut new_path_suffixes = HashSet::default(); - for path in paths { - if let Some(start_offset) = path.id_path.find(&format!("/{}/", channel)) { - new_path_suffixes.insert(( - path.channel_id, - path.id_path[(start_offset + 1)..].to_string(), - )); - } - } - - let paths_to_new_parent = channel_path::Entity::find() - .filter(channel_path::Column::ChannelId.eq(new_parent)) - .all(tx) - .await?; - - let mut new_paths = Vec::new(); - for path in paths_to_new_parent { - if path.id_path.contains(&format!("/{}/", channel)) { - Err(anyhow!("cycle"))?; - } - - new_paths.extend(new_path_suffixes.iter().map(|(channel_id, path_suffix)| { - channel_path::ActiveModel { - channel_id: ActiveValue::Set(*channel_id), - id_path: ActiveValue::Set(format!("{}{}", &path.id_path, path_suffix)), - } - })); - } - - channel_path::Entity::insert_many(new_paths) - .exec(&*tx) - .await?; - - // remove any root edges for the channel we just linked - { - channel_path::Entity::delete_many() - .filter(channel_path::Column::IdPath.like(&format!("/{}/%", channel))) - .exec(&*tx) - .await?; - } - - let membership = channel_member::Entity::find() - .filter( - channel_member::Column::ChannelId - .eq(channel) - .and(channel_member::Column::UserId.eq(user)), - ) - .all(tx) - .await?; - - let mut channel_info = self.get_user_channels(user, membership, &*tx).await?; - - channel_info.channels.edges.push(ChannelEdge { - channel_id: channel.to_proto(), - parent_id: new_parent.to_proto(), - }); - - Ok(channel_info.channels) - } - - /// Unlink a channel from a given parent. This will add in a root edge if - /// the channel has no other parents after this operation. - pub async fn unlink_channel( - &self, - user: UserId, - channel: ChannelId, - from: ChannelId, - ) -> Result<()> { - self.transaction(|tx| async move { - // Note that even with these maxed permissions, this linking operation - // is still insecure because you can't remove someone's permissions to a - // channel if they've linked the channel to one where they're an admin. - self.check_user_is_channel_admin(channel, user, &*tx) - .await?; - - self.unlink_channel_internal(user, channel, from, &*tx) - .await?; - - Ok(()) - }) - .await - } - - pub async fn unlink_channel_internal( - &self, - user: UserId, - channel: ChannelId, - from: ChannelId, - tx: &DatabaseTransaction, - ) -> Result<()> { - self.check_user_is_channel_admin(from, user, &*tx).await?; - - let sql = r#" - DELETE FROM channel_paths - WHERE - id_path LIKE '%/' || $1 || '/' || $2 || '/%' - RETURNING id_path, channel_id - "#; - - let paths = channel_path::Entity::find() - .from_raw_sql(Statement::from_sql_and_values( - self.pool.get_database_backend(), - sql, - [from.to_proto().into(), channel.to_proto().into()], - )) - .all(&*tx) - .await?; - - let is_stranded = channel_path::Entity::find() - .filter(channel_path::Column::ChannelId.eq(channel)) - .count(&*tx) - .await? - == 0; - - // Make sure that there is always at least one path to the channel - if is_stranded { - let root_paths: Vec<_> = paths - .iter() - .map(|path| { - let start_offset = path.id_path.find(&format!("/{}/", channel)).unwrap(); - channel_path::ActiveModel { - channel_id: ActiveValue::Set(path.channel_id), - id_path: ActiveValue::Set(path.id_path[start_offset..].to_string()), - } - }) - .collect(); - channel_path::Entity::insert_many(root_paths) - .exec(&*tx) - .await?; - } - - Ok(()) - } - - /// Move a channel from one parent to another, returns the - /// Channels that were moved for notifying clients + /// Move a channel from one parent to another pub async fn move_channel( &self, - user: UserId, - channel: ChannelId, - from: ChannelId, - to: ChannelId, - ) -> Result { - if from == to { - return Ok(ChannelGraph { - channels: vec![], - edges: vec![], - }); - } - + channel_id: ChannelId, + new_parent_id: Option, + admin_id: UserId, + ) -> Result> { self.transaction(|tx| async move { - self.check_user_is_channel_admin(channel, user, &*tx) + let channel = self.get_channel_internal(channel_id, &*tx).await?; + self.check_user_is_channel_admin(&channel, admin_id, &*tx) .await?; - let moved_channels = self.link_channel_internal(user, channel, to, &*tx).await?; + let new_parent_path; + let new_parent_channel; + if let Some(new_parent_id) = new_parent_id { + let new_parent = self.get_channel_internal(new_parent_id, &*tx).await?; + self.check_user_is_channel_admin(&new_parent, admin_id, &*tx) + .await?; - self.unlink_channel_internal(user, channel, from, &*tx) + new_parent_path = new_parent.path(); + new_parent_channel = Some(new_parent); + } else { + new_parent_path = String::new(); + new_parent_channel = None; + }; + + let previous_participants = self + .get_channel_participant_details_internal(&channel, &*tx) .await?; - Ok(moved_channels) + let old_path = format!("{}{}/", channel.parent_path, channel.id); + let new_path = format!("{}{}/", new_parent_path, channel.id); + + if old_path == new_path { + return Ok(None); + } + + let mut model = channel.into_active_model(); + model.parent_path = ActiveValue::Set(new_parent_path); + let channel = model.update(&*tx).await?; + + if new_parent_channel.is_none() { + channel_member::ActiveModel { + id: ActiveValue::NotSet, + channel_id: ActiveValue::Set(channel_id), + user_id: ActiveValue::Set(admin_id), + accepted: ActiveValue::Set(true), + role: ActiveValue::Set(ChannelRole::Admin), + } + .insert(&*tx) + .await?; + } + + let descendent_ids = + ChannelId::find_by_statement::(Statement::from_sql_and_values( + self.pool.get_database_backend(), + " + UPDATE channels SET parent_path = REPLACE(parent_path, $1, $2) + WHERE parent_path LIKE $3 || '%' + RETURNING id + ", + [old_path.clone().into(), new_path.into(), old_path.into()], + )) + .all(&*tx) + .await?; + + let participants_to_update: HashMap<_, _> = self + .participants_to_notify_for_channel_change( + new_parent_channel.as_ref().unwrap_or(&channel), + &*tx, + ) + .await? + .into_iter() + .collect(); + + let mut moved_channels: HashSet = HashSet::default(); + for id in descendent_ids { + moved_channels.insert(id); + } + moved_channels.insert(channel_id); + + let mut participants_to_remove: HashSet = HashSet::default(); + for participant in previous_participants { + if participant.kind == proto::channel_member::Kind::AncestorMember { + if !participants_to_update.contains_key(&participant.user_id) { + participants_to_remove.insert(participant.user_id); + } + } + } + + Ok(Some(MoveChannelResult { + participants_to_remove, + participants_to_update, + moved_channels, + })) }) .await } } +#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] +enum QueryIds { + Id, +} + #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] enum QueryUserIds { UserId, } - -#[derive(Debug)] -pub struct ChannelGraph { - pub channels: Vec, - pub edges: Vec, -} - -impl ChannelGraph { - pub fn is_empty(&self) -> bool { - self.channels.is_empty() && self.edges.is_empty() - } -} - -#[cfg(test)] -impl PartialEq for ChannelGraph { - fn eq(&self, other: &Self) -> bool { - // Order independent comparison for tests - let channels_set = self.channels.iter().collect::>(); - let other_channels_set = other.channels.iter().collect::>(); - let edges_set = self - .edges - .iter() - .map(|edge| (edge.channel_id, edge.parent_id)) - .collect::>(); - let other_edges_set = other - .edges - .iter() - .map(|edge| (edge.channel_id, edge.parent_id)) - .collect::>(); - - channels_set == other_channels_set && edges_set == other_edges_set - } -} - -#[cfg(not(test))] -impl PartialEq for ChannelGraph { - fn eq(&self, other: &Self) -> bool { - self.channels == other.channels && self.edges == other.edges - } -} diff --git a/crates/collab/src/db/queries/messages.rs b/crates/collab/src/db/queries/messages.rs index d406cbb091..47bb27df39 100644 --- a/crates/collab/src/db/queries/messages.rs +++ b/crates/collab/src/db/queries/messages.rs @@ -1,5 +1,4 @@ use super::*; -use futures::Stream; use rpc::Notification; use sea_orm::TryInsertResult; use time::OffsetDateTime; @@ -12,7 +11,8 @@ impl Database { user_id: UserId, ) -> Result<()> { self.transaction(|tx| async move { - self.check_user_is_channel_member(channel_id, user_id, &*tx) + let channel = self.get_channel_internal(channel_id, &*tx).await?; + self.check_user_is_channel_participant(&channel, user_id, &*tx) .await?; channel_chat_participant::ActiveModel { id: ActiveValue::NotSet, @@ -80,7 +80,8 @@ impl Database { before_message_id: Option, ) -> Result> { self.transaction(|tx| async move { - self.check_user_is_channel_member(channel_id, user_id, &*tx) + let channel = self.get_channel_internal(channel_id, &*tx).await?; + self.check_user_is_channel_participant(&channel, user_id, &*tx) .await?; let mut condition = @@ -94,7 +95,7 @@ impl Database { .filter(condition) .order_by_desc(channel_message::Column::Id) .limit(count as u64) - .stream(&*tx) + .all(&*tx) .await?; self.load_channel_messages(rows, &*tx).await @@ -111,27 +112,23 @@ impl Database { let rows = channel_message::Entity::find() .filter(channel_message::Column::Id.is_in(message_ids.iter().copied())) .order_by_desc(channel_message::Column::Id) - .stream(&*tx) + .all(&*tx) .await?; - let mut channel_ids = HashSet::::default(); - let messages = self - .load_channel_messages( - rows.map(|row| { - row.map(|row| { - channel_ids.insert(row.channel_id); - row - }) - }), - &*tx, - ) - .await?; + let mut channels = HashMap::::default(); + for row in &rows { + channels.insert( + row.channel_id, + self.get_channel_internal(row.channel_id, &*tx).await?, + ); + } - for channel_id in channel_ids { - self.check_user_is_channel_member(channel_id, user_id, &*tx) + for (_, channel) in channels { + self.check_user_is_channel_participant(&channel, user_id, &*tx) .await?; } + let messages = self.load_channel_messages(rows, &*tx).await?; Ok(messages) }) .await @@ -139,26 +136,26 @@ impl Database { async fn load_channel_messages( &self, - mut rows: impl Send + Unpin + Stream>, + rows: Vec, tx: &DatabaseTransaction, ) -> Result> { - let mut messages = Vec::new(); - while let Some(row) = rows.next().await { - let row = row?; - let nonce = row.nonce.as_u64_pair(); - messages.push(proto::ChannelMessage { - id: row.id.to_proto(), - sender_id: row.sender_id.to_proto(), - body: row.body, - timestamp: row.sent_at.assume_utc().unix_timestamp() as u64, - mentions: vec![], - nonce: Some(proto::Nonce { - upper_half: nonce.0, - lower_half: nonce.1, - }), - }); - } - drop(rows); + let mut messages = rows + .into_iter() + .map(|row| { + let nonce = row.nonce.as_u64_pair(); + proto::ChannelMessage { + id: row.id.to_proto(), + sender_id: row.sender_id.to_proto(), + body: row.body, + timestamp: row.sent_at.assume_utc().unix_timestamp() as u64, + mentions: vec![], + nonce: Some(proto::Nonce { + upper_half: nonce.0, + lower_half: nonce.1, + }), + } + }) + .collect::>(); messages.reverse(); let mut mentions = channel_message_mention::Entity::find() @@ -203,6 +200,10 @@ impl Database { nonce: u128, ) -> Result { self.transaction(|tx| async move { + let channel = self.get_channel_internal(channel_id, &*tx).await?; + self.check_user_is_channel_participant(&channel, user_id, &*tx) + .await?; + let mut rows = channel_chat_participant::Entity::find() .filter(channel_chat_participant::Column::ChannelId.eq(channel_id)) .stream(&*tx) @@ -307,9 +308,7 @@ impl Database { } } - let mut channel_members = self - .get_channel_participants_internal(channel_id, &*tx) - .await?; + let mut channel_members = self.get_channel_participants(&channel, &*tx).await?; channel_members.retain(|member| !participant_user_ids.contains(member)); Ok(CreatedChannelMessage { @@ -482,8 +481,9 @@ impl Database { .await?; if result.rows_affected == 0 { + let channel = self.get_channel_internal(channel_id, &*tx).await?; if self - .check_user_is_channel_admin(channel_id, user_id, &*tx) + .check_user_is_channel_admin(&channel, user_id, &*tx) .await .is_ok() { diff --git a/crates/collab/src/db/queries/rooms.rs b/crates/collab/src/db/queries/rooms.rs index d2120495b0..40fdf5d58f 100644 --- a/crates/collab/src/db/queries/rooms.rs +++ b/crates/collab/src/db/queries/rooms.rs @@ -50,12 +50,10 @@ impl Database { .map(|participant| participant.user_id), ); - let (channel_id, room) = self.get_channel_room(room_id, &tx).await?; + let (channel, room) = self.get_channel_room(room_id, &tx).await?; let channel_members; - if let Some(channel_id) = channel_id { - channel_members = self - .get_channel_participants_internal(channel_id, &tx) - .await?; + if let Some(channel) = &channel { + channel_members = self.get_channel_participants(channel, &tx).await?; } else { channel_members = Vec::new(); @@ -71,7 +69,7 @@ impl Database { Ok(RefreshedRoom { room, - channel_id, + channel_id: channel.map(|channel| channel.id), channel_members, stale_participant_user_ids, canceled_calls_to_user_ids, @@ -383,7 +381,6 @@ impl Database { pub(crate) async fn join_channel_room_internal( &self, - channel_id: ChannelId, room_id: RoomId, user_id: UserId, connection: ConnectionId, @@ -422,13 +419,12 @@ impl Database { .exec(&*tx) .await?; - let room = self.get_room(room_id, &tx).await?; - let channel_members = self - .get_channel_participants_internal(channel_id, &tx) - .await?; + let (channel, room) = self.get_channel_room(room_id, &tx).await?; + let channel = channel.ok_or_else(|| anyhow!("no channel for room"))?; + let channel_members = self.get_channel_participants(&channel, &*tx).await?; Ok(JoinRoom { room, - channel_id: Some(channel_id), + channel_id: Some(channel.id), channel_members, }) } @@ -722,17 +718,16 @@ impl Database { }); } - let (channel_id, room) = self.get_channel_room(room_id, &tx).await?; - let channel_members = if let Some(channel_id) = channel_id { - self.get_channel_participants_internal(channel_id, &tx) - .await? + let (channel, room) = self.get_channel_room(room_id, &tx).await?; + let channel_members = if let Some(channel) = &channel { + self.get_channel_participants(&channel, &tx).await? } else { Vec::new() }; Ok(RejoinedRoom { room, - channel_id, + channel_id: channel.map(|channel| channel.id), channel_members, rejoined_projects, reshared_projects, @@ -874,7 +869,7 @@ impl Database { .exec(&*tx) .await?; - let (channel_id, room) = self.get_channel_room(room_id, &tx).await?; + let (channel, room) = self.get_channel_room(room_id, &tx).await?; let deleted = if room.participants.is_empty() { let result = room::Entity::delete_by_id(room_id).exec(&*tx).await?; result.rows_affected > 0 @@ -882,15 +877,14 @@ impl Database { false }; - let channel_members = if let Some(channel_id) = channel_id { - self.get_channel_participants_internal(channel_id, &tx) - .await? + let channel_members = if let Some(channel) = &channel { + self.get_channel_participants(channel, &tx).await? } else { Vec::new() }; let left_room = LeftRoom { room, - channel_id, + channel_id: channel.map(|channel| channel.id), channel_members, left_projects, canceled_calls_to_user_ids, @@ -1078,7 +1072,7 @@ impl Database { &self, room_id: RoomId, tx: &DatabaseTransaction, - ) -> Result<(Option, proto::Room)> { + ) -> Result<(Option, proto::Room)> { let db_room = room::Entity::find_by_id(room_id) .one(tx) .await? @@ -1187,9 +1181,16 @@ impl Database { project_id: db_follower.project_id.to_proto(), }); } + drop(db_followers); + + let channel = if let Some(channel_id) = db_room.channel_id { + Some(self.get_channel_internal(channel_id, &*tx).await?) + } else { + None + }; Ok(( - db_room.channel_id, + channel, proto::Room { id: db_room.id.to_proto(), live_kit_room: db_room.live_kit_room, diff --git a/crates/collab/src/db/tables.rs b/crates/collab/src/db/tables.rs index 0acb266d9d..4f28ce4fbd 100644 --- a/crates/collab/src/db/tables.rs +++ b/crates/collab/src/db/tables.rs @@ -8,7 +8,6 @@ pub mod channel_chat_participant; pub mod channel_member; pub mod channel_message; pub mod channel_message_mention; -pub mod channel_path; pub mod contact; pub mod feature_flag; pub mod follower; diff --git a/crates/collab/src/db/tables/channel.rs b/crates/collab/src/db/tables/channel.rs index 0975a8cc30..e30ec9af61 100644 --- a/crates/collab/src/db/tables/channel.rs +++ b/crates/collab/src/db/tables/channel.rs @@ -8,6 +8,28 @@ pub struct Model { pub id: ChannelId, pub name: String, pub visibility: ChannelVisibility, + pub parent_path: String, +} + +impl Model { + pub fn parent_id(&self) -> Option { + self.ancestors().last() + } + + pub fn ancestors(&self) -> impl Iterator + '_ { + self.parent_path + .trim_end_matches('/') + .split('/') + .filter_map(|id| Some(ChannelId::from_proto(id.parse().ok()?))) + } + + pub fn ancestors_including_self(&self) -> impl Iterator + '_ { + self.ancestors().chain(Some(self.id)) + } + + pub fn path(&self) -> String { + format!("{}{}/", self.parent_path, self.id) + } } impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab/src/db/tables/channel_path.rs b/crates/collab/src/db/tables/channel_path.rs deleted file mode 100644 index 323f116dae..0000000000 --- a/crates/collab/src/db/tables/channel_path.rs +++ /dev/null @@ -1,15 +0,0 @@ -use crate::db::ChannelId; -use sea_orm::entity::prelude::*; - -#[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel)] -#[sea_orm(table_name = "channel_paths")] -pub struct Model { - #[sea_orm(primary_key)] - pub id_path: String, - pub channel_id: ChannelId, -} - -impl ActiveModelBehavior for ActiveModel {} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} diff --git a/crates/collab/src/db/tests.rs b/crates/collab/src/db/tests.rs index 83154b9a0d..b6a89ff6f8 100644 --- a/crates/collab/src/db/tests.rs +++ b/crates/collab/src/db/tests.rs @@ -7,11 +7,10 @@ mod message_tests; use super::*; use gpui::executor::Background; use parking_lot::Mutex; -use rpc::proto::ChannelEdge; use sea_orm::ConnectionTrait; use sqlx::migrate::MigrateDatabase; use std::sync::{ - atomic::{AtomicI32, Ordering::SeqCst}, + atomic::{AtomicI32, AtomicU32, Ordering::SeqCst}, Arc, }; @@ -153,29 +152,17 @@ impl Drop for TestDb { } } -/// The second tuples are (channel_id, parent) -fn graph(channels: &[(ChannelId, &'static str)], edges: &[(ChannelId, ChannelId)]) -> ChannelGraph { - let mut graph = ChannelGraph { - channels: vec![], - edges: vec![], - }; - - for (id, name) in channels { - graph.channels.push(Channel { +fn channel_tree(channels: &[(ChannelId, &[ChannelId], &'static str, ChannelRole)]) -> Vec { + channels + .iter() + .map(|(id, parent_path, name, role)| Channel { id: *id, name: name.to_string(), visibility: ChannelVisibility::Members, + role: *role, + parent_path: parent_path.to_vec(), }) - } - - for (channel, parent) in edges { - graph.edges.push(ChannelEdge { - channel_id: channel.to_proto(), - parent_id: parent.to_proto(), - }) - } - - graph + .collect() } static GITHUB_USER_ID: AtomicI32 = AtomicI32::new(5); @@ -193,3 +180,11 @@ async fn new_test_user(db: &Arc, email: &str) -> UserId { .unwrap() .user_id } + +static TEST_CONNECTION_ID: AtomicU32 = AtomicU32::new(1); +fn new_test_connection(server: ServerId) -> ConnectionId { + ConnectionId { + id: TEST_CONNECTION_ID.fetch_add(1, SeqCst), + owner_id: server.0 as u32, + } +} diff --git a/crates/collab/src/db/tests/channel_tests.rs b/crates/collab/src/db/tests/channel_tests.rs index 556437e45b..43526c7f24 100644 --- a/crates/collab/src/db/tests/channel_tests.rs +++ b/crates/collab/src/db/tests/channel_tests.rs @@ -1,12 +1,10 @@ use crate::{ db::{ - queries::channels::ChannelGraph, - tests::{graph, new_test_user, TEST_RELEASE_CHANNEL}, - ChannelId, ChannelRole, Database, NewUserParams, RoomId, + tests::{channel_tree, new_test_connection, new_test_user, TEST_RELEASE_CHANNEL}, + Channel, ChannelId, ChannelRole, Database, NewUserParams, RoomId, }, test_both_dbs, }; -use collections::{HashMap, HashSet}; use rpc::{ proto::{self}, ConnectionId, @@ -16,31 +14,8 @@ use std::sync::Arc; test_both_dbs!(test_channels, test_channels_postgres, test_channels_sqlite); async fn test_channels(db: &Arc) { - let a_id = db - .create_user( - "user1@example.com", - false, - NewUserParams { - github_login: "user1".into(), - github_user_id: 5, - }, - ) - .await - .unwrap() - .user_id; - - let b_id = db - .create_user( - "user2@example.com", - false, - NewUserParams { - github_login: "user2".into(), - github_user_id: 6, - }, - ) - .await - .unwrap() - .user_id; + let a_id = new_test_user(db, "user1@example.com").await; + let b_id = new_test_user(db, "user2@example.com").await; let zed_id = db.create_root_channel("zed", a_id).await.unwrap(); @@ -55,70 +30,72 @@ async fn test_channels(db: &Arc) { .await .unwrap(); - let crdb_id = db.create_channel("crdb", Some(zed_id), a_id).await.unwrap(); + let crdb_id = db.create_sub_channel("crdb", zed_id, a_id).await.unwrap(); let livestreaming_id = db - .create_channel("livestreaming", Some(zed_id), a_id) + .create_sub_channel("livestreaming", zed_id, a_id) .await .unwrap(); let replace_id = db - .create_channel("replace", Some(zed_id), a_id) + .create_sub_channel("replace", zed_id, a_id) .await .unwrap(); - let mut members = db.get_channel_members(replace_id).await.unwrap(); + let mut members = db + .transaction(|tx| async move { + let channel = db.get_channel_internal(replace_id, &*tx).await?; + Ok(db.get_channel_participants(&channel, &*tx).await?) + }) + .await + .unwrap(); members.sort(); assert_eq!(members, &[a_id, b_id]); let rust_id = db.create_root_channel("rust", a_id).await.unwrap(); - let cargo_id = db - .create_channel("cargo", Some(rust_id), a_id) - .await - .unwrap(); + let cargo_id = db.create_sub_channel("cargo", rust_id, a_id).await.unwrap(); let cargo_ra_id = db - .create_channel("cargo-ra", Some(cargo_id), a_id) + .create_sub_channel("cargo-ra", cargo_id, a_id) .await .unwrap(); let result = db.get_channels_for_user(a_id).await.unwrap(); assert_eq!( result.channels, - graph( - &[ - (zed_id, "zed"), - (crdb_id, "crdb"), - (livestreaming_id, "livestreaming"), - (replace_id, "replace"), - (rust_id, "rust"), - (cargo_id, "cargo"), - (cargo_ra_id, "cargo-ra") - ], - &[ - (crdb_id, zed_id), - (livestreaming_id, zed_id), - (replace_id, zed_id), - (cargo_id, rust_id), - (cargo_ra_id, cargo_id), - ] - ) + channel_tree(&[ + (zed_id, &[], "zed", ChannelRole::Admin), + (crdb_id, &[zed_id], "crdb", ChannelRole::Admin), + ( + livestreaming_id, + &[zed_id], + "livestreaming", + ChannelRole::Admin + ), + (replace_id, &[zed_id], "replace", ChannelRole::Admin), + (rust_id, &[], "rust", ChannelRole::Admin), + (cargo_id, &[rust_id], "cargo", ChannelRole::Admin), + ( + cargo_ra_id, + &[rust_id, cargo_id], + "cargo-ra", + ChannelRole::Admin + ) + ],) ); let result = db.get_channels_for_user(b_id).await.unwrap(); assert_eq!( result.channels, - graph( - &[ - (zed_id, "zed"), - (crdb_id, "crdb"), - (livestreaming_id, "livestreaming"), - (replace_id, "replace") - ], - &[ - (crdb_id, zed_id), - (livestreaming_id, zed_id), - (replace_id, zed_id) - ] - ) + channel_tree(&[ + (zed_id, &[], "zed", ChannelRole::Member), + (crdb_id, &[zed_id], "crdb", ChannelRole::Member), + ( + livestreaming_id, + &[zed_id], + "livestreaming", + ChannelRole::Member + ), + (replace_id, &[zed_id], "replace", ChannelRole::Member) + ],) ); // Update member permissions @@ -134,19 +111,17 @@ async fn test_channels(db: &Arc) { let result = db.get_channels_for_user(b_id).await.unwrap(); assert_eq!( result.channels, - graph( - &[ - (zed_id, "zed"), - (crdb_id, "crdb"), - (livestreaming_id, "livestreaming"), - (replace_id, "replace") - ], - &[ - (crdb_id, zed_id), - (livestreaming_id, zed_id), - (replace_id, zed_id) - ] - ) + channel_tree(&[ + (zed_id, &[], "zed", ChannelRole::Admin), + (crdb_id, &[zed_id], "crdb", ChannelRole::Admin), + ( + livestreaming_id, + &[zed_id], + "livestreaming", + ChannelRole::Admin + ), + (replace_id, &[zed_id], "replace", ChannelRole::Admin) + ],) ); // Remove a single channel @@ -173,35 +148,13 @@ test_both_dbs!( async fn test_joining_channels(db: &Arc) { let owner_id = db.create_server("test").await.unwrap().0 as u32; - let user_1 = db - .create_user( - "user1@example.com", - false, - NewUserParams { - github_login: "user1".into(), - github_user_id: 5, - }, - ) - .await - .unwrap() - .user_id; - let user_2 = db - .create_user( - "user2@example.com", - false, - NewUserParams { - github_login: "user2".into(), - github_user_id: 6, - }, - ) - .await - .unwrap() - .user_id; + let user_1 = new_test_user(db, "user1@example.com").await; + let user_2 = new_test_user(db, "user2@example.com").await; let channel_1 = db.create_root_channel("channel_1", user_1).await.unwrap(); // can join a room with membership to its channel - let (joined_room, _) = db + let (joined_room, _, _) = db .join_channel( channel_1, user_1, @@ -305,7 +258,7 @@ async fn test_channel_invites(db: &Arc) { .unwrap(); let channel_1_3 = db - .create_channel("channel_3", Some(channel_1_1), user_1) + .create_sub_channel("channel_3", channel_1_1, user_1) .await .unwrap(); @@ -318,7 +271,7 @@ async fn test_channel_invites(db: &Arc) { &[ proto::ChannelMember { user_id: user_1.to_proto(), - kind: proto::channel_member::Kind::Member.into(), + kind: proto::channel_member::Kind::AncestorMember.into(), role: proto::ChannelRole::Admin.into(), }, proto::ChannelMember { @@ -371,14 +324,10 @@ async fn test_channel_renames(db: &Arc) { .await .unwrap(); - let zed_archive_id = zed_id; - - let channel = db.get_channel(zed_archive_id, user_1).await.unwrap(); + let channel = db.get_channel(zed_id, user_1).await.unwrap(); assert_eq!(channel.name, "zed-archive"); - let non_permissioned_rename = db - .rename_channel(zed_archive_id, user_2, "hacked-lol") - .await; + let non_permissioned_rename = db.rename_channel(zed_id, user_2, "hacked-lol").await; assert!(non_permissioned_rename.is_err()); let bad_name_rename = db.rename_channel(zed_id, user_1, "#").await; @@ -407,20 +356,17 @@ async fn test_db_channel_moving(db: &Arc) { let zed_id = db.create_root_channel("zed", a_id).await.unwrap(); - let crdb_id = db.create_channel("crdb", Some(zed_id), a_id).await.unwrap(); + let crdb_id = db.create_sub_channel("crdb", zed_id, a_id).await.unwrap(); - let gpui2_id = db - .create_channel("gpui2", Some(zed_id), a_id) - .await - .unwrap(); + let gpui2_id = db.create_sub_channel("gpui2", zed_id, a_id).await.unwrap(); let livestreaming_id = db - .create_channel("livestreaming", Some(crdb_id), a_id) + .create_sub_channel("livestreaming", crdb_id, a_id) .await .unwrap(); let livestreaming_dag_id = db - .create_channel("livestreaming_dag", Some(livestreaming_id), a_id) + .create_sub_channel("livestreaming_dag", livestreaming_id, a_id) .await .unwrap(); @@ -430,316 +376,16 @@ async fn test_db_channel_moving(db: &Arc) { // /- gpui2 // zed -- crdb - livestreaming - livestreaming_dag let result = db.get_channels_for_user(a_id).await.unwrap(); - assert_dag( + assert_channel_tree( result.channels, &[ - (zed_id, None), - (crdb_id, Some(zed_id)), - (gpui2_id, Some(zed_id)), - (livestreaming_id, Some(crdb_id)), - (livestreaming_dag_id, Some(livestreaming_id)), + (zed_id, &[]), + (crdb_id, &[zed_id]), + (livestreaming_id, &[zed_id, crdb_id]), + (livestreaming_dag_id, &[zed_id, crdb_id, livestreaming_id]), + (gpui2_id, &[zed_id]), ], ); - - // Attempt to make a cycle - assert!(db - .link_channel(a_id, zed_id, livestreaming_id) - .await - .is_err()); - - // ======================================================================== - // Make a link - db.link_channel(a_id, livestreaming_id, zed_id) - .await - .unwrap(); - - // DAG is now: - // /- gpui2 - // zed -- crdb - livestreaming - livestreaming_dag - // \---------/ - let result = db.get_channels_for_user(a_id).await.unwrap(); - assert_dag( - result.channels, - &[ - (zed_id, None), - (crdb_id, Some(zed_id)), - (gpui2_id, Some(zed_id)), - (livestreaming_id, Some(zed_id)), - (livestreaming_id, Some(crdb_id)), - (livestreaming_dag_id, Some(livestreaming_id)), - ], - ); - - // ======================================================================== - // Create a new channel below a channel with multiple parents - let livestreaming_dag_sub_id = db - .create_channel("livestreaming_dag_sub", Some(livestreaming_dag_id), a_id) - .await - .unwrap(); - - // DAG is now: - // /- gpui2 - // zed -- crdb - livestreaming - livestreaming_dag - livestreaming_dag_sub_id - // \---------/ - let result = db.get_channels_for_user(a_id).await.unwrap(); - assert_dag( - result.channels, - &[ - (zed_id, None), - (crdb_id, Some(zed_id)), - (gpui2_id, Some(zed_id)), - (livestreaming_id, Some(zed_id)), - (livestreaming_id, Some(crdb_id)), - (livestreaming_dag_id, Some(livestreaming_id)), - (livestreaming_dag_sub_id, Some(livestreaming_dag_id)), - ], - ); - - // ======================================================================== - // Test a complex DAG by making another link - let returned_channels = db - .link_channel(a_id, livestreaming_dag_sub_id, livestreaming_id) - .await - .unwrap(); - - // DAG is now: - // /- gpui2 /---------------------\ - // zed - crdb - livestreaming - livestreaming_dag - livestreaming_dag_sub_id - // \--------/ - - // make sure we're getting just the new link - // Not using the assert_dag helper because we want to make sure we're returning the full data - pretty_assertions::assert_eq!( - returned_channels, - graph( - &[(livestreaming_dag_sub_id, "livestreaming_dag_sub")], - &[(livestreaming_dag_sub_id, livestreaming_id)] - ) - ); - - let result = db.get_channels_for_user(a_id).await.unwrap(); - assert_dag( - result.channels, - &[ - (zed_id, None), - (crdb_id, Some(zed_id)), - (gpui2_id, Some(zed_id)), - (livestreaming_id, Some(zed_id)), - (livestreaming_id, Some(crdb_id)), - (livestreaming_dag_id, Some(livestreaming_id)), - (livestreaming_dag_sub_id, Some(livestreaming_id)), - (livestreaming_dag_sub_id, Some(livestreaming_dag_id)), - ], - ); - - // ======================================================================== - // Test a complex DAG by making another link - let returned_channels = db - .link_channel(a_id, livestreaming_id, gpui2_id) - .await - .unwrap(); - - // DAG is now: - // /- gpui2 -\ /---------------------\ - // zed - crdb -- livestreaming - livestreaming_dag - livestreaming_dag_sub_id - // \---------/ - - // Make sure that we're correctly getting the full sub-dag - pretty_assertions::assert_eq!( - returned_channels, - graph( - &[ - (livestreaming_id, "livestreaming"), - (livestreaming_dag_id, "livestreaming_dag"), - (livestreaming_dag_sub_id, "livestreaming_dag_sub"), - ], - &[ - (livestreaming_id, gpui2_id), - (livestreaming_dag_id, livestreaming_id), - (livestreaming_dag_sub_id, livestreaming_id), - (livestreaming_dag_sub_id, livestreaming_dag_id), - ] - ) - ); - - let result = db.get_channels_for_user(a_id).await.unwrap(); - assert_dag( - result.channels, - &[ - (zed_id, None), - (crdb_id, Some(zed_id)), - (gpui2_id, Some(zed_id)), - (livestreaming_id, Some(zed_id)), - (livestreaming_id, Some(crdb_id)), - (livestreaming_id, Some(gpui2_id)), - (livestreaming_dag_id, Some(livestreaming_id)), - (livestreaming_dag_sub_id, Some(livestreaming_id)), - (livestreaming_dag_sub_id, Some(livestreaming_dag_id)), - ], - ); - - // ======================================================================== - // Test unlinking in a complex DAG by removing the inner link - db.unlink_channel(a_id, livestreaming_dag_sub_id, livestreaming_id) - .await - .unwrap(); - - // DAG is now: - // /- gpui2 -\ - // zed - crdb -- livestreaming - livestreaming_dag - livestreaming_dag_sub - // \---------/ - - let result = db.get_channels_for_user(a_id).await.unwrap(); - assert_dag( - result.channels, - &[ - (zed_id, None), - (crdb_id, Some(zed_id)), - (gpui2_id, Some(zed_id)), - (livestreaming_id, Some(gpui2_id)), - (livestreaming_id, Some(zed_id)), - (livestreaming_id, Some(crdb_id)), - (livestreaming_dag_id, Some(livestreaming_id)), - (livestreaming_dag_sub_id, Some(livestreaming_dag_id)), - ], - ); - - // ======================================================================== - // Test unlinking in a complex DAG by removing the inner link - db.unlink_channel(a_id, livestreaming_id, gpui2_id) - .await - .unwrap(); - - // DAG is now: - // /- gpui2 - // zed - crdb -- livestreaming - livestreaming_dag - livestreaming_dag_sub - // \---------/ - let result = db.get_channels_for_user(a_id).await.unwrap(); - assert_dag( - result.channels, - &[ - (zed_id, None), - (crdb_id, Some(zed_id)), - (gpui2_id, Some(zed_id)), - (livestreaming_id, Some(zed_id)), - (livestreaming_id, Some(crdb_id)), - (livestreaming_dag_id, Some(livestreaming_id)), - (livestreaming_dag_sub_id, Some(livestreaming_dag_id)), - ], - ); - - // ======================================================================== - // Test moving DAG nodes by moving livestreaming to be below gpui2 - db.move_channel(a_id, livestreaming_id, crdb_id, gpui2_id) - .await - .unwrap(); - - // DAG is now: - // /- gpui2 -- livestreaming - livestreaming_dag - livestreaming_dag_sub - // zed - crdb / - // \---------/ - let result = db.get_channels_for_user(a_id).await.unwrap(); - assert_dag( - result.channels, - &[ - (zed_id, None), - (crdb_id, Some(zed_id)), - (gpui2_id, Some(zed_id)), - (livestreaming_id, Some(zed_id)), - (livestreaming_id, Some(gpui2_id)), - (livestreaming_dag_id, Some(livestreaming_id)), - (livestreaming_dag_sub_id, Some(livestreaming_dag_id)), - ], - ); - - // ======================================================================== - // Deleting a channel should not delete children that still have other parents - db.delete_channel(gpui2_id, a_id).await.unwrap(); - - // DAG is now: - // zed - crdb - // \- livestreaming - livestreaming_dag - livestreaming_dag_sub - let result = db.get_channels_for_user(a_id).await.unwrap(); - assert_dag( - result.channels, - &[ - (zed_id, None), - (crdb_id, Some(zed_id)), - (livestreaming_id, Some(zed_id)), - (livestreaming_dag_id, Some(livestreaming_id)), - (livestreaming_dag_sub_id, Some(livestreaming_dag_id)), - ], - ); - - // ======================================================================== - // Unlinking a channel from it's parent should automatically promote it to a root channel - db.unlink_channel(a_id, crdb_id, zed_id).await.unwrap(); - - // DAG is now: - // crdb - // zed - // \- livestreaming - livestreaming_dag - livestreaming_dag_sub - - let result = db.get_channels_for_user(a_id).await.unwrap(); - assert_dag( - result.channels, - &[ - (zed_id, None), - (crdb_id, None), - (livestreaming_id, Some(zed_id)), - (livestreaming_dag_id, Some(livestreaming_id)), - (livestreaming_dag_sub_id, Some(livestreaming_dag_id)), - ], - ); - - // ======================================================================== - // You should be able to move a root channel into a non-root channel - db.link_channel(a_id, crdb_id, zed_id).await.unwrap(); - - // DAG is now: - // zed - crdb - // \- livestreaming - livestreaming_dag - livestreaming_dag_sub - - let result = db.get_channels_for_user(a_id).await.unwrap(); - assert_dag( - result.channels, - &[ - (zed_id, None), - (crdb_id, Some(zed_id)), - (livestreaming_id, Some(zed_id)), - (livestreaming_dag_id, Some(livestreaming_id)), - (livestreaming_dag_sub_id, Some(livestreaming_dag_id)), - ], - ); - - // ======================================================================== - // Prep for DAG deletion test - db.link_channel(a_id, livestreaming_id, crdb_id) - .await - .unwrap(); - - // DAG is now: - // zed - crdb - livestreaming - livestreaming_dag - livestreaming_dag_sub - // \--------/ - - let result = db.get_channels_for_user(a_id).await.unwrap(); - assert_dag( - result.channels, - &[ - (zed_id, None), - (crdb_id, Some(zed_id)), - (livestreaming_id, Some(zed_id)), - (livestreaming_id, Some(crdb_id)), - (livestreaming_dag_id, Some(livestreaming_id)), - (livestreaming_dag_sub_id, Some(livestreaming_dag_id)), - ], - ); - - // Deleting the parent of a DAG should delete the whole DAG: - db.delete_channel(zed_id, a_id).await.unwrap(); - let result = db.get_channels_for_user(a_id).await.unwrap(); - - assert!(result.channels.is_empty()) } test_both_dbs!( @@ -765,12 +411,12 @@ async fn test_db_channel_moving_bugs(db: &Arc) { let zed_id = db.create_root_channel("zed", user_id).await.unwrap(); let projects_id = db - .create_channel("projects", Some(zed_id), user_id) + .create_sub_channel("projects", zed_id, user_id) .await .unwrap(); let livestreaming_id = db - .create_channel("livestreaming", Some(projects_id), user_id) + .create_sub_channel("livestreaming", projects_id, user_id) .await .unwrap(); @@ -778,23 +424,30 @@ async fn test_db_channel_moving_bugs(db: &Arc) { // Move to same parent should be a no-op assert!(db - .move_channel(user_id, projects_id, zed_id, zed_id) + .move_channel(projects_id, Some(zed_id), user_id) .await .unwrap() - .is_empty()); - - // Stranding a channel should retain it's sub channels - db.unlink_channel(user_id, projects_id, zed_id) - .await - .unwrap(); + .is_none()); let result = db.get_channels_for_user(user_id).await.unwrap(); - assert_dag( + assert_channel_tree( result.channels, &[ - (zed_id, None), - (projects_id, None), - (livestreaming_id, Some(projects_id)), + (zed_id, &[]), + (projects_id, &[zed_id]), + (livestreaming_id, &[zed_id, projects_id]), + ], + ); + + // Move the project channel to the root + db.move_channel(projects_id, None, user_id).await.unwrap(); + let result = db.get_channels_for_user(user_id).await.unwrap(); + assert_channel_tree( + result.channels, + &[ + (zed_id, &[]), + (projects_id, &[]), + (livestreaming_id, &[projects_id]), ], ); } @@ -811,44 +464,52 @@ async fn test_user_is_channel_participant(db: &Arc) { let guest = new_test_user(db, "guest@example.com").await; let zed_channel = db.create_root_channel("zed", admin).await.unwrap(); - let active_channel = db - .create_channel("active", Some(zed_channel), admin) + let active_channel_id = db + .create_sub_channel("active", zed_channel, admin) .await .unwrap(); - let vim_channel = db - .create_channel("vim", Some(active_channel), admin) + let vim_channel_id = db + .create_sub_channel("vim", active_channel_id, admin) .await .unwrap(); - db.set_channel_visibility(vim_channel, crate::db::ChannelVisibility::Public, admin) + db.set_channel_visibility(vim_channel_id, crate::db::ChannelVisibility::Public, admin) .await .unwrap(); - db.invite_channel_member(active_channel, member, admin, ChannelRole::Member) + db.invite_channel_member(active_channel_id, member, admin, ChannelRole::Member) .await .unwrap(); - db.invite_channel_member(vim_channel, guest, admin, ChannelRole::Guest) + db.invite_channel_member(vim_channel_id, guest, admin, ChannelRole::Guest) .await .unwrap(); - db.respond_to_channel_invite(active_channel, member, true) + db.respond_to_channel_invite(active_channel_id, member, true) .await .unwrap(); db.transaction(|tx| async move { - db.check_user_is_channel_participant(vim_channel, admin, &*tx) - .await + db.check_user_is_channel_participant( + &db.get_channel_internal(vim_channel_id, &*tx).await?, + admin, + &*tx, + ) + .await }) .await .unwrap(); db.transaction(|tx| async move { - db.check_user_is_channel_participant(vim_channel, member, &*tx) - .await + db.check_user_is_channel_participant( + &db.get_channel_internal(vim_channel_id, &*tx).await?, + member, + &*tx, + ) + .await }) .await .unwrap(); let mut members = db - .get_channel_participant_details(vim_channel, admin) + .get_channel_participant_details(vim_channel_id, admin) .await .unwrap(); @@ -859,7 +520,7 @@ async fn test_user_is_channel_participant(db: &Arc) { &[ proto::ChannelMember { user_id: admin.to_proto(), - kind: proto::channel_member::Kind::Member.into(), + kind: proto::channel_member::Kind::AncestorMember.into(), role: proto::ChannelRole::Admin.into(), }, proto::ChannelMember { @@ -875,38 +536,49 @@ async fn test_user_is_channel_participant(db: &Arc) { ] ); - db.respond_to_channel_invite(vim_channel, guest, true) + db.respond_to_channel_invite(vim_channel_id, guest, true) .await .unwrap(); db.transaction(|tx| async move { - db.check_user_is_channel_participant(vim_channel, guest, &*tx) - .await + db.check_user_is_channel_participant( + &db.get_channel_internal(vim_channel_id, &*tx).await?, + guest, + &*tx, + ) + .await }) .await .unwrap(); let channels = db.get_channels_for_user(guest).await.unwrap().channels; - assert_dag(channels, &[(vim_channel, None)]); + assert_channel_tree(channels, &[(vim_channel_id, &[])]); let channels = db.get_channels_for_user(member).await.unwrap().channels; - assert_dag( + assert_channel_tree( channels, - &[(active_channel, None), (vim_channel, Some(active_channel))], + &[ + (active_channel_id, &[]), + (vim_channel_id, &[active_channel_id]), + ], ); - db.set_channel_member_role(vim_channel, admin, guest, ChannelRole::Banned) + db.set_channel_member_role(vim_channel_id, admin, guest, ChannelRole::Banned) .await .unwrap(); assert!(db .transaction(|tx| async move { - db.check_user_is_channel_participant(vim_channel, guest, &*tx) - .await + db.check_user_is_channel_participant( + &db.get_channel_internal(vim_channel_id, &*tx).await.unwrap(), + guest, + &*tx, + ) + .await }) .await .is_err()); let mut members = db - .get_channel_participant_details(vim_channel, admin) + .get_channel_participant_details(vim_channel_id, admin) .await .unwrap(); @@ -917,7 +589,7 @@ async fn test_user_is_channel_participant(db: &Arc) { &[ proto::ChannelMember { user_id: admin.to_proto(), - kind: proto::channel_member::Kind::Member.into(), + kind: proto::channel_member::Kind::AncestorMember.into(), role: proto::ChannelRole::Admin.into(), }, proto::ChannelMember { @@ -933,7 +605,7 @@ async fn test_user_is_channel_participant(db: &Arc) { ] ); - db.remove_channel_member(vim_channel, guest, admin) + db.remove_channel_member(vim_channel_id, guest, admin) .await .unwrap(); @@ -947,7 +619,7 @@ async fn test_user_is_channel_participant(db: &Arc) { // currently people invited to parent channels are not shown here let mut members = db - .get_channel_participant_details(vim_channel, admin) + .get_channel_participant_details(vim_channel_id, admin) .await .unwrap(); @@ -958,7 +630,7 @@ async fn test_user_is_channel_participant(db: &Arc) { &[ proto::ChannelMember { user_id: admin.to_proto(), - kind: proto::channel_member::Kind::Member.into(), + kind: proto::channel_member::Kind::AncestorMember.into(), role: proto::ChannelRole::Admin.into(), }, proto::ChannelMember { @@ -974,28 +646,42 @@ async fn test_user_is_channel_participant(db: &Arc) { .unwrap(); db.transaction(|tx| async move { - db.check_user_is_channel_participant(zed_channel, guest, &*tx) - .await + db.check_user_is_channel_participant( + &db.get_channel_internal(zed_channel, &*tx).await.unwrap(), + guest, + &*tx, + ) + .await }) .await .unwrap(); assert!(db .transaction(|tx| async move { - db.check_user_is_channel_participant(active_channel, guest, &*tx) - .await + db.check_user_is_channel_participant( + &db.get_channel_internal(active_channel_id, &*tx) + .await + .unwrap(), + guest, + &*tx, + ) + .await }) .await .is_err(),); db.transaction(|tx| async move { - db.check_user_is_channel_participant(vim_channel, guest, &*tx) - .await + db.check_user_is_channel_participant( + &db.get_channel_internal(vim_channel_id, &*tx).await.unwrap(), + guest, + &*tx, + ) + .await }) .await .unwrap(); let mut members = db - .get_channel_participant_details(vim_channel, admin) + .get_channel_participant_details(vim_channel_id, admin) .await .unwrap(); @@ -1006,7 +692,7 @@ async fn test_user_is_channel_participant(db: &Arc) { &[ proto::ChannelMember { user_id: admin.to_proto(), - kind: proto::channel_member::Kind::Member.into(), + kind: proto::channel_member::Kind::AncestorMember.into(), role: proto::ChannelRole::Admin.into(), }, proto::ChannelMember { @@ -1023,9 +709,9 @@ async fn test_user_is_channel_participant(db: &Arc) { ); let channels = db.get_channels_for_user(guest).await.unwrap().channels; - assert_dag( + assert_channel_tree( channels, - &[(zed_channel, None), (vim_channel, Some(zed_channel))], + &[(zed_channel, &[]), (vim_channel_id, &[zed_channel])], ) } @@ -1041,17 +727,17 @@ async fn test_user_joins_correct_channel(db: &Arc) { let zed_channel = db.create_root_channel("zed", admin).await.unwrap(); let active_channel = db - .create_channel("active", Some(zed_channel), admin) + .create_sub_channel("active", zed_channel, admin) .await .unwrap(); let vim_channel = db - .create_channel("vim", Some(active_channel), admin) + .create_sub_channel("vim", active_channel, admin) .await .unwrap(); let vim2_channel = db - .create_channel("vim2", Some(vim_channel), admin) + .create_sub_channel("vim2", vim_channel, admin) .await .unwrap(); @@ -1068,36 +754,66 @@ async fn test_user_joins_correct_channel(db: &Arc) { .unwrap(); let most_public = db - .transaction( - |tx| async move { db.most_public_ancestor_for_channel(vim_channel, &*tx).await }, - ) + .transaction(|tx| async move { + Ok(db + .public_ancestors_including_self( + &db.get_channel_internal(vim_channel, &*tx).await.unwrap(), + &tx, + ) + .await? + .first() + .cloned()) + }) + .await + .unwrap() + .unwrap() + .id; + + assert_eq!(most_public, zed_channel) +} + +test_both_dbs!( + test_guest_access, + test_guest_access_postgres, + test_guest_access_sqlite +); + +async fn test_guest_access(db: &Arc) { + let server = db.create_server("test").await.unwrap(); + + let admin = new_test_user(db, "admin@example.com").await; + let guest = new_test_user(db, "guest@example.com").await; + let guest_connection = new_test_connection(server); + + let zed_channel = db.create_root_channel("zed", admin).await.unwrap(); + db.set_channel_visibility(zed_channel, crate::db::ChannelVisibility::Public, admin) .await .unwrap(); - assert_eq!(most_public, Some(zed_channel)) + assert!(db + .join_channel_chat(zed_channel, guest_connection, guest) + .await + .is_err()); + + db.join_channel(zed_channel, guest, guest_connection, TEST_RELEASE_CHANNEL) + .await + .unwrap(); + + assert!(db + .join_channel_chat(zed_channel, guest_connection, guest) + .await + .is_ok()) } #[track_caller] -fn assert_dag(actual: ChannelGraph, expected: &[(ChannelId, Option)]) { - let mut actual_map: HashMap> = HashMap::default(); - for channel in actual.channels { - actual_map.insert(channel.id, HashSet::default()); - } - for edge in actual.edges { - actual_map - .get_mut(&ChannelId::from_proto(edge.channel_id)) - .unwrap() - .insert(ChannelId::from_proto(edge.parent_id)); - } - - let mut expected_map: HashMap> = HashMap::default(); - - for (child, parent) in expected { - let entry = expected_map.entry(*child).or_default(); - if let Some(parent) = parent { - entry.insert(*parent); - } - } - - pretty_assertions::assert_eq!(actual_map, expected_map) +fn assert_channel_tree(actual: Vec, expected: &[(ChannelId, &[ChannelId])]) { + let actual = actual + .iter() + .map(|channel| (channel.id, channel.parent_path.as_slice())) + .collect::>(); + pretty_assertions::assert_eq!( + actual, + expected.to_vec(), + "wrong channel ids and parent paths" + ); } diff --git a/crates/collab/src/db/tests/message_tests.rs b/crates/collab/src/db/tests/message_tests.rs index 97b3142930..10d9778612 100644 --- a/crates/collab/src/db/tests/message_tests.rs +++ b/crates/collab/src/db/tests/message_tests.rs @@ -15,18 +15,22 @@ test_both_dbs!( async fn test_channel_message_retrieval(db: &Arc) { let user = new_test_user(db, "user@example.com").await; - let channel = db.create_channel("channel", None, user).await.unwrap(); + let result = db.create_channel("channel", None, user).await.unwrap(); let owner_id = db.create_server("test").await.unwrap().0 as u32; - db.join_channel_chat(channel, rpc::ConnectionId { owner_id, id: 0 }, user) - .await - .unwrap(); + db.join_channel_chat( + result.channel.id, + rpc::ConnectionId { owner_id, id: 0 }, + user, + ) + .await + .unwrap(); let mut all_messages = Vec::new(); for i in 0..10 { all_messages.push( db.create_channel_message( - channel, + result.channel.id, user, &i.to_string(), &[], @@ -41,7 +45,7 @@ async fn test_channel_message_retrieval(db: &Arc) { } let messages = db - .get_channel_messages(channel, user, 3, None) + .get_channel_messages(result.channel.id, user, 3, None) .await .unwrap() .into_iter() @@ -51,7 +55,7 @@ async fn test_channel_message_retrieval(db: &Arc) { let messages = db .get_channel_messages( - channel, + result.channel.id, user, 4, Some(MessageId::from_proto(all_messages[6])), @@ -74,7 +78,7 @@ async fn test_channel_message_nonces(db: &Arc) { let user_a = new_test_user(db, "user_a@example.com").await; let user_b = new_test_user(db, "user_b@example.com").await; let user_c = new_test_user(db, "user_c@example.com").await; - let channel = db.create_channel("channel", None, user_a).await.unwrap(); + let channel = db.create_root_channel("channel", user_a).await.unwrap(); db.invite_channel_member(channel, user_b, user_a, ChannelRole::Member) .await .unwrap(); @@ -206,8 +210,8 @@ async fn test_unseen_channel_messages(db: &Arc) { let user = new_test_user(db, "user_a@example.com").await; let observer = new_test_user(db, "user_b@example.com").await; - let channel_1 = db.create_channel("channel", None, user).await.unwrap(); - let channel_2 = db.create_channel("channel-2", None, user).await.unwrap(); + let channel_1 = db.create_root_channel("channel", user).await.unwrap(); + let channel_2 = db.create_root_channel("channel-2", user).await.unwrap(); db.invite_channel_member(channel_1, observer, user, ChannelRole::Member) .await @@ -362,7 +366,12 @@ async fn test_channel_message_mentions(db: &Arc) { let user_b = new_test_user(db, "user_b@example.com").await; let user_c = new_test_user(db, "user_c@example.com").await; - let channel = db.create_channel("channel", None, user_a).await.unwrap(); + let channel = db + .create_channel("channel", None, user_a) + .await + .unwrap() + .channel + .id; db.invite_channel_member(channel, user_b, user_a, ChannelRole::Member) .await .unwrap(); diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 5a29861351..a0ec7da392 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -3,8 +3,11 @@ mod connection_pool; use crate::{ auth, db::{ - self, BufferId, ChannelId, ChannelVisibility, ChannelsForUser, CreatedChannelMessage, - Database, MessageId, NotificationId, ProjectId, RoomId, ServerId, User, UserId, + self, BufferId, ChannelId, ChannelRole, ChannelsForUser, CreateChannelResult, + CreatedChannelMessage, Database, InviteMemberResult, MembershipUpdated, MessageId, + MoveChannelResult, NotificationId, ProjectId, RemoveChannelMemberResult, + RenameChannelResult, RespondToChannelInvite, RoomId, ServerId, SetChannelVisibilityResult, + User, UserId, }, executor::Executor, AppState, Result, @@ -38,8 +41,8 @@ use lazy_static::lazy_static; use prometheus::{register_int_gauge, IntGauge}; use rpc::{ proto::{ - self, Ack, AnyTypedEnvelope, ChannelEdge, EntityMessage, EnvelopedMessage, - LiveKitConnectionInfo, RequestMessage, UpdateChannelBufferCollaborators, + self, Ack, AnyTypedEnvelope, EntityMessage, EnvelopedMessage, LiveKitConnectionInfo, + RequestMessage, UpdateChannelBufferCollaborators, }, Connection, ConnectionId, Peer, Receipt, TypedEnvelope, }; @@ -274,8 +277,6 @@ impl Server { .add_request_handler(get_channel_messages_by_id) .add_request_handler(get_notifications) .add_request_handler(mark_notification_as_read) - .add_request_handler(link_channel) - .add_request_handler(unlink_channel) .add_request_handler(move_channel) .add_request_handler(follow) .add_message_handler(unfollow) @@ -594,7 +595,7 @@ impl Server { let mut pool = this.connection_pool.lock(); pool.add_connection(connection_id, user_id, user.admin); this.peer.send(connection_id, build_initial_contacts_update(contacts, &pool))?; - this.peer.send(connection_id, build_initial_channels_update( + this.peer.send(connection_id, build_channels_update( channels_for_user, channel_invites ))?; @@ -951,6 +952,7 @@ async fn create_room( Some(proto::LiveKitConnectionInfo { server_url: live_kit.url().into(), token, + can_publish: true, }) }) } @@ -1031,6 +1033,7 @@ async fn join_room( Some(proto::LiveKitConnectionInfo { server_url: live_kit.url().into(), token, + can_publish: true, }) } else { None @@ -2217,38 +2220,21 @@ async fn create_channel( let db = session.db().await; let parent_id = request.parent_id.map(|id| ChannelId::from_proto(id)); - let id = db + let CreateChannelResult { + channel, + participants_to_update, + } = db .create_channel(&request.name, parent_id, session.user_id) .await?; - let channel = proto::Channel { - id: id.to_proto(), - name: request.name, - visibility: proto::ChannelVisibility::Members as i32, - }; - response.send(proto::CreateChannelResponse { - channel: Some(channel.clone()), + channel: Some(channel.to_proto()), parent_id: request.parent_id, })?; - let Some(parent_id) = parent_id else { - return Ok(()); - }; - - let update = proto::UpdateChannels { - channels: vec![channel], - insert_edge: vec![ChannelEdge { - parent_id: parent_id.to_proto(), - channel_id: id.to_proto(), - }], - ..Default::default() - }; - - let user_ids_to_notify = db.get_channel_members(parent_id).await?; - let connection_pool = session.connection_pool().await; - for user_id in user_ids_to_notify { + for (user_id, channels) in participants_to_update { + let update = build_channels_update(channels, vec![]); for connection_id in connection_pool.user_connection_ids(user_id) { if user_id == session.user_id { continue; @@ -2297,7 +2283,10 @@ async fn invite_channel_member( let db = session.db().await; let channel_id = ChannelId::from_proto(request.channel_id); let invitee_id = UserId::from_proto(request.user_id); - let notifications = db + let InviteMemberResult { + channel, + notifications, + } = db .invite_channel_member( channel_id, invitee_id, @@ -2306,21 +2295,17 @@ async fn invite_channel_member( ) .await?; - let channel = db.get_channel(channel_id, session.user_id).await?; + let update = proto::UpdateChannels { + channel_invitations: vec![channel.to_proto()], + ..Default::default() + }; - let mut update = proto::UpdateChannels::default(); - update.channel_invitations.push(proto::Channel { - id: channel.id.to_proto(), - visibility: channel.visibility.into(), - name: channel.name, - }); - - let pool = session.connection_pool().await; - for connection_id in pool.user_connection_ids(invitee_id) { + let connection_pool = session.connection_pool().await; + for connection_id in connection_pool.user_connection_ids(invitee_id) { session.peer.send(connection_id, update.clone())?; } - send_notifications(&*pool, &session.peer, notifications); + send_notifications(&*connection_pool, &session.peer, notifications); response.send(proto::Ack {})?; Ok(()) @@ -2335,20 +2320,22 @@ async fn remove_channel_member( let channel_id = ChannelId::from_proto(request.channel_id); let member_id = UserId::from_proto(request.user_id); - let removed_notification_id = db + let RemoveChannelMemberResult { + membership_update, + notification_id, + } = db .remove_channel_member(channel_id, member_id, session.user_id) .await?; - let mut update = proto::UpdateChannels::default(); - update.delete_channels.push(channel_id.to_proto()); - - for connection_id in session - .connection_pool() - .await - .user_connection_ids(member_id) - { - session.peer.send(connection_id, update.clone()).trace_err(); - if let Some(notification_id) = removed_notification_id { + let connection_pool = &session.connection_pool().await; + notify_membership_updated( + &connection_pool, + membership_update, + member_id, + &session.peer, + ); + for connection_id in connection_pool.user_connection_ids(member_id) { + if let Some(notification_id) = notification_id { session .peer .send( @@ -2374,22 +2361,27 @@ async fn set_channel_visibility( let channel_id = ChannelId::from_proto(request.channel_id); let visibility = request.visibility().into(); - let channel = db + let SetChannelVisibilityResult { + participants_to_update, + participants_to_remove, + channels_to_remove, + } = db .set_channel_visibility(channel_id, visibility, session.user_id) .await?; - let mut update = proto::UpdateChannels::default(); - update.channels.push(proto::Channel { - id: channel.id.to_proto(), - name: channel.name, - visibility: channel.visibility.into(), - }); - - let member_ids = db.get_channel_members(channel_id).await?; - let connection_pool = session.connection_pool().await; - for member_id in member_ids { - for connection_id in connection_pool.user_connection_ids(member_id) { + for (user_id, channels) in participants_to_update { + let update = build_channels_update(channels, vec![]); + for connection_id in connection_pool.user_connection_ids(user_id) { + session.peer.send(connection_id, update.clone())?; + } + } + for user_id in participants_to_remove { + let update = proto::UpdateChannels { + delete_channels: channels_to_remove.iter().map(|id| id.to_proto()).collect(), + ..Default::default() + }; + for connection_id in connection_pool.user_connection_ids(user_id) { session.peer.send(connection_id, update.clone())?; } } @@ -2406,7 +2398,7 @@ async fn set_channel_member_role( let db = session.db().await; let channel_id = ChannelId::from_proto(request.channel_id); let member_id = UserId::from_proto(request.user_id); - let channel_member = db + let result = db .set_channel_member_role( channel_id, session.user_id, @@ -2415,22 +2407,30 @@ async fn set_channel_member_role( ) .await?; - let channel = db.get_channel(channel_id, session.user_id).await?; + match result { + db::SetMemberRoleResult::MembershipUpdated(membership_update) => { + let connection_pool = session.connection_pool().await; + notify_membership_updated( + &connection_pool, + membership_update, + member_id, + &session.peer, + ) + } + db::SetMemberRoleResult::InviteUpdated(channel) => { + let update = proto::UpdateChannels { + channel_invitations: vec![channel.to_proto()], + ..Default::default() + }; - let mut update = proto::UpdateChannels::default(); - if channel_member.accepted { - update.channel_permissions.push(proto::ChannelPermission { - channel_id: channel.id.to_proto(), - role: request.role, - }); - } - - for connection_id in session - .connection_pool() - .await - .user_connection_ids(member_id) - { - session.peer.send(connection_id, update.clone())?; + for connection_id in session + .connection_pool() + .await + .user_connection_ids(member_id) + { + session.peer.send(connection_id, update.clone())?; + } + } } response.send(proto::Ack {})?; @@ -2444,26 +2444,25 @@ async fn rename_channel( ) -> Result<()> { let db = session.db().await; let channel_id = ChannelId::from_proto(request.channel_id); - let channel = db + let RenameChannelResult { + channel, + participants_to_update, + } = db .rename_channel(channel_id, session.user_id, &request.name) .await?; - let channel = proto::Channel { - id: channel.id.to_proto(), - name: channel.name, - visibility: channel.visibility.into(), - }; response.send(proto::RenameChannelResponse { - channel: Some(channel.clone()), + channel: Some(channel.to_proto()), })?; - let mut update = proto::UpdateChannels::default(); - update.channels.push(channel); - - let member_ids = db.get_channel_members(channel_id).await?; let connection_pool = session.connection_pool().await; - for member_id in member_ids { - for connection_id in connection_pool.user_connection_ids(member_id) { + for (user_id, channel) in participants_to_update { + for connection_id in connection_pool.user_connection_ids(user_id) { + let update = proto::UpdateChannels { + channels: vec![channel.to_proto()], + ..Default::default() + }; + session.peer.send(connection_id, update.clone())?; } } @@ -2471,131 +2470,55 @@ async fn rename_channel( Ok(()) } -async fn link_channel( - request: proto::LinkChannel, - response: Response, - session: Session, -) -> Result<()> { - let db = session.db().await; - let channel_id = ChannelId::from_proto(request.channel_id); - let to = ChannelId::from_proto(request.to); - let channels_to_send = db.link_channel(session.user_id, channel_id, to).await?; - - let members = db.get_channel_members(to).await?; - let connection_pool = session.connection_pool().await; - let update = proto::UpdateChannels { - channels: channels_to_send - .channels - .into_iter() - .map(|channel| proto::Channel { - id: channel.id.to_proto(), - visibility: channel.visibility.into(), - name: channel.name, - }) - .collect(), - insert_edge: channels_to_send.edges, - ..Default::default() - }; - for member_id in members { - for connection_id in connection_pool.user_connection_ids(member_id) { - session.peer.send(connection_id, update.clone())?; - } - } - - response.send(Ack {})?; - - Ok(()) -} - -async fn unlink_channel( - request: proto::UnlinkChannel, - response: Response, - session: Session, -) -> Result<()> { - let db = session.db().await; - let channel_id = ChannelId::from_proto(request.channel_id); - let from = ChannelId::from_proto(request.from); - - db.unlink_channel(session.user_id, channel_id, from).await?; - - let members = db.get_channel_members(from).await?; - - let update = proto::UpdateChannels { - delete_edge: vec![proto::ChannelEdge { - channel_id: channel_id.to_proto(), - parent_id: from.to_proto(), - }], - ..Default::default() - }; - let connection_pool = session.connection_pool().await; - for member_id in members { - for connection_id in connection_pool.user_connection_ids(member_id) { - session.peer.send(connection_id, update.clone())?; - } - } - - response.send(Ack {})?; - - Ok(()) -} - async fn move_channel( request: proto::MoveChannel, response: Response, session: Session, ) -> Result<()> { - let db = session.db().await; let channel_id = ChannelId::from_proto(request.channel_id); - let from_parent = ChannelId::from_proto(request.from); - let to = ChannelId::from_proto(request.to); + let to = request.to.map(ChannelId::from_proto); - let channels_to_send = db - .move_channel(session.user_id, channel_id, from_parent, to) + let result = session + .db() + .await + .move_channel(channel_id, to, session.user_id) .await?; - if channels_to_send.is_empty() { - response.send(Ack {})?; - return Ok(()); - } - - let members_from = db.get_channel_members(from_parent).await?; - let members_to = db.get_channel_members(to).await?; - - let update = proto::UpdateChannels { - delete_edge: vec![proto::ChannelEdge { - channel_id: channel_id.to_proto(), - parent_id: from_parent.to_proto(), - }], - ..Default::default() - }; - let connection_pool = session.connection_pool().await; - for member_id in members_from { - for connection_id in connection_pool.user_connection_ids(member_id) { - session.peer.send(connection_id, update.clone())?; - } - } - - let update = proto::UpdateChannels { - channels: channels_to_send - .channels - .into_iter() - .map(|channel| proto::Channel { - id: channel.id.to_proto(), - visibility: channel.visibility.into(), - name: channel.name, - }) - .collect(), - insert_edge: channels_to_send.edges, - ..Default::default() - }; - for member_id in members_to { - for connection_id in connection_pool.user_connection_ids(member_id) { - session.peer.send(connection_id, update.clone())?; - } - } + notify_channel_moved(result, session).await?; response.send(Ack {})?; + Ok(()) +} +async fn notify_channel_moved(result: Option, session: Session) -> Result<()> { + let Some(MoveChannelResult { + participants_to_remove, + participants_to_update, + moved_channels, + }) = result + else { + return Ok(()); + }; + let moved_channels: Vec = moved_channels.iter().map(|id| id.to_proto()).collect(); + + let connection_pool = session.connection_pool().await; + for (user_id, channels) in participants_to_update { + let mut update = build_channels_update(channels, vec![]); + update.delete_channels = moved_channels.clone(); + for connection_id in connection_pool.user_connection_ids(user_id) { + session.peer.send(connection_id, update.clone())?; + } + } + + for user_id in participants_to_remove { + let update = proto::UpdateChannels { + delete_channels: moved_channels.clone(), + ..Default::default() + }; + for connection_id in connection_pool.user_connection_ids(user_id) { + session.peer.send(connection_id, update.clone())?; + } + } Ok(()) } @@ -2620,81 +2543,39 @@ async fn respond_to_channel_invite( ) -> Result<()> { let db = session.db().await; let channel_id = ChannelId::from_proto(request.channel_id); - let notifications = db + let RespondToChannelInvite { + membership_update, + notifications, + } = db .respond_to_channel_invite(channel_id, session.user_id, request.accept) .await?; - if request.accept { - channel_membership_updated(db, channel_id, &session).await?; + let connection_pool = session.connection_pool().await; + if let Some(membership_update) = membership_update { + notify_membership_updated( + &connection_pool, + membership_update, + session.user_id, + &session.peer, + ); } else { - let mut update = proto::UpdateChannels::default(); - update - .remove_channel_invitations - .push(channel_id.to_proto()); - session.peer.send(session.connection_id, update)?; - } + let update = proto::UpdateChannels { + remove_channel_invitations: vec![channel_id.to_proto()], + ..Default::default() + }; + + for connection_id in connection_pool.user_connection_ids(session.user_id) { + session.peer.send(connection_id, update.clone())?; + } + }; + + send_notifications(&*connection_pool, &session.peer, notifications); - send_notifications( - &*session.connection_pool().await, - &session.peer, - notifications, - ); response.send(proto::Ack {})?; Ok(()) } -async fn channel_membership_updated( - db: tokio::sync::MutexGuard<'_, DbHandle>, - channel_id: ChannelId, - session: &Session, -) -> Result<(), crate::Error> { - let mut update = proto::UpdateChannels::default(); - update - .remove_channel_invitations - .push(channel_id.to_proto()); - - let result = db.get_channel_for_user(channel_id, session.user_id).await?; - update.channels.extend( - result - .channels - .channels - .into_iter() - .map(|channel| proto::Channel { - id: channel.id.to_proto(), - visibility: channel.visibility.into(), - name: channel.name, - }), - ); - update.unseen_channel_messages = result.channel_messages; - update.unseen_channel_buffer_changes = result.unseen_buffer_changes; - update.insert_edge = result.channels.edges; - update - .channel_participants - .extend( - result - .channel_participants - .into_iter() - .map(|(channel_id, user_ids)| proto::ChannelParticipants { - channel_id: channel_id.to_proto(), - participant_user_ids: user_ids.into_iter().map(UserId::to_proto).collect(), - }), - ); - update - .channel_permissions - .extend( - result - .channels_with_admin_privileges - .into_iter() - .map(|channel_id| proto::ChannelPermission { - channel_id: channel_id.to_proto(), - role: proto::ChannelRole::Admin.into(), - }), - ); - session.peer.send(session.connection_id, update)?; - Ok(()) -} - async fn join_channel( request: proto::JoinChannel, response: Response, @@ -2727,7 +2608,7 @@ async fn join_channel_internal( leave_room_for_session(&session).await?; let db = session.db().await; - let (joined_room, joined_channel) = db + let (joined_room, membership_updated, role) = db .join_channel( channel_id, session.user_id, @@ -2737,16 +2618,32 @@ async fn join_channel_internal( .await?; let live_kit_connection_info = session.live_kit_client.as_ref().and_then(|live_kit| { - let token = live_kit - .room_token( - &joined_room.room.live_kit_room, - &session.user_id.to_string(), + let (can_publish, token) = if role == ChannelRole::Guest { + ( + false, + live_kit + .guest_token( + &joined_room.room.live_kit_room, + &session.user_id.to_string(), + ) + .trace_err()?, ) - .trace_err()?; + } else { + ( + true, + live_kit + .room_token( + &joined_room.room.live_kit_room, + &session.user_id.to_string(), + ) + .trace_err()?, + ) + }; Some(LiveKitConnectionInfo { server_url: live_kit.url().into(), token, + can_publish, }) }); @@ -2756,8 +2653,14 @@ async fn join_channel_internal( live_kit_connection_info, })?; - if let Some(joined_channel) = joined_channel { - channel_membership_updated(db, joined_channel, &session).await? + let connection_pool = session.connection_pool().await; + if let Some(membership_updated) = membership_updated { + notify_membership_updated( + &connection_pool, + membership_updated, + session.user_id, + &session.peer, + ); } room_updated(&joined_room.room, &session.peer); @@ -3281,23 +3184,37 @@ fn to_tungstenite_message(message: AxumMessage) -> TungsteniteMessage { } } -fn build_initial_channels_update( +fn notify_membership_updated( + connection_pool: &ConnectionPool, + result: MembershipUpdated, + user_id: UserId, + peer: &Peer, +) { + let mut update = build_channels_update(result.new_channels, vec![]); + update.delete_channels = result + .removed_channels + .into_iter() + .map(|id| id.to_proto()) + .collect(); + update.remove_channel_invitations = vec![result.channel_id.to_proto()]; + + for connection_id in connection_pool.user_connection_ids(user_id) { + peer.send(connection_id, update.clone()).trace_err(); + } +} + +fn build_channels_update( channels: ChannelsForUser, channel_invites: Vec, ) -> proto::UpdateChannels { let mut update = proto::UpdateChannels::default(); - for channel in channels.channels.channels { - update.channels.push(proto::Channel { - id: channel.id.to_proto(), - name: channel.name, - visibility: channel.visibility.into(), - }); + for channel in channels.channels { + update.channels.push(channel.to_proto()); } update.unseen_channel_buffer_changes = channels.unseen_buffer_changes; update.unseen_channel_messages = channels.channel_messages; - update.insert_edge = channels.channels.edges; for (channel_id, participants) in channels.channel_participants { update @@ -3308,25 +3225,8 @@ fn build_initial_channels_update( }); } - update - .channel_permissions - .extend( - channels - .channels_with_admin_privileges - .into_iter() - .map(|id| proto::ChannelPermission { - channel_id: id.to_proto(), - role: proto::ChannelRole::Admin.into(), - }), - ); - for channel in channel_invites { - update.channel_invitations.push(proto::Channel { - id: channel.id.to_proto(), - name: channel.name, - // TODO: Visibility - visibility: ChannelVisibility::Public.into(), - }); + update.channel_invitations.push(channel.to_proto()); } update diff --git a/crates/collab/src/tests.rs b/crates/collab/src/tests.rs index 139910e1f6..e8da66a75a 100644 --- a/crates/collab/src/tests.rs +++ b/crates/collab/src/tests.rs @@ -40,3 +40,7 @@ fn room_participants(room: &ModelHandle, cx: &mut TestAppContext) -> RoomP RoomParticipants { remote, pending } }) } + +fn channel_id(room: &ModelHandle, cx: &mut TestAppContext) -> Option { + cx.read(|cx| room.read(cx).channel_id()) +} diff --git a/crates/collab/src/tests/channel_buffer_tests.rs b/crates/collab/src/tests/channel_buffer_tests.rs index 14ae159ab8..5ca40a3c2d 100644 --- a/crates/collab/src/tests/channel_buffer_tests.rs +++ b/crates/collab/src/tests/channel_buffer_tests.rs @@ -3,7 +3,7 @@ use crate::{ tests::TestServer, }; use call::ActiveCall; -use channel::{Channel, ACKNOWLEDGE_DEBOUNCE_INTERVAL}; +use channel::ACKNOWLEDGE_DEBOUNCE_INTERVAL; use client::ParticipantIndex; use client::{Collaborator, UserId}; use collab_ui::channel_view::ChannelView; @@ -11,10 +11,7 @@ use collections::HashMap; use editor::{Anchor, Editor, ToOffset}; use futures::future; use gpui::{executor::Deterministic, ModelHandle, TestAppContext, ViewContext}; -use rpc::{ - proto::{self, PeerId}, - RECEIVE_TIMEOUT, -}; +use rpc::{proto::PeerId, RECEIVE_TIMEOUT}; use serde_json::json; use std::{ops::Range, sync::Arc}; @@ -410,11 +407,8 @@ async fn test_channel_buffer_disconnect( server.disconnect_client(client_a.peer_id().unwrap()); deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); - channel_buffer_a.update(cx_a, |buffer, _| { - assert_eq!( - buffer.channel().as_ref(), - &channel(channel_id, "the-channel") - ); + channel_buffer_a.update(cx_a, |buffer, cx| { + assert_eq!(buffer.channel(cx).unwrap().name, "the-channel"); assert!(!buffer.is_connected()); }); @@ -435,25 +429,12 @@ async fn test_channel_buffer_disconnect( deterministic.run_until_parked(); // Channel buffer observed the deletion - channel_buffer_b.update(cx_b, |buffer, _| { - assert_eq!( - buffer.channel().as_ref(), - &channel(channel_id, "the-channel") - ); + channel_buffer_b.update(cx_b, |buffer, cx| { + assert!(buffer.channel(cx).is_none()); assert!(!buffer.is_connected()); }); } -fn channel(id: u64, name: &'static str) -> Channel { - Channel { - id, - name: name.to_string(), - visibility: proto::ChannelVisibility::Members, - unseen_note_version: None, - unseen_message_id: None, - } -} - #[gpui::test] async fn test_rejoin_channel_buffer( deterministic: Arc, @@ -698,7 +679,7 @@ async fn test_following_to_channel_notes_without_a_shared_project( .await .unwrap(); channel_view_1_a.update(cx_a, |notes, cx| { - assert_eq!(notes.channel(cx).name, "channel-1"); + assert_eq!(notes.channel(cx).unwrap().name, "channel-1"); notes.editor.update(cx, |editor, cx| { editor.insert("Hello from A.", cx); editor.change_selections(None, cx, |selections| { @@ -730,7 +711,7 @@ async fn test_following_to_channel_notes_without_a_shared_project( .expect("active item is not a channel view") }); channel_view_1_b.read_with(cx_b, |notes, cx| { - assert_eq!(notes.channel(cx).name, "channel-1"); + assert_eq!(notes.channel(cx).unwrap().name, "channel-1"); let editor = notes.editor.read(cx); assert_eq!(editor.text(cx), "Hello from A."); assert_eq!(editor.selections.ranges::(cx), &[3..4]); @@ -742,7 +723,7 @@ async fn test_following_to_channel_notes_without_a_shared_project( .await .unwrap(); channel_view_2_a.read_with(cx_a, |notes, cx| { - assert_eq!(notes.channel(cx).name, "channel-2"); + assert_eq!(notes.channel(cx).unwrap().name, "channel-2"); }); // Client B is taken to the notes for channel 2. @@ -759,7 +740,7 @@ async fn test_following_to_channel_notes_without_a_shared_project( .expect("active item is not a channel view") }); channel_view_2_b.read_with(cx_b, |notes, cx| { - assert_eq!(notes.channel(cx).name, "channel-2"); + assert_eq!(notes.channel(cx).unwrap().name, "channel-2"); }); } diff --git a/crates/collab/src/tests/channel_tests.rs b/crates/collab/src/tests/channel_tests.rs index 54a958e71c..a33ded6492 100644 --- a/crates/collab/src/tests/channel_tests.rs +++ b/crates/collab/src/tests/channel_tests.rs @@ -1,10 +1,12 @@ use crate::{ + db::{self, UserId}, rpc::RECONNECT_TIMEOUT, tests::{room_participants, RoomParticipants, TestServer}, }; use call::ActiveCall; use channel::{ChannelId, ChannelMembership, ChannelStore}; use client::User; +use futures::future::try_join_all; use gpui::{executor::Deterministic, ModelHandle, TestAppContext}; use rpc::{ proto::{self, ChannelRole}, @@ -47,22 +49,19 @@ async fn test_core_channels( id: channel_a_id, name: "channel-a".to_string(), depth: 0, - user_is_admin: true, + role: ChannelRole::Admin, }, ExpectedChannel { id: channel_b_id, name: "channel-b".to_string(), depth: 1, - user_is_admin: true, + role: ChannelRole::Admin, }, ], ); client_b.channel_store().read_with(cx_b, |channels, _| { - assert!(channels - .channel_dag_entries() - .collect::>() - .is_empty()) + assert!(channels.ordered_channels().collect::>().is_empty()) }); // Invite client B to channel A as client A. @@ -94,7 +93,7 @@ async fn test_core_channels( id: channel_a_id, name: "channel-a".to_string(), depth: 0, - user_is_admin: false, + role: ChannelRole::Member, }], ); @@ -141,13 +140,13 @@ async fn test_core_channels( ExpectedChannel { id: channel_a_id, name: "channel-a".to_string(), - user_is_admin: false, + role: ChannelRole::Member, depth: 0, }, ExpectedChannel { id: channel_b_id, name: "channel-b".to_string(), - user_is_admin: false, + role: ChannelRole::Member, depth: 1, }, ], @@ -169,19 +168,19 @@ async fn test_core_channels( ExpectedChannel { id: channel_a_id, name: "channel-a".to_string(), - user_is_admin: false, + role: ChannelRole::Member, depth: 0, }, ExpectedChannel { id: channel_b_id, name: "channel-b".to_string(), - user_is_admin: false, + role: ChannelRole::Member, depth: 1, }, ExpectedChannel { id: channel_c_id, name: "channel-c".to_string(), - user_is_admin: false, + role: ChannelRole::Member, depth: 2, }, ], @@ -213,19 +212,19 @@ async fn test_core_channels( id: channel_a_id, name: "channel-a".to_string(), depth: 0, - user_is_admin: true, + role: ChannelRole::Admin, }, ExpectedChannel { id: channel_b_id, name: "channel-b".to_string(), depth: 1, - user_is_admin: true, + role: ChannelRole::Admin, }, ExpectedChannel { id: channel_c_id, name: "channel-c".to_string(), depth: 2, - user_is_admin: true, + role: ChannelRole::Admin, }, ], ); @@ -247,7 +246,7 @@ async fn test_core_channels( id: channel_a_id, name: "channel-a".to_string(), depth: 0, - user_is_admin: true, + role: ChannelRole::Admin, }], ); assert_channels( @@ -257,7 +256,7 @@ async fn test_core_channels( id: channel_a_id, name: "channel-a".to_string(), depth: 0, - user_is_admin: true, + role: ChannelRole::Admin, }], ); @@ -280,18 +279,27 @@ async fn test_core_channels( id: channel_a_id, name: "channel-a".to_string(), depth: 0, - user_is_admin: true, + role: ChannelRole::Admin, }], ); // Client B no longer has access to the channel assert_channels(client_b.channel_store(), cx_b, &[]); - // When disconnected, client A sees no channels. server.forbid_connections(); server.disconnect_client(client_a.peer_id().unwrap()); deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); - assert_channels(client_a.channel_store(), cx_a, &[]); + + server + .app_state + .db + .rename_channel( + db::ChannelId::from_proto(channel_a_id), + UserId::from_proto(client_a.id()), + "channel-a-renamed", + ) + .await + .unwrap(); server.allow_connections(); deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); @@ -300,9 +308,9 @@ async fn test_core_channels( cx_a, &[ExpectedChannel { id: channel_a_id, - name: "channel-a".to_string(), + name: "channel-a-renamed".to_string(), depth: 0, - user_is_admin: true, + role: ChannelRole::Admin, }], ); } @@ -410,7 +418,7 @@ async fn test_channel_room( id: zed_id, name: "zed".to_string(), depth: 0, - user_is_admin: false, + role: ChannelRole::Member, }], ); client_b.channel_store().read_with(cx_b, |channels, _| { @@ -643,7 +651,7 @@ async fn test_permissions_update_while_invited( depth: 0, id: rust_id, name: "rust".to_string(), - user_is_admin: false, + role: ChannelRole::Member, }], ); assert_channels(client_b.channel_store(), cx_b, &[]); @@ -671,7 +679,7 @@ async fn test_permissions_update_while_invited( depth: 0, id: rust_id, name: "rust".to_string(), - user_is_admin: false, + role: ChannelRole::Member, }], ); assert_channels(client_b.channel_store(), cx_b, &[]); @@ -711,7 +719,7 @@ async fn test_channel_rename( depth: 0, id: rust_id, name: "rust-archive".to_string(), - user_is_admin: true, + role: ChannelRole::Admin, }], ); @@ -723,7 +731,7 @@ async fn test_channel_rename( depth: 0, id: rust_id, name: "rust-archive".to_string(), - user_is_admin: false, + role: ChannelRole::Member, }], ); } @@ -846,7 +854,7 @@ async fn test_lost_channel_creation( depth: 0, id: channel_id, name: "x".to_string(), - user_is_admin: false, + role: ChannelRole::Member, }], ); @@ -870,13 +878,13 @@ async fn test_lost_channel_creation( depth: 0, id: channel_id, name: "x".to_string(), - user_is_admin: true, + role: ChannelRole::Admin, }, ExpectedChannel { depth: 1, id: subchannel_id, name: "subchannel".to_string(), - user_is_admin: true, + role: ChannelRole::Admin, }, ], ); @@ -901,17 +909,327 @@ async fn test_lost_channel_creation( depth: 0, id: channel_id, name: "x".to_string(), - user_is_admin: false, + role: ChannelRole::Member, }, ExpectedChannel { depth: 1, id: subchannel_id, name: "subchannel".to_string(), - user_is_admin: false, + role: ChannelRole::Member, }, ], ); } + +#[gpui::test] +async fn test_channel_link_notifications( + deterministic: Arc, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, + cx_c: &mut TestAppContext, +) { + deterministic.forbid_parking(); + + let mut server = TestServer::start(&deterministic).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + let client_c = server.create_client(cx_c, "user_c").await; + + let user_b = client_b.user_id().unwrap(); + let user_c = client_c.user_id().unwrap(); + + let channels = server + .make_channel_tree(&[("zed", None)], (&client_a, cx_a)) + .await; + let zed_channel = channels[0]; + + try_join_all(client_a.channel_store().update(cx_a, |channel_store, cx| { + [ + channel_store.set_channel_visibility(zed_channel, proto::ChannelVisibility::Public, cx), + channel_store.invite_member(zed_channel, user_b, proto::ChannelRole::Member, cx), + channel_store.invite_member(zed_channel, user_c, proto::ChannelRole::Guest, cx), + ] + })) + .await + .unwrap(); + + deterministic.run_until_parked(); + + client_b + .channel_store() + .update(cx_b, |channel_store, cx| { + channel_store.respond_to_channel_invite(zed_channel, true, cx) + }) + .await + .unwrap(); + + client_c + .channel_store() + .update(cx_c, |channel_store, cx| { + channel_store.respond_to_channel_invite(zed_channel, true, cx) + }) + .await + .unwrap(); + + deterministic.run_until_parked(); + + // we have an admin (a), member (b) and guest (c) all part of the zed channel. + + // create a new private channel, make it public, and move it under the previous one, and verify it shows for b and not c + let active_channel = client_a + .channel_store() + .update(cx_a, |channel_store, cx| { + channel_store.create_channel("active", Some(zed_channel), cx) + }) + .await + .unwrap(); + + // the new channel shows for b and not c + assert_channels_list_shape( + client_a.channel_store(), + cx_a, + &[(zed_channel, 0), (active_channel, 1)], + ); + assert_channels_list_shape( + client_b.channel_store(), + cx_b, + &[(zed_channel, 0), (active_channel, 1)], + ); + assert_channels_list_shape(client_c.channel_store(), cx_c, &[(zed_channel, 0)]); + + let vim_channel = client_a + .channel_store() + .update(cx_a, |channel_store, cx| { + channel_store.create_channel("vim", None, cx) + }) + .await + .unwrap(); + + client_a + .channel_store() + .update(cx_a, |channel_store, cx| { + channel_store.set_channel_visibility(vim_channel, proto::ChannelVisibility::Public, cx) + }) + .await + .unwrap(); + + client_a + .channel_store() + .update(cx_a, |channel_store, cx| { + channel_store.move_channel(vim_channel, Some(active_channel), cx) + }) + .await + .unwrap(); + + deterministic.run_until_parked(); + + // the new channel shows for b and c + assert_channels_list_shape( + client_a.channel_store(), + cx_a, + &[(zed_channel, 0), (active_channel, 1), (vim_channel, 2)], + ); + assert_channels_list_shape( + client_b.channel_store(), + cx_b, + &[(zed_channel, 0), (active_channel, 1), (vim_channel, 2)], + ); + assert_channels_list_shape( + client_c.channel_store(), + cx_c, + &[(zed_channel, 0), (vim_channel, 1)], + ); + + let helix_channel = client_a + .channel_store() + .update(cx_a, |channel_store, cx| { + channel_store.create_channel("helix", None, cx) + }) + .await + .unwrap(); + + client_a + .channel_store() + .update(cx_a, |channel_store, cx| { + channel_store.move_channel(helix_channel, Some(vim_channel), cx) + }) + .await + .unwrap(); + + client_a + .channel_store() + .update(cx_a, |channel_store, cx| { + channel_store.set_channel_visibility( + helix_channel, + proto::ChannelVisibility::Public, + cx, + ) + }) + .await + .unwrap(); + + // the new channel shows for b and c + assert_channels_list_shape( + client_b.channel_store(), + cx_b, + &[ + (zed_channel, 0), + (active_channel, 1), + (vim_channel, 2), + (helix_channel, 3), + ], + ); + assert_channels_list_shape( + client_c.channel_store(), + cx_c, + &[(zed_channel, 0), (vim_channel, 1), (helix_channel, 2)], + ); + + client_a + .channel_store() + .update(cx_a, |channel_store, cx| { + channel_store.set_channel_visibility(vim_channel, proto::ChannelVisibility::Members, cx) + }) + .await + .unwrap(); + + // the members-only channel is still shown for c, but hidden for b + assert_channels_list_shape( + client_b.channel_store(), + cx_b, + &[ + (zed_channel, 0), + (active_channel, 1), + (vim_channel, 2), + (helix_channel, 3), + ], + ); + client_b + .channel_store() + .read_with(cx_b, |channel_store, _| { + assert_eq!( + channel_store + .channel_for_id(vim_channel) + .unwrap() + .visibility, + proto::ChannelVisibility::Members + ) + }); + + assert_channels_list_shape(client_c.channel_store(), cx_c, &[(zed_channel, 0)]); +} + +#[gpui::test] +async fn test_channel_membership_notifications( + deterministic: Arc, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + deterministic.forbid_parking(); + + deterministic.forbid_parking(); + + let mut server = TestServer::start(&deterministic).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_c").await; + + let user_b = client_b.user_id().unwrap(); + + let channels = server + .make_channel_tree( + &[ + ("zed", None), + ("active", Some("zed")), + ("vim", Some("active")), + ], + (&client_a, cx_a), + ) + .await; + let zed_channel = channels[0]; + let _active_channel = channels[1]; + let vim_channel = channels[2]; + + try_join_all(client_a.channel_store().update(cx_a, |channel_store, cx| { + [ + channel_store.set_channel_visibility(zed_channel, proto::ChannelVisibility::Public, cx), + channel_store.set_channel_visibility(vim_channel, proto::ChannelVisibility::Public, cx), + channel_store.invite_member(vim_channel, user_b, proto::ChannelRole::Member, cx), + channel_store.invite_member(zed_channel, user_b, proto::ChannelRole::Guest, cx), + ] + })) + .await + .unwrap(); + + deterministic.run_until_parked(); + + client_b + .channel_store() + .update(cx_b, |channel_store, cx| { + channel_store.respond_to_channel_invite(zed_channel, true, cx) + }) + .await + .unwrap(); + + client_b + .channel_store() + .update(cx_b, |channel_store, cx| { + channel_store.respond_to_channel_invite(vim_channel, true, cx) + }) + .await + .unwrap(); + + deterministic.run_until_parked(); + + // we have an admin (a), and a guest (b) with access to all of zed, and membership in vim. + assert_channels( + client_b.channel_store(), + cx_b, + &[ + ExpectedChannel { + depth: 0, + id: zed_channel, + name: "zed".to_string(), + role: ChannelRole::Guest, + }, + ExpectedChannel { + depth: 1, + id: vim_channel, + name: "vim".to_string(), + role: ChannelRole::Member, + }, + ], + ); + + client_a + .channel_store() + .update(cx_a, |channel_store, cx| { + channel_store.remove_member(vim_channel, user_b, cx) + }) + .await + .unwrap(); + + deterministic.run_until_parked(); + + assert_channels( + client_b.channel_store(), + cx_b, + &[ + ExpectedChannel { + depth: 0, + id: zed_channel, + name: "zed".to_string(), + role: ChannelRole::Guest, + }, + ExpectedChannel { + depth: 1, + id: vim_channel, + name: "vim".to_string(), + role: ChannelRole::Guest, + }, + ], + ) +} + #[gpui::test] async fn test_guest_access( deterministic: Arc, @@ -925,44 +1243,79 @@ async fn test_guest_access( let client_b = server.create_client(cx_b, "user_b").await; let channels = server - .make_channel_tree(&[("channel-a", None)], (&client_a, cx_a)) + .make_channel_tree( + &[("channel-a", None), ("channel-b", Some("channel-a"))], + (&client_a, cx_a), + ) .await; - let channel_a_id = channels[0]; + let channel_a = channels[0]; + let channel_b = channels[1]; let active_call_b = cx_b.read(ActiveCall::global); - // should not be allowed to join + // Non-members should not be allowed to join assert!(active_call_b - .update(cx_b, |call, cx| call.join_channel(channel_a_id, cx)) + .update(cx_b, |call, cx| call.join_channel(channel_a, cx)) .await .is_err()); + // Make channels A and B public client_a .channel_store() .update(cx_a, |channel_store, cx| { - channel_store.set_channel_visibility(channel_a_id, proto::ChannelVisibility::Public, cx) + channel_store.set_channel_visibility(channel_a, proto::ChannelVisibility::Public, cx) + }) + .await + .unwrap(); + client_a + .channel_store() + .update(cx_a, |channel_store, cx| { + channel_store.set_channel_visibility(channel_b, proto::ChannelVisibility::Public, cx) }) .await .unwrap(); + // Client B joins channel A as a guest active_call_b - .update(cx_b, |call, cx| call.join_channel(channel_a_id, cx)) + .update(cx_b, |call, cx| call.join_channel(channel_a, cx)) .await .unwrap(); deterministic.run_until_parked(); - - assert!(client_b - .channel_store() - .update(cx_b, |channel_store, _| channel_store - .channel_for_id(channel_a_id) - .is_some())); + assert_channels_list_shape( + client_a.channel_store(), + cx_a, + &[(channel_a, 0), (channel_b, 1)], + ); + assert_channels_list_shape( + client_b.channel_store(), + cx_b, + &[(channel_a, 0), (channel_b, 1)], + ); client_a.channel_store().update(cx_a, |channel_store, _| { - let participants = channel_store.channel_participants(channel_a_id); + let participants = channel_store.channel_participants(channel_a); assert_eq!(participants.len(), 1); assert_eq!(participants[0].id, client_b.user_id().unwrap()); - }) + }); + + client_a + .channel_store() + .update(cx_a, |channel_store, cx| { + channel_store.set_channel_visibility(channel_a, proto::ChannelVisibility::Members, cx) + }) + .await + .unwrap(); + + assert_channels_list_shape(client_b.channel_store(), cx_b, &[]); + + active_call_b + .update(cx_b, |call, cx| call.join_channel(channel_b, cx)) + .await + .unwrap(); + + deterministic.run_until_parked(); + assert_channels_list_shape(client_b.channel_store(), cx_b, &[(channel_b, 0)]); } #[gpui::test] @@ -1030,14 +1383,14 @@ async fn test_invite_access( async fn test_channel_moving( deterministic: Arc, cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, - cx_c: &mut TestAppContext, + _cx_b: &mut TestAppContext, + _cx_c: &mut TestAppContext, ) { deterministic.forbid_parking(); let mut server = TestServer::start(&deterministic).await; let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; - let client_c = server.create_client(cx_c, "user_c").await; + // let client_b = server.create_client(cx_b, "user_b").await; + // let client_c = server.create_client(cx_c, "user_c").await; let channels = server .make_channel_tree( @@ -1071,7 +1424,7 @@ async fn test_channel_moving( client_a .channel_store() .update(cx_a, |channel_store, cx| { - channel_store.move_channel(channel_d_id, channel_c_id, channel_b_id, cx) + channel_store.move_channel(channel_d_id, Some(channel_b_id), cx) }) .await .unwrap(); @@ -1089,188 +1442,6 @@ async fn test_channel_moving( (channel_d_id, 2), ], ); - - client_a - .channel_store() - .update(cx_a, |channel_store, cx| { - channel_store.link_channel(channel_d_id, channel_c_id, cx) - }) - .await - .unwrap(); - - // Current shape for A: - // /------\ - // a - b -- c -- d - assert_channels_list_shape( - client_a.channel_store(), - cx_a, - &[ - (channel_a_id, 0), - (channel_b_id, 1), - (channel_c_id, 2), - (channel_d_id, 3), - (channel_d_id, 2), - ], - ); - - let b_channels = server - .make_channel_tree( - &[ - ("channel-mu", None), - ("channel-gamma", Some("channel-mu")), - ("channel-epsilon", Some("channel-mu")), - ], - (&client_b, cx_b), - ) - .await; - let channel_mu_id = b_channels[0]; - let channel_ga_id = b_channels[1]; - let channel_ep_id = b_channels[2]; - - // Current shape for B: - // /- ep - // mu -- ga - assert_channels_list_shape( - client_b.channel_store(), - cx_b, - &[(channel_mu_id, 0), (channel_ep_id, 1), (channel_ga_id, 1)], - ); - - client_a - .add_admin_to_channel((&client_b, cx_b), channel_b_id, cx_a) - .await; - - // Current shape for B: - // /- ep - // mu -- ga - // /---------\ - // b -- c -- d - assert_channels_list_shape( - client_b.channel_store(), - cx_b, - &[ - // New channels from a - (channel_b_id, 0), - (channel_c_id, 1), - (channel_d_id, 2), - (channel_d_id, 1), - // B's old channels - (channel_mu_id, 0), - (channel_ep_id, 1), - (channel_ga_id, 1), - ], - ); - - client_b - .add_admin_to_channel((&client_c, cx_c), channel_ep_id, cx_b) - .await; - - // Current shape for C: - // - ep - assert_channels_list_shape(client_c.channel_store(), cx_c, &[(channel_ep_id, 0)]); - - client_b - .channel_store() - .update(cx_b, |channel_store, cx| { - channel_store.link_channel(channel_b_id, channel_ep_id, cx) - }) - .await - .unwrap(); - - // Current shape for B: - // /---------\ - // /- ep -- b -- c -- d - // mu -- ga - assert_channels_list_shape( - client_b.channel_store(), - cx_b, - &[ - (channel_mu_id, 0), - (channel_ep_id, 1), - (channel_b_id, 2), - (channel_c_id, 3), - (channel_d_id, 4), - (channel_d_id, 3), - (channel_ga_id, 1), - ], - ); - - // Current shape for C: - // /---------\ - // ep -- b -- c -- d - assert_channels_list_shape( - client_c.channel_store(), - cx_c, - &[ - (channel_ep_id, 0), - (channel_b_id, 1), - (channel_c_id, 2), - (channel_d_id, 3), - (channel_d_id, 2), - ], - ); - - client_b - .channel_store() - .update(cx_b, |channel_store, cx| { - channel_store.link_channel(channel_ga_id, channel_b_id, cx) - }) - .await - .unwrap(); - - // Current shape for B: - // /---------\ - // /- ep -- b -- c -- d - // / \ - // mu ---------- ga - assert_channels_list_shape( - client_b.channel_store(), - cx_b, - &[ - (channel_mu_id, 0), - (channel_ep_id, 1), - (channel_b_id, 2), - (channel_c_id, 3), - (channel_d_id, 4), - (channel_d_id, 3), - (channel_ga_id, 3), - (channel_ga_id, 1), - ], - ); - - // Current shape for A: - // /------\ - // a - b -- c -- d - // \-- ga - assert_channels_list_shape( - client_a.channel_store(), - cx_a, - &[ - (channel_a_id, 0), - (channel_b_id, 1), - (channel_c_id, 2), - (channel_d_id, 3), - (channel_d_id, 2), - (channel_ga_id, 2), - ], - ); - - // Current shape for C: - // /-------\ - // ep -- b -- c -- d - // \-- ga - assert_channels_list_shape( - client_c.channel_store(), - cx_c, - &[ - (channel_ep_id, 0), - (channel_b_id, 1), - (channel_c_id, 2), - (channel_d_id, 3), - (channel_d_id, 2), - (channel_ga_id, 2), - ], - ); } #[derive(Debug, PartialEq)] @@ -1278,7 +1449,7 @@ struct ExpectedChannel { depth: usize, id: ChannelId, name: String, - user_is_admin: bool, + role: ChannelRole, } #[track_caller] @@ -1295,7 +1466,7 @@ fn assert_channel_invitations( depth: 0, name: channel.name.clone(), id: channel.id, - user_is_admin: store.is_user_admin(channel.id), + role: channel.role, }) .collect::>() }); @@ -1310,12 +1481,12 @@ fn assert_channels( ) { let actual = channel_store.read_with(cx, |store, _| { store - .channel_dag_entries() + .ordered_channels() .map(|(depth, channel)| ExpectedChannel { depth, name: channel.name.clone(), id: channel.id, - user_is_admin: store.is_user_admin(channel.id), + role: channel.role, }) .collect::>() }); @@ -1332,7 +1503,7 @@ fn assert_channels_list_shape( let actual = channel_store.read_with(cx, |store, _| { store - .channel_dag_entries() + .ordered_channels() .map(|(depth, channel)| (channel.id, depth)) .collect::>() }); diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index 8396e8947f..550c3a2bd8 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -1,6 +1,6 @@ use crate::{ rpc::{CLEANUP_TIMEOUT, RECONNECT_TIMEOUT}, - tests::{room_participants, RoomParticipants, TestClient, TestServer}, + tests::{channel_id, room_participants, RoomParticipants, TestClient, TestServer}, }; use call::{room, ActiveCall, ParticipantLocation, Room}; use client::{User, RECEIVE_TIMEOUT}; @@ -469,6 +469,119 @@ async fn test_calling_multiple_users_simultaneously( ); } +#[gpui::test(iterations = 10)] +async fn test_joining_channels_and_calling_multiple_users_simultaneously( + deterministic: Arc, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, + cx_c: &mut TestAppContext, +) { + deterministic.forbid_parking(); + let mut server = TestServer::start(&deterministic).await; + + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + let client_c = server.create_client(cx_c, "user_c").await; + server + .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)]) + .await; + + let channel_1 = server + .make_channel( + "channel1", + None, + (&client_a, cx_a), + &mut [(&client_b, cx_b), (&client_c, cx_c)], + ) + .await; + + let channel_2 = server + .make_channel( + "channel2", + None, + (&client_a, cx_a), + &mut [(&client_b, cx_b), (&client_c, cx_c)], + ) + .await; + + let active_call_a = cx_a.read(ActiveCall::global); + + // Simultaneously join channel 1 and then channel 2 + active_call_a + .update(cx_a, |call, cx| call.join_channel(channel_1, cx)) + .detach(); + let join_channel_2 = active_call_a.update(cx_a, |call, cx| call.join_channel(channel_2, cx)); + + join_channel_2.await.unwrap(); + + let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone()); + deterministic.run_until_parked(); + + assert_eq!(channel_id(&room_a, cx_a), Some(channel_2)); + + // Leave the room + active_call_a + .update(cx_a, |call, cx| { + let hang_up = call.hang_up(cx); + hang_up + }) + .await + .unwrap(); + + // Initiating invites and then joining a channel should fail gracefully + let b_invite = active_call_a.update(cx_a, |call, cx| { + call.invite(client_b.user_id().unwrap(), None, cx) + }); + let c_invite = active_call_a.update(cx_a, |call, cx| { + call.invite(client_c.user_id().unwrap(), None, cx) + }); + + let join_channel = active_call_a.update(cx_a, |call, cx| call.join_channel(channel_1, cx)); + + b_invite.await.unwrap(); + c_invite.await.unwrap(); + join_channel.await.unwrap(); + + let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone()); + deterministic.run_until_parked(); + + assert_eq!( + room_participants(&room_a, cx_a), + RoomParticipants { + remote: Default::default(), + pending: vec!["user_b".to_string(), "user_c".to_string()] + } + ); + + assert_eq!(channel_id(&room_a, cx_a), None); + + // Leave the room + active_call_a + .update(cx_a, |call, cx| { + let hang_up = call.hang_up(cx); + hang_up + }) + .await + .unwrap(); + + // Simultaneously join channel 1 and call user B and user C from client A. + let join_channel = active_call_a.update(cx_a, |call, cx| call.join_channel(channel_1, cx)); + + let b_invite = active_call_a.update(cx_a, |call, cx| { + call.invite(client_b.user_id().unwrap(), None, cx) + }); + let c_invite = active_call_a.update(cx_a, |call, cx| { + call.invite(client_c.user_id().unwrap(), None, cx) + }); + + join_channel.await.unwrap(); + b_invite.await.unwrap(); + c_invite.await.unwrap(); + + active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone()); + deterministic.run_until_parked(); +} + #[gpui::test(iterations = 10)] async fn test_room_uniqueness( deterministic: Arc, @@ -4555,11 +4668,7 @@ async fn test_prettier_formatting_buffer( .insert_tree(&directory, json!({ "a.rs": buffer_text })) .await; let (project_a, worktree_id) = client_a.build_local_project(&directory, cx_a).await; - let prettier_format_suffix = project_a.update(cx_a, |project, _| { - let suffix = project.enable_test_prettier(&[test_plugin]); - project.languages().add(language); - suffix - }); + let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX; let buffer_a = cx_a .background() .spawn(project_a.update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx))) diff --git a/crates/collab/src/tests/random_channel_buffer_tests.rs b/crates/collab/src/tests/random_channel_buffer_tests.rs index 1b24c7a3d2..38bc3f7c12 100644 --- a/crates/collab/src/tests/random_channel_buffer_tests.rs +++ b/crates/collab/src/tests/random_channel_buffer_tests.rs @@ -48,7 +48,7 @@ impl RandomizedTest for RandomChannelBufferTest { let db = &server.app_state.db; for ix in 0..CHANNEL_COUNT { let id = db - .create_channel(&format!("channel-{ix}"), None, users[0].user_id) + .create_root_channel(&format!("channel-{ix}"), users[0].user_id) .await .unwrap(); for user in &users[1..] { @@ -83,7 +83,7 @@ impl RandomizedTest for RandomChannelBufferTest { match rng.gen_range(0..100_u32) { 0..=29 => { let channel_name = client.channel_store().read_with(cx, |store, cx| { - store.channel_dag_entries().find_map(|(_, channel)| { + store.ordered_channels().find_map(|(_, channel)| { if store.has_open_channel_buffer(channel.id, cx) { None } else { @@ -98,15 +98,16 @@ impl RandomizedTest for RandomChannelBufferTest { 30..=40 => { if let Some(buffer) = channel_buffers.iter().choose(rng) { - let channel_name = buffer.read_with(cx, |b, _| b.channel().name.clone()); + let channel_name = + buffer.read_with(cx, |b, cx| b.channel(cx).unwrap().name.clone()); break ChannelBufferOperation::LeaveChannelNotes { channel_name }; } } _ => { if let Some(buffer) = channel_buffers.iter().choose(rng) { - break buffer.read_with(cx, |b, _| { - let channel_name = b.channel().name.clone(); + break buffer.read_with(cx, |b, cx| { + let channel_name = b.channel(cx).unwrap().name.clone(); let edits = b .buffer() .read_with(cx, |buffer, _| buffer.get_random_edits(rng, 3)); @@ -130,7 +131,7 @@ impl RandomizedTest for RandomChannelBufferTest { ChannelBufferOperation::JoinChannelNotes { channel_name } => { let buffer = client.channel_store().update(cx, |store, cx| { let channel_id = store - .channel_dag_entries() + .ordered_channels() .find(|(_, c)| c.name == channel_name) .unwrap() .1 @@ -153,7 +154,7 @@ impl RandomizedTest for RandomChannelBufferTest { let buffer = cx.update(|cx| { let mut left_buffer = Err(TestError::Inapplicable); client.channel_buffers().retain(|buffer| { - if buffer.read(cx).channel().name == channel_name { + if buffer.read(cx).channel(cx).unwrap().name == channel_name { left_buffer = Ok(buffer.clone()); false } else { @@ -179,7 +180,9 @@ impl RandomizedTest for RandomChannelBufferTest { client .channel_buffers() .iter() - .find(|buffer| buffer.read(cx).channel().name == channel_name) + .find(|buffer| { + buffer.read(cx).channel(cx).unwrap().name == channel_name + }) .cloned() }) .ok_or_else(|| TestError::Inapplicable)?; @@ -250,7 +253,7 @@ impl RandomizedTest for RandomChannelBufferTest { if let Some(channel_buffer) = client .channel_buffers() .iter() - .find(|b| b.read(cx).channel().id == channel_id.to_proto()) + .find(|b| b.read(cx).channel_id == channel_id.to_proto()) { let channel_buffer = channel_buffer.read(cx); diff --git a/crates/collab/src/tests/test_server.rs b/crates/collab/src/tests/test_server.rs index f7c4fa4146..d6ebe1e84e 100644 --- a/crates/collab/src/tests/test_server.rs +++ b/crates/collab/src/tests/test_server.rs @@ -611,38 +611,6 @@ impl TestClient { ) -> WindowHandle { cx.add_window(|cx| Workspace::new(0, project.clone(), self.app_state.clone(), cx)) } - - pub async fn add_admin_to_channel( - &self, - user: (&TestClient, &mut TestAppContext), - channel: u64, - cx_self: &mut TestAppContext, - ) { - let (other_client, other_cx) = user; - - cx_self - .read(ChannelStore::global) - .update(cx_self, |channel_store, cx| { - channel_store.invite_member( - channel, - other_client.user_id().unwrap(), - ChannelRole::Admin, - cx, - ) - }) - .await - .unwrap(); - - cx_self.foreground().run_until_parked(); - - other_cx - .read(ChannelStore::global) - .update(other_cx, |channel_store, cx| { - channel_store.respond_to_channel_invite(channel, true, cx) - }) - .await - .unwrap(); - } } impl Drop for TestClient { diff --git a/crates/collab_ui/Cargo.toml b/crates/collab_ui/Cargo.toml index 8aee0da8dd..791c6b2fa7 100644 --- a/crates/collab_ui/Cargo.toml +++ b/crates/collab_ui/Cargo.toml @@ -61,6 +61,7 @@ postage.workspace = true serde.workspace = true serde_derive.workspace = true time.workspace = true +smallvec.workspace = true [dev-dependencies] call = { path = "../call", features = ["test-support"] } diff --git a/crates/collab_ui/src/channel_view.rs b/crates/collab_ui/src/channel_view.rs index e62ee8ef4b..1bdcebd018 100644 --- a/crates/collab_ui/src/channel_view.rs +++ b/crates/collab_ui/src/channel_view.rs @@ -15,13 +15,14 @@ use gpui::{ ViewContext, ViewHandle, }; use project::Project; +use smallvec::SmallVec; use std::{ any::{Any, TypeId}, sync::Arc, }; use util::ResultExt; use workspace::{ - item::{FollowableItem, Item, ItemHandle}, + item::{FollowableItem, Item, ItemEvent, ItemHandle}, register_followable_item, searchable::SearchableItemHandle, ItemNavHistory, Pane, SaveIntent, ViewId, Workspace, WorkspaceId, @@ -140,6 +141,12 @@ impl ChannelView { editor.set_collaboration_hub(Box::new(ChannelBufferCollaborationHub( channel_buffer.clone(), ))); + editor.set_read_only( + !channel_buffer + .read(cx) + .channel(cx) + .is_some_and(|c| c.can_edit_notes()), + ); editor }); let _editor_event_subscription = cx.subscribe(&editor, |_, _, e, cx| cx.emit(e.clone())); @@ -157,8 +164,8 @@ impl ChannelView { } } - pub fn channel(&self, cx: &AppContext) -> Arc { - self.channel_buffer.read(cx).channel() + pub fn channel(&self, cx: &AppContext) -> Option> { + self.channel_buffer.read(cx).channel(cx) } fn handle_channel_buffer_event( @@ -172,6 +179,13 @@ impl ChannelView { editor.set_read_only(true); cx.notify(); }), + ChannelBufferEvent::ChannelChanged => { + self.editor.update(cx, |editor, cx| { + editor.set_read_only(!self.channel(cx).is_some_and(|c| c.can_edit_notes())); + cx.emit(editor::Event::TitleChanged); + cx.notify() + }); + } ChannelBufferEvent::BufferEdited => { if cx.is_self_focused() || self.editor.is_focused(cx) { self.acknowledge_buffer_version(cx); @@ -179,7 +193,7 @@ impl ChannelView { self.channel_store.update(cx, |store, cx| { let channel_buffer = self.channel_buffer.read(cx); store.notes_changed( - channel_buffer.channel().id, + channel_buffer.channel_id, channel_buffer.epoch(), &channel_buffer.buffer().read(cx).version(), cx, @@ -187,7 +201,7 @@ impl ChannelView { }); } } - _ => {} + ChannelBufferEvent::CollaboratorsChanged => {} } } @@ -195,7 +209,7 @@ impl ChannelView { self.channel_store.update(cx, |store, cx| { let channel_buffer = self.channel_buffer.read(cx); store.acknowledge_notes_version( - channel_buffer.channel().id, + channel_buffer.channel_id, channel_buffer.epoch(), &channel_buffer.buffer().read(cx).version(), cx, @@ -250,11 +264,17 @@ impl Item for ChannelView { style: &theme::Tab, cx: &gpui::AppContext, ) -> AnyElement { - let channel_name = &self.channel_buffer.read(cx).channel().name; - let label = if self.channel_buffer.read(cx).is_connected() { - format!("#{}", channel_name) + let label = if let Some(channel) = self.channel(cx) { + match ( + channel.can_edit_notes(), + self.channel_buffer.read(cx).is_connected(), + ) { + (true, true) => format!("#{}", channel.name), + (false, true) => format!("#{} (read-only)", channel.name), + (_, false) => format!("#{} (disconnected)", channel.name), + } } else { - format!("#{} (disconnected)", channel_name) + format!("channel notes (disconnected)") }; Label::new(label, style.label.to_owned()).into_any() } @@ -298,6 +318,10 @@ impl Item for ChannelView { fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option { self.editor.read(cx).pixel_position_of_cursor(cx) } + + fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> { + editor::Editor::to_item_events(event) + } } impl FollowableItem for ChannelView { @@ -313,7 +337,7 @@ impl FollowableItem for ChannelView { Some(proto::view::Variant::ChannelView( proto::view::ChannelView { - channel_id: channel_buffer.channel().id, + channel_id: channel_buffer.channel_id, editor: if let Some(proto::view::Variant::Editor(proto)) = self.editor.read(cx).to_state_proto(cx) { diff --git a/crates/collab_ui/src/chat_panel.rs b/crates/collab_ui/src/chat_panel.rs index 5b922037c5..5a4dafb6d4 100644 --- a/crates/collab_ui/src/chat_panel.rs +++ b/crates/collab_ui/src/chat_panel.rs @@ -263,21 +263,22 @@ impl ChatPanel { fn set_active_chat(&mut self, chat: ModelHandle, cx: &mut ViewContext) { if self.active_chat.as_ref().map(|e| &e.0) != Some(&chat) { - self.markdown_data.clear(); - let id = { + let channel_id = chat.read(cx).channel_id; + { + self.markdown_data.clear(); let chat = chat.read(cx); - let channel = chat.channel().clone(); self.message_list.reset(chat.message_count()); + + let channel_name = chat.channel(cx).map(|channel| channel.name.clone()); self.input_editor.update(cx, |editor, cx| { - editor.set_channel(channel.clone(), cx); + editor.set_channel(channel_id, channel_name, cx); }); - channel.id }; let subscription = cx.subscribe(&chat, Self::channel_did_change); self.active_chat = Some((chat, subscription)); self.acknowledge_last_message(cx); self.channel_select.update(cx, |select, cx| { - if let Some(ix) = self.channel_store.read(cx).index_of_channel(id) { + if let Some(ix) = self.channel_store.read(cx).index_of_channel(channel_id) { select.set_selected_index(ix, cx); } }); @@ -361,7 +362,8 @@ impl ChatPanel { let is_admin = self .channel_store .read(cx) - .is_user_admin(active_chat.channel().id); + .is_channel_admin(active_chat.channel_id); + let last_message = active_chat.message(ix.saturating_sub(1)); let this_message = active_chat.message(ix).clone(); let is_continuation = last_message.id != this_message.id @@ -676,7 +678,7 @@ impl ChatPanel { .active_chat .as_ref() .and_then(|(chat, _)| { - (chat.read(cx).channel().id == selected_channel_id) + (chat.read(cx).channel_id == selected_channel_id) .then(|| Task::ready(anyhow::Ok(chat.clone()))) }) .unwrap_or_else(|| { @@ -714,7 +716,7 @@ impl ChatPanel { fn open_notes(&mut self, _: &OpenChannelNotes, cx: &mut ViewContext) { if let Some((chat, _)) = &self.active_chat { - let channel_id = chat.read(cx).channel().id; + let channel_id = chat.read(cx).channel_id; if let Some(workspace) = self.workspace.upgrade(cx) { ChannelView::open(channel_id, workspace, cx).detach(); } @@ -723,7 +725,7 @@ impl ChatPanel { fn join_call(&mut self, _: &JoinCall, cx: &mut ViewContext) { if let Some((chat, _)) = &self.active_chat { - let channel_id = chat.read(cx).channel().id; + let channel_id = chat.read(cx).channel_id; ActiveCall::global(cx) .update(cx, |call, cx| call.join_channel(channel_id, cx)) .detach_and_log_err(cx); diff --git a/crates/collab_ui/src/chat_panel/message_editor.rs b/crates/collab_ui/src/chat_panel/message_editor.rs index b72af36e63..6dbe3aa204 100644 --- a/crates/collab_ui/src/chat_panel/message_editor.rs +++ b/crates/collab_ui/src/chat_panel/message_editor.rs @@ -1,4 +1,4 @@ -use channel::{Channel, ChannelMembership, ChannelStore, MessageParams}; +use channel::{ChannelId, ChannelMembership, ChannelStore, MessageParams}; use client::UserId; use collections::HashMap; use editor::{AnchorRangeExt, Editor}; @@ -30,7 +30,7 @@ pub struct MessageEditor { users: HashMap, mentions: Vec, mentions_task: Option>, - channel: Option>, + channel_id: Option, } impl MessageEditor { @@ -68,24 +68,33 @@ impl MessageEditor { editor, channel_store, users: HashMap::default(), - channel: None, + channel_id: None, mentions: Vec::new(), mentions_task: None, } } - pub fn set_channel(&mut self, channel: Arc, cx: &mut ViewContext) { + pub fn set_channel( + &mut self, + channel_id: u64, + channel_name: Option, + cx: &mut ViewContext, + ) { self.editor.update(cx, |editor, cx| { - editor.set_placeholder_text(format!("Message #{}", channel.name), cx); + if let Some(channel_name) = channel_name { + editor.set_placeholder_text(format!("Message #{}", channel_name), cx); + } else { + editor.set_placeholder_text(format!("Message Channel"), cx); + } }); - self.channel = Some(channel); + self.channel_id = Some(channel_id); self.refresh_users(cx); } pub fn refresh_users(&mut self, cx: &mut ViewContext) { - if let Some(channel) = &self.channel { + if let Some(channel_id) = self.channel_id { let members = self.channel_store.update(cx, |store, cx| { - store.get_channel_member_details(channel.id, cx) + store.get_channel_member_details(channel_id, cx) }); cx.spawn(|this, mut cx| async move { let members = members.await?; diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index e907127ca4..8d68ee12c0 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -9,7 +9,7 @@ use crate::{ }; use anyhow::Result; use call::ActiveCall; -use channel::{Channel, ChannelData, ChannelEvent, ChannelId, ChannelPath, ChannelStore}; +use channel::{Channel, ChannelEvent, ChannelId, ChannelStore}; use channel_modal::ChannelModal; use client::{ proto::{self, PeerId}, @@ -55,17 +55,17 @@ use workspace::{ #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] struct ToggleCollapse { - location: ChannelPath, + location: ChannelId, } #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] struct NewChannel { - location: ChannelPath, + location: ChannelId, } #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] struct RenameChannel { - location: ChannelPath, + channel_id: ChannelId, } #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] @@ -111,18 +111,6 @@ pub struct CopyChannelLink { #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] struct StartMoveChannelFor { channel_id: ChannelId, - parent_id: Option, -} - -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] -struct StartLinkChannelFor { - channel_id: ChannelId, - parent_id: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] -struct LinkChannel { - to: ChannelId, } #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] @@ -130,14 +118,6 @@ struct MoveChannel { to: ChannelId, } -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] -struct UnlinkChannel { - channel_id: ChannelId, - parent_id: ChannelId, -} - -type DraggedChannel = (Channel, Option); - actions!( collab_panel, [ @@ -147,8 +127,7 @@ actions!( CollapseSelectedChannel, ExpandSelectedChannel, StartMoveChannel, - StartLinkChannel, - MoveOrLinkToSelected, + MoveSelected, InsertSpace, ] ); @@ -166,11 +145,8 @@ impl_actions!( JoinChannelCall, JoinChannelChat, CopyChannelLink, - LinkChannel, StartMoveChannelFor, - StartLinkChannelFor, MoveChannel, - UnlinkChannel, ToggleSelectedIx ] ); @@ -178,14 +154,6 @@ impl_actions!( #[derive(Debug, Copy, Clone, PartialEq, Eq)] struct ChannelMoveClipboard { channel_id: ChannelId, - parent_id: Option, - intent: ClipboardIntent, -} - -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -enum ClipboardIntent { - Move, - Link, } const COLLABORATION_PANEL_KEY: &'static str = "CollaborationPanel"; @@ -232,87 +200,35 @@ pub fn init(cx: &mut AppContext) { _: &mut ViewContext| { panel.channel_clipboard = Some(ChannelMoveClipboard { channel_id: action.channel_id, - parent_id: action.parent_id, - intent: ClipboardIntent::Move, }); }, ); - cx.add_action( - |panel: &mut CollabPanel, - action: &StartLinkChannelFor, - _: &mut ViewContext| { - panel.channel_clipboard = Some(ChannelMoveClipboard { - channel_id: action.channel_id, - parent_id: action.parent_id, - intent: ClipboardIntent::Link, - }) - }, - ); - cx.add_action( |panel: &mut CollabPanel, _: &StartMoveChannel, _: &mut ViewContext| { - if let Some((_, path)) = panel.selected_channel() { + if let Some(channel) = panel.selected_channel() { panel.channel_clipboard = Some(ChannelMoveClipboard { - channel_id: path.channel_id(), - parent_id: path.parent_id(), - intent: ClipboardIntent::Move, + channel_id: channel.id, }) } }, ); cx.add_action( - |panel: &mut CollabPanel, _: &StartLinkChannel, _: &mut ViewContext| { - if let Some((_, path)) = panel.selected_channel() { - panel.channel_clipboard = Some(ChannelMoveClipboard { - channel_id: path.channel_id(), - parent_id: path.parent_id(), - intent: ClipboardIntent::Link, - }) - } - }, - ); + |panel: &mut CollabPanel, _: &MoveSelected, cx: &mut ViewContext| { + let Some(clipboard) = panel.channel_clipboard.take() else { + return; + }; + let Some(selected_channel) = panel.selected_channel() else { + return; + }; - cx.add_action( - |panel: &mut CollabPanel, _: &MoveOrLinkToSelected, cx: &mut ViewContext| { - let clipboard = panel.channel_clipboard.take(); - if let Some(((selected_channel, _), clipboard)) = - panel.selected_channel().zip(clipboard) - { - match clipboard.intent { - ClipboardIntent::Move if clipboard.parent_id.is_some() => { - let parent_id = clipboard.parent_id.unwrap(); - panel.channel_store.update(cx, |channel_store, cx| { - channel_store - .move_channel( - clipboard.channel_id, - parent_id, - selected_channel.id, - cx, - ) - .detach_and_log_err(cx) - }) - } - _ => panel.channel_store.update(cx, |channel_store, cx| { - channel_store - .link_channel(clipboard.channel_id, selected_channel.id, cx) - .detach_and_log_err(cx) - }), - } - } - }, - ); - - cx.add_action( - |panel: &mut CollabPanel, action: &LinkChannel, cx: &mut ViewContext| { - if let Some(clipboard) = panel.channel_clipboard.take() { - panel.channel_store.update(cx, |channel_store, cx| { - channel_store - .link_channel(clipboard.channel_id, action.to, cx) - .detach_and_log_err(cx) + panel + .channel_store + .update(cx, |channel_store, cx| { + channel_store.move_channel(clipboard.channel_id, Some(selected_channel.id), cx) }) - } + .detach_and_log_err(cx) }, ); @@ -320,39 +236,23 @@ pub fn init(cx: &mut AppContext) { |panel: &mut CollabPanel, action: &MoveChannel, cx: &mut ViewContext| { if let Some(clipboard) = panel.channel_clipboard.take() { panel.channel_store.update(cx, |channel_store, cx| { - if let Some(parent) = clipboard.parent_id { - channel_store - .move_channel(clipboard.channel_id, parent, action.to, cx) - .detach_and_log_err(cx) - } else { - channel_store - .link_channel(clipboard.channel_id, action.to, cx) - .detach_and_log_err(cx) - } + channel_store + .move_channel(clipboard.channel_id, Some(action.to), cx) + .detach_and_log_err(cx) }) } }, ); - - cx.add_action( - |panel: &mut CollabPanel, action: &UnlinkChannel, cx: &mut ViewContext| { - panel.channel_store.update(cx, |channel_store, cx| { - channel_store - .unlink_channel(action.channel_id, action.parent_id, cx) - .detach_and_log_err(cx) - }) - }, - ); } #[derive(Debug)] pub enum ChannelEditingState { Create { - location: Option, + location: Option, pending_name: Option, }, Rename { - location: ChannelPath, + location: ChannelId, pending_name: Option, }, } @@ -386,16 +286,23 @@ pub struct CollabPanel { list_state: ListState, subscriptions: Vec, collapsed_sections: Vec
, - collapsed_channels: Vec, - drag_target_channel: Option, + collapsed_channels: Vec, + drag_target_channel: ChannelDragTarget, workspace: WeakViewHandle, context_menu_on_selected: bool, } +#[derive(PartialEq, Eq)] +enum ChannelDragTarget { + None, + Root, + Channel(ChannelId), +} + #[derive(Serialize, Deserialize)] struct SerializedCollabPanel { width: Option, - collapsed_channels: Option>, + collapsed_channels: Option>, } #[derive(Debug)] @@ -440,7 +347,7 @@ enum ListEntry { Channel { channel: Arc, depth: usize, - path: ChannelPath, + has_children: bool, }, ChannelNotes { channel_id: ChannelId, @@ -575,14 +482,14 @@ impl CollabPanel { ListEntry::Channel { channel, depth, - path, + has_children, } => { let channel_row = this.render_channel( &*channel, *depth, - path.to_owned(), &theme, is_selected, + *has_children, ix, cx, ); @@ -677,7 +584,7 @@ impl CollabPanel { workspace: workspace.weak_handle(), client: workspace.app_state().client.clone(), context_menu_on_selected: true, - drag_target_channel: None, + drag_target_channel: ChannelDragTarget::None, list_state, }; @@ -941,7 +848,7 @@ impl CollabPanel { if channel_store.channel_count() > 0 || self.channel_editing_state.is_some() { self.match_candidates.clear(); self.match_candidates - .extend(channel_store.channel_dag_entries().enumerate().map( + .extend(channel_store.ordered_channels().enumerate().map( |(ix, (_, channel))| StringMatchCandidate { id: ix, string: channel.name.clone(), @@ -963,48 +870,52 @@ impl CollabPanel { } let mut collapse_depth = None; for mat in matches { - let (channel, path) = channel_store - .channel_dag_entry_at(mat.candidate_id) - .unwrap(); - let depth = path.len() - 1; + let channel = channel_store.channel_at_index(mat.candidate_id).unwrap(); + let depth = channel.parent_path.len(); - if collapse_depth.is_none() && self.is_channel_collapsed(path) { + if collapse_depth.is_none() && self.is_channel_collapsed(channel.id) { collapse_depth = Some(depth); } else if let Some(collapsed_depth) = collapse_depth { if depth > collapsed_depth { continue; } - if self.is_channel_collapsed(path) { + if self.is_channel_collapsed(channel.id) { collapse_depth = Some(depth); } else { collapse_depth = None; } } + let has_children = channel_store + .channel_at_index(mat.candidate_id + 1) + .map_or(false, |next_channel| { + next_channel.parent_path.ends_with(&[channel.id]) + }); + match &self.channel_editing_state { Some(ChannelEditingState::Create { - location: parent_path, + location: parent_id, .. - }) if parent_path.as_ref() == Some(path) => { + }) if *parent_id == Some(channel.id) => { self.entries.push(ListEntry::Channel { channel: channel.clone(), depth, - path: path.clone(), + has_children: false, }); self.entries .push(ListEntry::ChannelEditor { depth: depth + 1 }); } Some(ChannelEditingState::Rename { - location: parent_path, + location: parent_id, .. - }) if parent_path == path => { + }) if parent_id == &channel.id => { self.entries.push(ListEntry::ChannelEditor { depth }); } _ => { self.entries.push(ListEntry::Channel { channel: channel.clone(), depth, - path: path.clone(), + has_children, }); } } @@ -1546,6 +1457,7 @@ impl CollabPanel { let mut channel_link = None; let mut channel_tooltip_text = None; let mut channel_icon = None; + let mut is_dragged_over = false; let text = match section { Section::ActiveCall => { @@ -1629,26 +1541,37 @@ impl CollabPanel { cx, ), ), - Section::Channels => Some( - MouseEventHandler::new::(0, cx, |state, _| { - render_icon_button( - theme - .collab_panel - .add_contact_button - .style_for(is_selected, state), - "icons/plus.svg", - ) - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, |_, this, cx| this.new_root_channel(cx)) - .with_tooltip::( - 0, - "Create a channel", - None, - tooltip_style.clone(), - cx, - ), - ), + Section::Channels => { + if cx + .global::>() + .currently_dragged::(cx.window()) + .is_some() + && self.drag_target_channel == ChannelDragTarget::Root + { + is_dragged_over = true; + } + + Some( + MouseEventHandler::new::(0, cx, |state, _| { + render_icon_button( + theme + .collab_panel + .add_contact_button + .style_for(is_selected, state), + "icons/plus.svg", + ) + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, |_, this, cx| this.new_root_channel(cx)) + .with_tooltip::( + 0, + "Create a channel", + None, + tooltip_style.clone(), + cx, + ), + ) + } _ => None, }; @@ -1719,9 +1642,37 @@ impl CollabPanel { .constrained() .with_height(theme.collab_panel.row_height) .contained() - .with_style(header_style.container) + .with_style(if is_dragged_over { + theme.collab_panel.dragged_over_header + } else { + header_style.container + }) }); + result = result + .on_move(move |_, this, cx| { + if cx + .global::>() + .currently_dragged::(cx.window()) + .is_some() + { + this.drag_target_channel = ChannelDragTarget::Root; + cx.notify() + } + }) + .on_up(MouseButton::Left, move |_, this, cx| { + if let Some((_, dragged_channel)) = cx + .global::>() + .currently_dragged::(cx.window()) + { + this.channel_store + .update(cx, |channel_store, cx| { + channel_store.move_channel(dragged_channel.id, None, cx) + }) + .detach_and_log_err(cx) + } + }); + if can_collapse { result = result .with_cursor_style(CursorStyle::PointingHand) @@ -1972,24 +1923,23 @@ impl CollabPanel { &self, channel: &Channel, depth: usize, - path: ChannelPath, theme: &theme::Theme, is_selected: bool, + has_children: bool, ix: usize, cx: &mut ViewContext, ) -> AnyElement { let channel_id = channel.id; let collab_theme = &theme.collab_panel; - let has_children = self.channel_store.read(cx).has_children(channel_id); let is_public = self .channel_store .read(cx) .channel_for_id(channel_id) .map(|channel| channel.visibility) == Some(proto::ChannelVisibility::Public); - let other_selected = - self.selected_channel().map(|channel| channel.0.id) == Some(channel.id); - let disclosed = has_children.then(|| !self.collapsed_channels.binary_search(&path).is_ok()); + let other_selected = self.selected_channel().map(|channel| channel.id) == Some(channel.id); + let disclosed = + has_children.then(|| !self.collapsed_channels.binary_search(&channel.id).is_ok()); let is_active = iife!({ let call_channel = ActiveCall::global(cx) @@ -2012,13 +1962,9 @@ impl CollabPanel { let mut is_dragged_over = false; if cx .global::>() - .currently_dragged::(cx.window()) + .currently_dragged::(cx.window()) .is_some() - && self - .drag_target_channel - .as_ref() - .filter(|(_, dragged_path)| path.starts_with(dragged_path)) - .is_some() + && self.drag_target_channel == ChannelDragTarget::Channel(channel_id) { is_dragged_over = true; } @@ -2201,7 +2147,7 @@ impl CollabPanel { .disclosable( disclosed, Box::new(ToggleCollapse { - location: path.clone(), + location: channel.id.clone(), }), ) .with_id(ix) @@ -2221,7 +2167,7 @@ impl CollabPanel { ) }) .on_click(MouseButton::Left, move |_, this, cx| { - if this.drag_target_channel.take().is_none() { + if this.drag_target_channel == ChannelDragTarget::None { if is_active { this.open_channel_notes(&OpenChannelNotes { channel_id }, cx) } else { @@ -2230,76 +2176,43 @@ impl CollabPanel { } }) .on_click(MouseButton::Right, { - let path = path.clone(); + let channel = channel.clone(); move |e, this, cx| { - this.deploy_channel_context_menu(Some(e.position), &path, ix, cx); + this.deploy_channel_context_menu(Some(e.position), &channel, ix, cx); } }) - .on_up(MouseButton::Left, move |e, this, cx| { + .on_up(MouseButton::Left, move |_, this, cx| { if let Some((_, dragged_channel)) = cx .global::>() - .currently_dragged::(cx.window()) + .currently_dragged::(cx.window()) { - if e.modifiers.alt { - this.channel_store.update(cx, |channel_store, cx| { - channel_store - .link_channel(dragged_channel.0.id, channel_id, cx) - .detach_and_log_err(cx) + this.channel_store + .update(cx, |channel_store, cx| { + channel_store.move_channel(dragged_channel.id, Some(channel_id), cx) }) - } else { - this.channel_store.update(cx, |channel_store, cx| { - match dragged_channel.1 { - Some(parent_id) => channel_store.move_channel( - dragged_channel.0.id, - parent_id, - channel_id, - cx, - ), - None => { - channel_store.link_channel(dragged_channel.0.id, channel_id, cx) - } - } - .detach_and_log_err(cx) - }) - } + .detach_and_log_err(cx) } }) .on_move({ let channel = channel.clone(); - let path = path.clone(); move |_, this, cx| { - if let Some((_, _dragged_channel)) = - cx.global::>() - .currently_dragged::(cx.window()) + if let Some((_, dragged_channel)) = cx + .global::>() + .currently_dragged::(cx.window()) { - match &this.drag_target_channel { - Some(current_target) - if current_target.0 == channel && current_target.1 == path => - { - return - } - _ => { - this.drag_target_channel = Some((channel.clone(), path.clone())); - cx.notify(); - } + if channel.id != dragged_channel.id { + this.drag_target_channel = ChannelDragTarget::Channel(channel.id); } + cx.notify() } } }) - .as_draggable( - (channel.clone(), path.parent_id()), - move |modifiers, (channel, _), cx: &mut ViewContext| { + .as_draggable::<_, Channel>( + channel.clone(), + move |_, channel, cx: &mut ViewContext| { let theme = &theme::current(cx).collab_panel; Flex::::row() - .with_children(modifiers.alt.then(|| { - Svg::new("icons/plus.svg") - .with_color(theme.channel_hash.color) - .constrained() - .with_width(theme.channel_hash.width) - .aligned() - .left() - })) .with_child( Svg::new("icons/hash.svg") .with_color(theme.channel_hash.color) @@ -2631,39 +2544,29 @@ impl CollabPanel { } fn has_subchannels(&self, ix: usize) -> bool { - self.entries - .get(ix) - .zip(self.entries.get(ix + 1)) - .map(|entries| match entries { - ( - ListEntry::Channel { - path: this_path, .. - }, - ListEntry::Channel { - path: next_path, .. - }, - ) => next_path.starts_with(this_path), - _ => false, - }) - .unwrap_or(false) + self.entries.get(ix).map_or(false, |entry| { + if let ListEntry::Channel { has_children, .. } = entry { + *has_children + } else { + false + } + }) } fn deploy_channel_context_menu( &mut self, position: Option, - path: &ChannelPath, + channel: &Channel, ix: usize, cx: &mut ViewContext, ) { self.context_menu_on_selected = position.is_none(); - let channel_name = self.channel_clipboard.as_ref().and_then(|channel| { - let channel_name = self - .channel_store + let clipboard_channel_name = self.channel_clipboard.as_ref().and_then(|clipboard| { + self.channel_store .read(cx) - .channel_for_id(channel.channel_id) - .map(|channel| channel.name.clone())?; - Some(channel_name) + .channel_for_id(clipboard.channel_id) + .map(|channel| channel.name.clone()) }); self.context_menu.update(cx, |context_menu, cx| { @@ -2687,7 +2590,7 @@ impl CollabPanel { )); if self.has_subchannels(ix) { - let expand_action_name = if self.is_channel_collapsed(&path) { + let expand_action_name = if self.is_channel_collapsed(channel.id) { "Expand Subchannels" } else { "Collapse Subchannels" @@ -2695,7 +2598,7 @@ impl CollabPanel { items.push(ContextMenuItem::action( expand_action_name, ToggleCollapse { - location: path.clone(), + location: channel.id, }, )); } @@ -2703,84 +2606,52 @@ impl CollabPanel { items.push(ContextMenuItem::action( "Open Notes", OpenChannelNotes { - channel_id: path.channel_id(), + channel_id: channel.id, }, )); items.push(ContextMenuItem::action( "Open Chat", JoinChannelChat { - channel_id: path.channel_id(), + channel_id: channel.id, }, )); items.push(ContextMenuItem::action( "Copy Channel Link", CopyChannelLink { - channel_id: path.channel_id(), + channel_id: channel.id, }, )); - if self.channel_store.read(cx).is_user_admin(path.channel_id()) { - let parent_id = path.parent_id(); - + if self.channel_store.read(cx).is_channel_admin(channel.id) { items.extend([ ContextMenuItem::Separator, ContextMenuItem::action( "New Subchannel", NewChannel { - location: path.clone(), + location: channel.id, }, ), ContextMenuItem::action( "Rename", RenameChannel { - location: path.clone(), + channel_id: channel.id, }, ), - ContextMenuItem::Separator, - ]); - - if let Some(parent_id) = parent_id { - items.push(ContextMenuItem::action( - "Unlink from parent", - UnlinkChannel { - channel_id: path.channel_id(), - parent_id, - }, - )); - } - - items.extend([ ContextMenuItem::action( "Move this channel", StartMoveChannelFor { - channel_id: path.channel_id(), - parent_id, - }, - ), - ContextMenuItem::action( - "Link this channel", - StartLinkChannelFor { - channel_id: path.channel_id(), - parent_id, + channel_id: channel.id, }, ), ]); - if let Some(channel_name) = channel_name { + if let Some(channel_name) = clipboard_channel_name { items.push(ContextMenuItem::Separator); items.push(ContextMenuItem::action( format!("Move '#{}' here", channel_name), - MoveChannel { - to: path.channel_id(), - }, - )); - items.push(ContextMenuItem::action( - format!("Link '#{}' here", channel_name), - LinkChannel { - to: path.channel_id(), - }, + MoveChannel { to: channel.id }, )); } @@ -2789,20 +2660,20 @@ impl CollabPanel { ContextMenuItem::action( "Invite Members", InviteMembers { - channel_id: path.channel_id(), + channel_id: channel.id, }, ), ContextMenuItem::action( "Manage Members", ManageMembers { - channel_id: path.channel_id(), + channel_id: channel.id, }, ), ContextMenuItem::Separator, ContextMenuItem::action( "Delete", RemoveChannel { - channel_id: path.channel_id(), + channel_id: channel.id, }, ), ]); @@ -2973,11 +2844,7 @@ impl CollabPanel { self.channel_store .update(cx, |channel_store, cx| { - channel_store.create_channel( - &channel_name, - location.as_ref().map(|location| location.channel_id()), - cx, - ) + channel_store.create_channel(&channel_name, *location, cx) }) .detach(); cx.notify(); @@ -2994,7 +2861,7 @@ impl CollabPanel { self.channel_store .update(cx, |channel_store, cx| { - channel_store.rename(location.channel_id(), &channel_name, cx) + channel_store.rename(*location, &channel_name, cx) }) .detach(); cx.notify(); @@ -3021,33 +2888,27 @@ impl CollabPanel { _: &CollapseSelectedChannel, cx: &mut ViewContext, ) { - let Some((_, path)) = self - .selected_channel() - .map(|(channel, parent)| (channel.id, parent)) - else { + let Some(channel_id) = self.selected_channel().map(|channel| channel.id) else { return; }; - if self.is_channel_collapsed(&path) { + if self.is_channel_collapsed(channel_id) { return; } - self.toggle_channel_collapsed(&path.clone(), cx); + self.toggle_channel_collapsed(channel_id, cx); } fn expand_selected_channel(&mut self, _: &ExpandSelectedChannel, cx: &mut ViewContext) { - let Some((_, path)) = self - .selected_channel() - .map(|(channel, parent)| (channel.id, parent)) - else { + let Some(id) = self.selected_channel().map(|channel| channel.id) else { return; }; - if !self.is_channel_collapsed(&path) { + if !self.is_channel_collapsed(id) { return; } - self.toggle_channel_collapsed(path.to_owned(), cx) + self.toggle_channel_collapsed(id, cx) } fn toggle_channel_collapsed_action( @@ -3055,21 +2916,16 @@ impl CollabPanel { action: &ToggleCollapse, cx: &mut ViewContext, ) { - self.toggle_channel_collapsed(&action.location, cx); + self.toggle_channel_collapsed(action.location, cx); } - fn toggle_channel_collapsed<'a>( - &mut self, - path: impl Into>, - cx: &mut ViewContext, - ) { - let path = path.into(); - match self.collapsed_channels.binary_search(&path) { + fn toggle_channel_collapsed<'a>(&mut self, channel_id: ChannelId, cx: &mut ViewContext) { + match self.collapsed_channels.binary_search(&channel_id) { Ok(ix) => { self.collapsed_channels.remove(ix); } Err(ix) => { - self.collapsed_channels.insert(ix, path.into_owned()); + self.collapsed_channels.insert(ix, channel_id); } }; self.serialize(cx); @@ -3078,8 +2934,8 @@ impl CollabPanel { cx.focus_self(); } - fn is_channel_collapsed(&self, path: &ChannelPath) -> bool { - self.collapsed_channels.binary_search(path).is_ok() + fn is_channel_collapsed(&self, channel_id: ChannelId) -> bool { + self.collapsed_channels.binary_search(&channel_id).is_ok() } fn leave_call(cx: &mut ViewContext) { @@ -3142,16 +2998,16 @@ impl CollabPanel { } fn remove(&mut self, _: &Remove, cx: &mut ViewContext) { - if let Some((channel, _)) = self.selected_channel() { + if let Some(channel) = self.selected_channel() { self.remove_channel(channel.id, cx) } } fn rename_selected_channel(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext) { - if let Some((_, parent)) = self.selected_channel() { + if let Some(channel) = self.selected_channel() { self.rename_channel( &RenameChannel { - location: parent.to_owned(), + channel_id: channel.id, }, cx, ); @@ -3160,15 +3016,12 @@ impl CollabPanel { fn rename_channel(&mut self, action: &RenameChannel, cx: &mut ViewContext) { let channel_store = self.channel_store.read(cx); - if !channel_store.is_user_admin(action.location.channel_id()) { + if !channel_store.is_channel_admin(action.channel_id) { return; } - if let Some(channel) = channel_store - .channel_for_id(action.location.channel_id()) - .cloned() - { + if let Some(channel) = channel_store.channel_for_id(action.channel_id).cloned() { self.channel_editing_state = Some(ChannelEditingState::Rename { - location: action.location.to_owned(), + location: action.channel_id.to_owned(), pending_name: None, }); self.channel_name_editor.update(cx, |editor, cx| { @@ -3188,22 +3041,18 @@ impl CollabPanel { } fn show_inline_context_menu(&mut self, _: &menu::ShowContextMenu, cx: &mut ViewContext) { - let Some((_, path)) = self.selected_channel() else { + let Some(channel) = self.selected_channel() else { return; }; - self.deploy_channel_context_menu(None, &path.to_owned(), self.selection.unwrap(), cx); + self.deploy_channel_context_menu(None, &channel.clone(), self.selection.unwrap(), cx); } - fn selected_channel(&self) -> Option<(&Arc, &ChannelPath)> { + fn selected_channel(&self) -> Option<&Arc> { self.selection .and_then(|ix| self.entries.get(ix)) .and_then(|entry| match entry { - ListEntry::Channel { - channel, - path: parent, - .. - } => Some((channel, parent)), + ListEntry::Channel { channel, .. } => Some(channel), _ => None, }) } @@ -3620,19 +3469,13 @@ impl PartialEq for ListEntry { } } ListEntry::Channel { - channel: channel_1, - depth: depth_1, - path: parent_1, + channel: channel_1, .. } => { if let ListEntry::Channel { - channel: channel_2, - depth: depth_2, - path: parent_2, + channel: channel_2, .. } = other { - return channel_1.id == channel_2.id - && depth_1 == depth_2 - && parent_1 == parent_2; + return channel_1.id == channel_2.id; } } ListEntry::ChannelNotes { channel_id } => { diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index 14d9466e3e..cef8faf601 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -88,8 +88,10 @@ impl View for CollabTitlebarItem { .zip(peer_id) .zip(ActiveCall::global(cx).read(cx).room().cloned()) { - right_container - .add_children(self.render_in_call_share_unshare_button(&workspace, &theme, cx)); + if room.read(cx).can_publish() { + right_container + .add_children(self.render_in_call_share_unshare_button(&workspace, &theme, cx)); + } right_container.add_child(self.render_leave_call(&theme, cx)); let muted = room.read(cx).is_muted(cx); let speaking = room.read(cx).is_speaking(); @@ -97,9 +99,14 @@ impl View for CollabTitlebarItem { self.render_current_user(&workspace, &theme, &user, peer_id, muted, speaking, cx), ); left_container.add_children(self.render_collaborators(&workspace, &theme, &room, cx)); - right_container.add_child(self.render_toggle_mute(&theme, &room, cx)); + if room.read(cx).can_publish() { + right_container.add_child(self.render_toggle_mute(&theme, &room, cx)); + } right_container.add_child(self.render_toggle_deafen(&theme, &room, cx)); - right_container.add_child(self.render_toggle_screen_sharing_button(&theme, &room, cx)); + if room.read(cx).can_publish() { + right_container + .add_child(self.render_toggle_screen_sharing_button(&theme, &room, cx)); + } } let status = workspace.read(cx).client().status(); diff --git a/crates/collab_ui/src/notification_panel.rs b/crates/collab_ui/src/notification_panel.rs index e245a919f3..1720c8dcdc 100644 --- a/crates/collab_ui/src/notification_panel.rs +++ b/crates/collab_ui/src/notification_panel.rs @@ -477,7 +477,7 @@ impl NotificationPanel { return panel.read_with(cx, |panel, cx| { panel.is_scrolled_to_bottom() && panel.active_chat().map_or(false, |chat| { - chat.read(cx).channel().id == *channel_id + chat.read(cx).channel_id == *channel_id }) }); } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 82945fc00b..101d2297a9 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -966,8 +966,11 @@ impl CompletionsMenu { ) { if self.selected_item > 0 { self.selected_item -= 1; + } else { + self.selected_item = self.matches.len() - 1; self.list.scroll_to(ScrollTarget::Show(self.selected_item)); } + self.list.scroll_to(ScrollTarget::Show(self.selected_item)); self.attempt_resolve_selected_completion_documentation(project, cx); cx.notify(); } @@ -979,8 +982,10 @@ impl CompletionsMenu { ) { if self.selected_item + 1 < self.matches.len() { self.selected_item += 1; - self.list.scroll_to(ScrollTarget::Show(self.selected_item)); + } else { + self.selected_item = 0; } + self.list.scroll_to(ScrollTarget::Show(self.selected_item)); self.attempt_resolve_selected_completion_documentation(project, cx); cx.notify(); } @@ -1532,17 +1537,23 @@ impl CodeActionsMenu { fn select_prev(&mut self, cx: &mut ViewContext) { if self.selected_item > 0 { self.selected_item -= 1; + } else { + self.selected_item = self.actions.len() - 1; self.list.scroll_to(ScrollTarget::Show(self.selected_item)); - cx.notify() } + self.list.scroll_to(ScrollTarget::Show(self.selected_item)); + cx.notify(); } fn select_next(&mut self, cx: &mut ViewContext) { if self.selected_item + 1 < self.actions.len() { self.selected_item += 1; self.list.scroll_to(ScrollTarget::Show(self.selected_item)); - cx.notify() + } else { + self.selected_item = 0; + self.list.scroll_to(ScrollTarget::Show(self.selected_item)); } + cx.notify(); } fn select_last(&mut self, cx: &mut ViewContext) { @@ -6542,7 +6553,7 @@ impl Editor { { if selections .iter() - .find(|selection| selection.equals(&offset_range)) + .find(|selection| selection.range().overlaps(&offset_range)) .is_none() { next_selected_range = Some(offset_range); diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 6421fc6f7a..d5738ec9f6 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -5117,7 +5117,6 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) { let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; project.update(cx, |project, _| { - project.enable_test_prettier(&[]); project.languages().add(Arc::new(language)); }); let buffer = project @@ -7864,10 +7863,9 @@ async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) { fs.insert_file("/file.rs", Default::default()).await; let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; - let prettier_format_suffix = project.update(cx, |project, _| { - let suffix = project.enable_test_prettier(&[test_plugin]); + let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX; + project.update(cx, |project, _| { project.languages().add(Arc::new(language)); - suffix }); let buffer = project .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx)) diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index 580faf1050..332eb3c1c5 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -369,6 +369,30 @@ pub fn find_boundary( map.clip_point(offset.to_display_point(map), Bias::Right) } +pub fn chars_after( + map: &DisplaySnapshot, + mut offset: usize, +) -> impl Iterator)> + '_ { + map.buffer_snapshot.chars_at(offset).map(move |ch| { + let before = offset; + offset = offset + ch.len_utf8(); + (ch, before..offset) + }) +} + +pub fn chars_before( + map: &DisplaySnapshot, + mut offset: usize, +) -> impl Iterator)> + '_ { + map.buffer_snapshot + .reversed_chars_at(offset) + .map(move |ch| { + let after = offset; + offset = offset - ch.len_utf8(); + (ch, offset..after) + }) +} + pub fn is_inside_word(map: &DisplaySnapshot, point: DisplayPoint) -> bool { let raw_point = point.to_point(map); let scope = map.buffer_snapshot.language_scope_at(raw_point); @@ -707,7 +731,9 @@ mod tests { let (snapshot, display_points) = marked_display_snapshot(marked_text, cx); assert_eq!( surrounding_word(&snapshot, display_points[1]), - display_points[0]..display_points[2] + display_points[0]..display_points[2], + "{}", + marked_text.to_string() ); } @@ -717,7 +743,7 @@ mod tests { assert("loremˇ ˇ ˇipsum", cx); assert("lorem\nˇˇˇ\nipsum", cx); assert("lorem\nˇˇipsumˇ", cx); - assert("lorem,ˇˇ ˇipsum", cx); + assert("loremˇ,ˇˇ ipsum", cx); assert("ˇloremˇˇ, ipsum", cx); } diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 063c7616a8..0194123bd2 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -373,8 +373,8 @@ pub(crate) struct DiagnosticEndpoint { #[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Debug)] pub enum CharKind { - Punctuation, Whitespace, + Punctuation, Word, } diff --git a/crates/live_kit_client/src/test.rs b/crates/live_kit_client/src/test.rs index 8df8ab4abb..b39ad333d2 100644 --- a/crates/live_kit_client/src/test.rs +++ b/crates/live_kit_client/src/test.rs @@ -306,6 +306,16 @@ impl live_kit_server::api::Client for TestApiClient { token::VideoGrant::to_join(room), ) } + + fn guest_token(&self, room: &str, identity: &str) -> Result { + let server = TestServer::get(&self.url)?; + token::create( + &server.api_key, + &server.secret_key, + Some(identity), + token::VideoGrant::for_guest(room), + ) + } } pub type Sid = String; diff --git a/crates/live_kit_server/src/api.rs b/crates/live_kit_server/src/api.rs index 417a17bdc9..2c1e174fb4 100644 --- a/crates/live_kit_server/src/api.rs +++ b/crates/live_kit_server/src/api.rs @@ -12,6 +12,7 @@ pub trait Client: Send + Sync { async fn delete_room(&self, name: String) -> Result<()>; async fn remove_participant(&self, room: String, identity: String) -> Result<()>; fn room_token(&self, room: &str, identity: &str) -> Result; + fn guest_token(&self, room: &str, identity: &str) -> Result; } #[derive(Clone)] @@ -138,4 +139,13 @@ impl Client for LiveKitClient { token::VideoGrant::to_join(room), ) } + + fn guest_token(&self, room: &str, identity: &str) -> Result { + token::create( + &self.key, + &self.secret, + Some(identity), + token::VideoGrant::for_guest(room), + ) + } } diff --git a/crates/live_kit_server/src/token.rs b/crates/live_kit_server/src/token.rs index 072a8be0c9..b98f5892ae 100644 --- a/crates/live_kit_server/src/token.rs +++ b/crates/live_kit_server/src/token.rs @@ -57,6 +57,15 @@ impl<'a> VideoGrant<'a> { ..Default::default() } } + + pub fn for_guest(room: &'a str) -> Self { + Self { + room: Some(Cow::Borrowed(room)), + room_join: Some(true), + can_subscribe: Some(true), + ..Default::default() + } + } } pub fn create( diff --git a/crates/node_runtime/src/node_runtime.rs b/crates/node_runtime/src/node_runtime.rs index dcb8833f8c..a099a025e6 100644 --- a/crates/node_runtime/src/node_runtime.rs +++ b/crates/node_runtime/src/node_runtime.rs @@ -220,96 +220,31 @@ impl NodeRuntime for RealNodeRuntime { } } -pub struct FakeNodeRuntime(Option); - -struct PrettierSupport { - plugins: Vec<&'static str>, -} +pub struct FakeNodeRuntime; impl FakeNodeRuntime { pub fn new() -> Arc { - Arc::new(FakeNodeRuntime(None)) - } - - pub fn with_prettier_support(plugins: &[&'static str]) -> Arc { - Arc::new(FakeNodeRuntime(Some(PrettierSupport::new(plugins)))) + Arc::new(Self) } } #[async_trait::async_trait] impl NodeRuntime for FakeNodeRuntime { async fn binary_path(&self) -> anyhow::Result { - if let Some(prettier_support) = &self.0 { - prettier_support.binary_path().await - } else { - unreachable!() - } + unreachable!() } async fn run_npm_subcommand( &self, - directory: Option<&Path>, + _: Option<&Path>, subcommand: &str, args: &[&str], ) -> anyhow::Result { - if let Some(prettier_support) = &self.0 { - prettier_support - .run_npm_subcommand(directory, subcommand, args) - .await - } else { - unreachable!() - } + unreachable!("Should not run npm subcommand '{subcommand}' with args {args:?}") } async fn npm_package_latest_version(&self, name: &str) -> anyhow::Result { - if let Some(prettier_support) = &self.0 { - prettier_support.npm_package_latest_version(name).await - } else { - unreachable!() - } - } - - async fn npm_install_packages( - &self, - directory: &Path, - packages: &[(&str, &str)], - ) -> anyhow::Result<()> { - if let Some(prettier_support) = &self.0 { - prettier_support - .npm_install_packages(directory, packages) - .await - } else { - unreachable!() - } - } -} - -impl PrettierSupport { - const PACKAGE_VERSION: &str = "0.0.1"; - - fn new(plugins: &[&'static str]) -> Self { - Self { - plugins: plugins.to_vec(), - } - } -} - -#[async_trait::async_trait] -impl NodeRuntime for PrettierSupport { - async fn binary_path(&self) -> anyhow::Result { - Ok(PathBuf::from("prettier_fake_node")) - } - - async fn run_npm_subcommand(&self, _: Option<&Path>, _: &str, _: &[&str]) -> Result { - unreachable!() - } - - async fn npm_package_latest_version(&self, name: &str) -> anyhow::Result { - if name == "prettier" || self.plugins.contains(&name) { - Ok(Self::PACKAGE_VERSION.to_string()) - } else { - panic!("Unexpected package name: {name}") - } + unreachable!("Should not query npm package '{name}' for latest version") } async fn npm_install_packages( @@ -317,32 +252,6 @@ impl NodeRuntime for PrettierSupport { _: &Path, packages: &[(&str, &str)], ) -> anyhow::Result<()> { - assert_eq!( - packages.len(), - self.plugins.len() + 1, - "Unexpected packages length to install: {:?}, expected `prettier` + {:?}", - packages, - self.plugins - ); - for (name, version) in packages { - assert!( - name == &"prettier" || self.plugins.contains(name), - "Unexpected package `{}` to install in packages {:?}, expected {} for `prettier` + {:?}", - name, - packages, - Self::PACKAGE_VERSION, - self.plugins - ); - assert_eq!( - version, - &Self::PACKAGE_VERSION, - "Unexpected package version `{}` to install in packages {:?}, expected {} for `prettier` + {:?}", - version, - packages, - Self::PACKAGE_VERSION, - self.plugins - ); - } - Ok(()) + unreachable!("Should not install packages {packages:?}") } } diff --git a/crates/prettier/src/prettier.rs b/crates/prettier/src/prettier.rs index 09b793e5a2..bddfcb3a8f 100644 --- a/crates/prettier/src/prettier.rs +++ b/crates/prettier/src/prettier.rs @@ -44,6 +44,9 @@ pub const PRETTIER_SERVER_JS: &str = include_str!("./prettier_server.js"); const PRETTIER_PACKAGE_NAME: &str = "prettier"; const TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME: &str = "prettier-plugin-tailwindcss"; +#[cfg(any(test, feature = "test-support"))] +pub const FORMAT_SUFFIX: &str = "\nformatted by test prettier"; + impl Prettier { pub const CONFIG_FILE_NAMES: &'static [&'static str] = &[ ".prettierrc", @@ -60,9 +63,6 @@ impl Prettier { ".editorconfig", ]; - #[cfg(any(test, feature = "test-support"))] - pub const FORMAT_SUFFIX: &str = "\nformatted by test prettier"; - pub async fn locate( starting_path: Option, fs: Arc, @@ -349,7 +349,7 @@ impl Prettier { #[cfg(any(test, feature = "test-support"))] Self::Test(_) => Ok(buffer .read_with(cx, |buffer, cx| { - let formatted_text = buffer.text() + Self::FORMAT_SUFFIX; + let formatted_text = buffer.text() + FORMAT_SUFFIX; buffer.diff(formatted_text, cx) }) .await), diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 2eb1fd421c..fd21c64945 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -53,7 +53,7 @@ use lsp::{ use lsp_command::*; use node_runtime::NodeRuntime; use postage::watch; -use prettier::{LocateStart, Prettier, PRETTIER_SERVER_FILE, PRETTIER_SERVER_JS}; +use prettier::{LocateStart, Prettier}; use project_settings::{LspSettings, ProjectSettings}; use rand::prelude::*; use search::SearchQuery; @@ -79,16 +79,15 @@ use std::{ time::{Duration, Instant}, }; use terminals::Terminals; -use text::{Anchor, LineEnding, Rope}; +use text::Anchor; use util::{ - debug_panic, defer, - http::HttpClient, - merge_json_value_into, - paths::{DEFAULT_PRETTIER_DIR, LOCAL_SETTINGS_RELATIVE_PATH}, - post_inc, ResultExt, TryFutureExt as _, + debug_panic, defer, http::HttpClient, merge_json_value_into, + paths::LOCAL_SETTINGS_RELATIVE_PATH, post_inc, ResultExt, TryFutureExt as _, }; pub use fs::*; +#[cfg(any(test, feature = "test-support"))] +pub use prettier::FORMAT_SUFFIX as TEST_PRETTIER_FORMAT_SUFFIX; pub use worktree::*; pub trait Item { @@ -836,16 +835,6 @@ impl Project { project } - /// Enables a prettier mock that avoids interacting with node runtime, prettier LSP wrapper, or any real file changes. - /// Instead, if appends the suffix to every input, this suffix is returned by this method. - #[cfg(any(test, feature = "test-support"))] - pub fn enable_test_prettier(&mut self, plugins: &[&'static str]) -> &'static str { - self.node = Some(node_runtime::FakeNodeRuntime::with_prettier_support( - plugins, - )); - Prettier::FORMAT_SUFFIX - } - fn on_settings_changed(&mut self, cx: &mut ModelContext) { let mut language_servers_to_start = Vec::new(); let mut language_formatters_to_check = Vec::new(); @@ -8489,6 +8478,18 @@ impl Project { } } + #[cfg(any(test, feature = "test-support"))] + fn install_default_formatters( + &self, + _worktree: Option, + _new_language: &Language, + _language_settings: &LanguageSettings, + _cx: &mut ModelContext, + ) -> Task> { + return Task::ready(Ok(())); + } + + #[cfg(not(any(test, feature = "test-support")))] fn install_default_formatters( &self, worktree: Option, @@ -8519,7 +8520,7 @@ impl Project { return Task::ready(Ok(())); }; - let default_prettier_dir = DEFAULT_PRETTIER_DIR.as_path(); + let default_prettier_dir = util::paths::DEFAULT_PRETTIER_DIR.as_path(); let already_running_prettier = self .prettier_instances .get(&(worktree, default_prettier_dir.to_path_buf())) @@ -8528,10 +8529,10 @@ impl Project { let fs = Arc::clone(&self.fs); cx.background() .spawn(async move { - let prettier_wrapper_path = default_prettier_dir.join(PRETTIER_SERVER_FILE); + let prettier_wrapper_path = default_prettier_dir.join(prettier::PRETTIER_SERVER_FILE); // method creates parent directory if it doesn't exist - fs.save(&prettier_wrapper_path, &Rope::from(PRETTIER_SERVER_JS), LineEnding::Unix).await - .with_context(|| format!("writing {PRETTIER_SERVER_FILE} file at {prettier_wrapper_path:?}"))?; + fs.save(&prettier_wrapper_path, &text::Rope::from(prettier::PRETTIER_SERVER_JS), text::LineEnding::Unix).await + .with_context(|| format!("writing {} file at {prettier_wrapper_path:?}", prettier::PRETTIER_SERVER_FILE))?; let packages_to_versions = future::try_join_all( prettier_plugins diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index f69d028483..206777879b 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -171,8 +171,6 @@ message Envelope { AckChannelMessage ack_channel_message = 143; GetChannelMessagesById get_channel_messages_by_id = 144; - LinkChannel link_channel = 145; - UnlinkChannel unlink_channel = 146; MoveChannel move_channel = 147; SetChannelVisibility set_channel_visibility = 148; @@ -342,6 +340,7 @@ message RoomUpdated { message LiveKitConnectionInfo { string server_url = 1; string token = 2; + bool can_publish = 3; } message ShareProject { @@ -971,13 +970,10 @@ message LspDiskBasedDiagnosticsUpdated {} message UpdateChannels { repeated Channel channels = 1; - repeated ChannelEdge insert_edge = 2; - repeated ChannelEdge delete_edge = 3; repeated uint64 delete_channels = 4; repeated Channel channel_invitations = 5; repeated uint64 remove_channel_invitations = 6; repeated ChannelParticipants channel_participants = 7; - repeated ChannelPermission channel_permissions = 8; repeated UnseenChannelMessage unseen_channel_messages = 9; repeated UnseenChannelBufferChange unseen_channel_buffer_changes = 10; } @@ -993,11 +989,6 @@ message UnseenChannelBufferChange { repeated VectorClockEntry version = 3; } -message ChannelEdge { - uint64 channel_id = 1; - uint64 parent_id = 2; -} - message ChannelPermission { uint64 channel_id = 1; ChannelRole role = 3; @@ -1137,20 +1128,9 @@ message GetChannelMessagesById { repeated uint64 message_ids = 1; } -message LinkChannel { - uint64 channel_id = 1; - uint64 to = 2; -} - -message UnlinkChannel { - uint64 channel_id = 1; - uint64 from = 2; -} - message MoveChannel { uint64 channel_id = 1; - uint64 from = 2; - uint64 to = 3; + optional uint64 to = 2; } message JoinChannelBuffer { @@ -1585,6 +1565,8 @@ message Channel { uint64 id = 1; string name = 2; ChannelVisibility visibility = 3; + ChannelRole role = 4; + repeated uint64 parent_path = 5; } message Contact { diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index c501c85107..77a69122c2 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -210,7 +210,6 @@ messages!( (LeaveChannelChat, Foreground), (LeaveProject, Foreground), (LeaveRoom, Foreground), - (LinkChannel, Foreground), (MarkNotificationRead, Foreground), (MoveChannel, Foreground), (OnTypeFormatting, Background), @@ -263,7 +262,6 @@ messages!( (SynchronizeBuffersResponse, Foreground), (Test, Foreground), (Unfollow, Foreground), - (UnlinkChannel, Foreground), (UnshareProject, Foreground), (UpdateBuffer, Foreground), (UpdateBufferFile, Foreground), @@ -327,7 +325,6 @@ request_messages!( (JoinRoom, JoinRoomResponse), (LeaveChannelBuffer, Ack), (LeaveRoom, Ack), - (LinkChannel, Ack), (MarkNotificationRead, Ack), (MoveChannel, Ack), (OnTypeFormatting, OnTypeFormattingResponse), @@ -362,7 +359,6 @@ request_messages!( (ShareProject, ShareProjectResponse), (SynchronizeBuffers, SynchronizeBuffersResponse), (Test, Test), - (UnlinkChannel, Ack), (UpdateBuffer, Ack), (UpdateParticipantLocation, Ack), (UpdateProject, Ack), diff --git a/crates/semantic_index/src/embedding_queue.rs b/crates/semantic_index/src/embedding_queue.rs index 6ae8faa4cd..d57d5c7bbe 100644 --- a/crates/semantic_index/src/embedding_queue.rs +++ b/crates/semantic_index/src/embedding_queue.rs @@ -41,6 +41,7 @@ pub struct EmbeddingQueue { pending_batch_token_count: usize, finished_files_tx: channel::Sender, finished_files_rx: channel::Receiver, + api_key: Option, } #[derive(Clone)] @@ -50,7 +51,11 @@ pub struct FileFragmentToEmbed { } impl EmbeddingQueue { - pub fn new(embedding_provider: Arc, executor: Arc) -> Self { + pub fn new( + embedding_provider: Arc, + executor: Arc, + api_key: Option, + ) -> Self { let (finished_files_tx, finished_files_rx) = channel::unbounded(); Self { embedding_provider, @@ -59,9 +64,14 @@ impl EmbeddingQueue { pending_batch_token_count: 0, finished_files_tx, finished_files_rx, + api_key, } } + pub fn set_api_key(&mut self, api_key: Option) { + self.api_key = api_key + } + pub fn push(&mut self, file: FileToEmbed) { if file.spans.is_empty() { self.finished_files_tx.try_send(file).unwrap(); @@ -108,6 +118,7 @@ impl EmbeddingQueue { let finished_files_tx = self.finished_files_tx.clone(); let embedding_provider = self.embedding_provider.clone(); + let api_key = self.api_key.clone(); self.executor .spawn(async move { @@ -132,7 +143,7 @@ impl EmbeddingQueue { return; }; - match embedding_provider.embed_batch(spans).await { + match embedding_provider.embed_batch(spans, api_key).await { Ok(embeddings) => { let mut embeddings = embeddings.into_iter(); for fragment in batch { diff --git a/crates/semantic_index/src/semantic_index.rs b/crates/semantic_index/src/semantic_index.rs index 926eb3045c..6863918d5d 100644 --- a/crates/semantic_index/src/semantic_index.rs +++ b/crates/semantic_index/src/semantic_index.rs @@ -124,6 +124,8 @@ pub struct SemanticIndex { _embedding_task: Task<()>, _parsing_files_tasks: Vec>, projects: HashMap, ProjectState>, + api_key: Option, + embedding_queue: Arc>, } struct ProjectState { @@ -269,7 +271,7 @@ pub struct SearchResult { } impl SemanticIndex { - pub fn global(cx: &AppContext) -> Option> { + pub fn global(cx: &mut AppContext) -> Option> { if cx.has_global::>() { Some(cx.global::>().clone()) } else { @@ -277,12 +279,26 @@ impl SemanticIndex { } } + pub fn authenticate(&mut self, cx: &AppContext) { + if self.api_key.is_none() { + self.api_key = self.embedding_provider.retrieve_credentials(cx); + + self.embedding_queue + .lock() + .set_api_key(self.api_key.clone()); + } + } + + pub fn is_authenticated(&self) -> bool { + self.api_key.is_some() + } + pub fn enabled(cx: &AppContext) -> bool { settings::get::(cx).enabled } pub fn status(&self, project: &ModelHandle) -> SemanticIndexStatus { - if !self.embedding_provider.is_authenticated() { + if !self.is_authenticated() { return SemanticIndexStatus::NotAuthenticated; } @@ -324,7 +340,7 @@ impl SemanticIndex { Ok(cx.add_model(|cx| { let t0 = Instant::now(); let embedding_queue = - EmbeddingQueue::new(embedding_provider.clone(), cx.background().clone()); + EmbeddingQueue::new(embedding_provider.clone(), cx.background().clone(), None); let _embedding_task = cx.background().spawn({ let embedded_files = embedding_queue.finished_files(); let db = db.clone(); @@ -389,6 +405,8 @@ impl SemanticIndex { _embedding_task, _parsing_files_tasks, projects: Default::default(), + api_key: None, + embedding_queue } })) } @@ -703,12 +721,13 @@ impl SemanticIndex { let index = self.index_project(project.clone(), cx); let embedding_provider = self.embedding_provider.clone(); + let api_key = self.api_key.clone(); cx.spawn(|this, mut cx| async move { index.await?; let t0 = Instant::now(); let query = embedding_provider - .embed_batch(vec![query]) + .embed_batch(vec![query], api_key) .await? .pop() .ok_or_else(|| anyhow!("could not embed query"))?; @@ -926,6 +945,7 @@ impl SemanticIndex { let fs = self.fs.clone(); let db_path = self.db.path().clone(); let background = cx.background().clone(); + let api_key = self.api_key.clone(); cx.background().spawn(async move { let db = VectorDatabase::new(fs, db_path.clone(), background).await?; let mut results = Vec::::new(); @@ -940,10 +960,15 @@ impl SemanticIndex { .parse_file_with_template(None, &snapshot.text(), language) .log_err() .unwrap_or_default(); - if Self::embed_spans(&mut spans, embedding_provider.as_ref(), &db) - .await - .log_err() - .is_some() + if Self::embed_spans( + &mut spans, + embedding_provider.as_ref(), + &db, + api_key.clone(), + ) + .await + .log_err() + .is_some() { for span in spans { let similarity = span.embedding.unwrap().similarity(&query); @@ -983,8 +1008,11 @@ impl SemanticIndex { project: ModelHandle, cx: &mut ModelContext, ) -> Task> { - if !self.embedding_provider.is_authenticated() { - return Task::ready(Err(anyhow!("user is not authenticated"))); + if self.api_key.is_none() { + self.authenticate(cx); + if self.api_key.is_none() { + return Task::ready(Err(anyhow!("user is not authenticated"))); + } } if !self.projects.contains_key(&project.downgrade()) { @@ -1165,6 +1193,7 @@ impl SemanticIndex { spans: &mut [Span], embedding_provider: &dyn EmbeddingProvider, db: &VectorDatabase, + api_key: Option, ) -> Result<()> { let mut batch = Vec::new(); let mut batch_tokens = 0; @@ -1187,7 +1216,7 @@ impl SemanticIndex { if batch_tokens + span.token_count > embedding_provider.max_tokens_per_batch() { let batch_embeddings = embedding_provider - .embed_batch(mem::take(&mut batch)) + .embed_batch(mem::take(&mut batch), api_key.clone()) .await?; embeddings.extend(batch_embeddings); batch_tokens = 0; @@ -1199,7 +1228,7 @@ impl SemanticIndex { if !batch.is_empty() { let batch_embeddings = embedding_provider - .embed_batch(mem::take(&mut batch)) + .embed_batch(mem::take(&mut batch), api_key) .await?; embeddings.extend(batch_embeddings); diff --git a/crates/semantic_index/src/semantic_index_tests.rs b/crates/semantic_index/src/semantic_index_tests.rs index 002dee33e3..1c117c9ea2 100644 --- a/crates/semantic_index/src/semantic_index_tests.rs +++ b/crates/semantic_index/src/semantic_index_tests.rs @@ -11,7 +11,7 @@ use ai::{ }; use anyhow::Result; use async_trait::async_trait; -use gpui::{executor::Deterministic, Task, TestAppContext}; +use gpui::{executor::Deterministic, AppContext, Task, TestAppContext}; use language::{Language, LanguageConfig, LanguageRegistry, ToOffset}; use parking_lot::Mutex; use pretty_assertions::assert_eq; @@ -232,7 +232,7 @@ async fn test_embedding_batching(cx: &mut TestAppContext, mut rng: StdRng) { let embedding_provider = Arc::new(FakeEmbeddingProvider::default()); - let mut queue = EmbeddingQueue::new(embedding_provider.clone(), cx.background()); + let mut queue = EmbeddingQueue::new(embedding_provider.clone(), cx.background(), None); for file in &files { queue.push(file.clone()); } @@ -1288,8 +1288,8 @@ impl EmbeddingProvider for FakeEmbeddingProvider { fn base_model(&self) -> Box { Box::new(DummyLanguageModel {}) } - fn is_authenticated(&self) -> bool { - true + fn retrieve_credentials(&self, _cx: &AppContext) -> Option { + Some("Fake Credentials".to_string()) } fn max_tokens_per_batch(&self) -> usize { 1000 @@ -1299,7 +1299,11 @@ impl EmbeddingProvider for FakeEmbeddingProvider { None } - async fn embed_batch(&self, spans: Vec) -> Result> { + async fn embed_batch( + &self, + spans: Vec, + _api_key: Option, + ) -> Result> { self.embedding_count .fetch_add(spans.len(), atomic::Ordering::SeqCst); diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 3f4264886f..e4b8c02eca 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -250,6 +250,7 @@ pub struct CollabPanel { pub add_contact_button: Toggleable>, pub add_channel_button: Toggleable>, pub header_row: ContainedText, + pub dragged_over_header: ContainerStyle, pub subheader_row: Toggleable>, pub leave_call: Interactive, pub contact_row: Toggleable>, diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index e8d954bc13..92010b63c0 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -40,6 +40,7 @@ pub enum Motion { NextLineStart, StartOfLineDownward, EndOfLineDownward, + GoToColumn, } #[derive(Clone, Deserialize, PartialEq)] @@ -119,6 +120,7 @@ actions!( NextLineStart, StartOfLineDownward, EndOfLineDownward, + GoToColumn, ] ); impl_actions!( @@ -215,6 +217,7 @@ pub fn init(cx: &mut AppContext) { cx.add_action(|_: &mut Workspace, &EndOfLineDownward, cx: _| { motion(Motion::EndOfLineDownward, cx) }); + cx.add_action(|_: &mut Workspace, &GoToColumn, cx: _| motion(Motion::GoToColumn, cx)); cx.add_action(|_: &mut Workspace, action: &RepeatFind, cx: _| { repeat_motion(action.backwards, cx) }) @@ -292,6 +295,7 @@ impl Motion { | Right | StartOfLine { .. } | EndOfLineDownward + | GoToColumn | NextWordStart { .. } | PreviousWordStart { .. } | FirstNonWhitespace { .. } @@ -317,6 +321,7 @@ impl Motion { | EndOfParagraph | StartOfLineDownward | EndOfLineDownward + | GoToColumn | NextWordStart { .. } | PreviousWordStart { .. } | FirstNonWhitespace { .. } @@ -346,6 +351,7 @@ impl Motion { | StartOfLineDownward | StartOfParagraph | EndOfParagraph + | GoToColumn | NextWordStart { .. } | PreviousWordStart { .. } | FirstNonWhitespace { .. } @@ -429,6 +435,7 @@ impl Motion { NextLineStart => (next_line_start(map, point, times), SelectionGoal::None), StartOfLineDownward => (next_line_start(map, point, times - 1), SelectionGoal::None), EndOfLineDownward => (next_line_end(map, point, times), SelectionGoal::None), + GoToColumn => (go_to_column(map, point, times), SelectionGoal::None), }; (new_point != point || infallible).then_some((new_point, goal)) @@ -919,6 +926,11 @@ fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> first_non_whitespace(map, false, correct_line) } +fn go_to_column(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint { + let correct_line = start_of_relative_buffer_row(map, point, 0); + right(map, correct_line, times.saturating_sub(1)) +} + pub(crate) fn next_line_end( map: &DisplaySnapshot, mut point: DisplayPoint, diff --git a/crates/vim/src/normal/delete.rs b/crates/vim/src/normal/delete.rs index 77e0e47be5..b8105aeb8d 100644 --- a/crates/vim/src/normal/delete.rs +++ b/crates/vim/src/normal/delete.rs @@ -193,10 +193,10 @@ mod test { } #[gpui::test] - async fn test_delete_e(cx: &mut gpui::TestAppContext) { + async fn test_delete_next_word_end(cx: &mut gpui::TestAppContext) { let mut cx = NeovimBackedTestContext::new(cx).await.binding(["d", "e"]); - cx.assert("Teˇst Test").await; - cx.assert("Tˇest test").await; + // cx.assert("Teˇst Test").await; + // cx.assert("Tˇest test").await; cx.assert(indoc! {" Test teˇst test"}) diff --git a/crates/vim/src/object.rs b/crates/vim/src/object.rs index 653d4ca7b6..2897d6fe91 100644 --- a/crates/vim/src/object.rs +++ b/crates/vim/src/object.rs @@ -2,7 +2,7 @@ use std::ops::Range; use editor::{ char_kind, - display_map::DisplaySnapshot, + display_map::{DisplaySnapshot, ToDisplayPoint}, movement::{self, FindRange}, Bias, CharKind, DisplayPoint, }; @@ -20,6 +20,7 @@ pub enum Object { Quotes, BackQuotes, DoubleQuotes, + VerticalBars, Parentheses, SquareBrackets, CurlyBrackets, @@ -40,6 +41,7 @@ actions!( Quotes, BackQuotes, DoubleQuotes, + VerticalBars, Parentheses, SquareBrackets, CurlyBrackets, @@ -64,6 +66,7 @@ pub fn init(cx: &mut AppContext) { }); cx.add_action(|_: &mut Workspace, _: &CurlyBrackets, cx: _| object(Object::CurlyBrackets, cx)); cx.add_action(|_: &mut Workspace, _: &AngleBrackets, cx: _| object(Object::AngleBrackets, cx)); + cx.add_action(|_: &mut Workspace, _: &VerticalBars, cx: _| object(Object::VerticalBars, cx)); } fn object(object: Object, cx: &mut WindowContext) { @@ -79,9 +82,11 @@ fn object(object: Object, cx: &mut WindowContext) { impl Object { pub fn is_multiline(self) -> bool { match self { - Object::Word { .. } | Object::Quotes | Object::BackQuotes | Object::DoubleQuotes => { - false - } + Object::Word { .. } + | Object::Quotes + | Object::BackQuotes + | Object::VerticalBars + | Object::DoubleQuotes => false, Object::Sentence | Object::Parentheses | Object::AngleBrackets @@ -96,6 +101,7 @@ impl Object { Object::Quotes | Object::BackQuotes | Object::DoubleQuotes + | Object::VerticalBars | Object::Parentheses | Object::SquareBrackets | Object::CurlyBrackets @@ -111,6 +117,7 @@ impl Object { | Object::Quotes | Object::BackQuotes | Object::DoubleQuotes + | Object::VerticalBars | Object::Parentheses | Object::SquareBrackets | Object::CurlyBrackets @@ -142,6 +149,9 @@ impl Object { Object::DoubleQuotes => { surrounding_markers(map, relative_to, around, self.is_multiline(), '"', '"') } + Object::VerticalBars => { + surrounding_markers(map, relative_to, around, self.is_multiline(), '|', '|') + } Object::Parentheses => { surrounding_markers(map, relative_to, around, self.is_multiline(), '(', ')') } @@ -427,110 +437,151 @@ fn surrounding_markers( relative_to: DisplayPoint, around: bool, search_across_lines: bool, - start_marker: char, - end_marker: char, + open_marker: char, + close_marker: char, ) -> Option> { - let mut matched_ends = 0; - let mut start = None; - for (char, mut point) in map.reverse_chars_at(relative_to) { - if char == start_marker { - if matched_ends > 0 { - matched_ends -= 1; - } else { - if around { - start = Some(point) - } else { - *point.column_mut() += char.len_utf8() as u32; - start = Some(point) + let point = relative_to.to_offset(map, Bias::Left); + + let mut matched_closes = 0; + let mut opening = None; + + if let Some((ch, range)) = movement::chars_after(map, point).next() { + if ch == open_marker { + if open_marker == close_marker { + let mut total = 0; + for (ch, _) in movement::chars_before(map, point) { + if ch == '\n' { + break; + } + if ch == open_marker { + total += 1; + } } - break; + if total % 2 == 0 { + opening = Some(range) + } + } else { + opening = Some(range) } - } else if char == end_marker { - matched_ends += 1; - } else if char == '\n' && !search_across_lines { - break; } } - let mut matched_starts = 0; - let mut end = None; - for (char, mut point) in map.chars_at(relative_to) { - if char == end_marker { - if start.is_none() { + if opening.is_none() { + for (ch, range) in movement::chars_before(map, point) { + if ch == '\n' && !search_across_lines { break; } - if matched_starts > 0 { - matched_starts -= 1; - } else { - if around { - *point.column_mut() += char.len_utf8() as u32; - end = Some(point); - } else { - end = Some(point); + if ch == open_marker { + if matched_closes == 0 { + opening = Some(range); + break; } - - break; + matched_closes -= 1; + } else if ch == close_marker { + matched_closes += 1 } } - - if char == start_marker { - if start.is_none() { - if around { - start = Some(point); - } else { - *point.column_mut() += char.len_utf8() as u32; - start = Some(point); - } - } else { - matched_starts += 1; - } - } - - if char == '\n' && !search_across_lines { - break; - } } - let (Some(mut start), Some(mut end)) = (start, end) else { + if opening.is_none() { + for (ch, range) in movement::chars_after(map, point) { + if ch == open_marker { + opening = Some(range); + break; + } else if ch == close_marker { + break; + } + } + } + + let Some(mut opening) = opening else { return None; }; - if !around { - // if a block starts with a newline, move the start to after the newline. - let mut was_newline = false; - for (char, point) in map.chars_at(start) { - if was_newline { - start = point; - } else if char == '\n' { - was_newline = true; - continue; - } + let mut matched_opens = 0; + let mut closing = None; + + for (ch, range) in movement::chars_after(map, opening.end) { + if ch == '\n' && !search_across_lines { break; } - // if a block ends with a newline, then whitespace, then the delimeter, - // move the end to after the newline. - let mut new_end = end; - for (char, point) in map.reverse_chars_at(end) { - if char == '\n' { - end = new_end; + + if ch == close_marker { + if matched_opens == 0 { + closing = Some(range); break; } - if !char.is_whitespace() { - break; - } - new_end = point + matched_opens -= 1; + } else if ch == open_marker { + matched_opens += 1; } } - Some(start..end) + let Some(mut closing) = closing else { + return None; + }; + + if around && !search_across_lines { + let mut found = false; + + for (ch, range) in movement::chars_after(map, closing.end) { + if ch.is_whitespace() && ch != '\n' { + found = true; + closing.end = range.end; + } else { + break; + } + } + + if !found { + for (ch, range) in movement::chars_before(map, opening.start) { + if ch.is_whitespace() && ch != '\n' { + opening.start = range.start + } else { + break; + } + } + } + } + + if !around && search_across_lines { + if let Some((ch, range)) = movement::chars_after(map, opening.end).next() { + if ch == '\n' { + opening.end = range.end + } + } + + for (ch, range) in movement::chars_before(map, closing.start) { + if !ch.is_whitespace() { + break; + } + if ch != '\n' { + closing.start = range.start + } + } + } + + let result = if around { + opening.start..closing.end + } else { + opening.end..closing.start + }; + + Some( + map.clip_point(result.start.to_display_point(map), Bias::Left) + ..map.clip_point(result.end.to_display_point(map), Bias::Right), + ) } #[cfg(test)] mod test { use indoc::indoc; - use crate::test::{ExemptionFeatures, NeovimBackedTestContext}; + use crate::{ + state::Mode, + test::{ExemptionFeatures, NeovimBackedTestContext, VimTestContext}, + }; const WORD_LOCATIONS: &'static str = indoc! {" The quick ˇbrowˇnˇ••• @@ -765,13 +816,6 @@ mod test { let mut cx = NeovimBackedTestContext::new(cx).await; for (start, end) in SURROUNDING_OBJECTS { - if ((start == &'\'' || start == &'`' || start == &'"') - && !ExemptionFeatures::QuotesSeekForward.supported()) - || (start == &'<' && !ExemptionFeatures::AngleBracketsFreezeNeovim.supported()) - { - continue; - } - let marked_string = SURROUNDING_MARKER_STRING .replace('`', &start.to_string()) .replace('\'', &end.to_string()); @@ -786,6 +830,63 @@ mod test { .await; } } + #[gpui::test] + async fn test_singleline_surrounding_character_objects(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + cx.set_shared_wrap(12).await; + + cx.set_shared_state(indoc! { + "helˇlo \"world\"!" + }) + .await; + cx.simulate_shared_keystrokes(["v", "i", "\""]).await; + cx.assert_shared_state(indoc! { + "hello \"«worldˇ»\"!" + }) + .await; + + cx.set_shared_state(indoc! { + "hello \"wˇorld\"!" + }) + .await; + cx.simulate_shared_keystrokes(["v", "i", "\""]).await; + cx.assert_shared_state(indoc! { + "hello \"«worldˇ»\"!" + }) + .await; + + cx.set_shared_state(indoc! { + "hello \"wˇorld\"!" + }) + .await; + cx.simulate_shared_keystrokes(["v", "a", "\""]).await; + cx.assert_shared_state(indoc! { + "hello« \"world\"ˇ»!" + }) + .await; + + cx.set_shared_state(indoc! { + "hello \"wˇorld\" !" + }) + .await; + cx.simulate_shared_keystrokes(["v", "a", "\""]).await; + cx.assert_shared_state(indoc! { + "hello «\"world\" ˇ»!" + }) + .await; + + cx.set_shared_state(indoc! { + "hello \"wˇorld\"• + goodbye" + }) + .await; + cx.simulate_shared_keystrokes(["v", "a", "\""]).await; + cx.assert_shared_state(indoc! { + "hello «\"world\" ˇ» + goodbye" + }) + .await; + } #[gpui::test] async fn test_multiline_surrounding_character_objects(cx: &mut gpui::TestAppContext) { @@ -827,6 +928,66 @@ mod test { return false }"}) .await; + + cx.set_shared_state(indoc! { + "func empty(a string) bool { + if a == \"\" ˇ{ + return true + } + return false + }" + }) + .await; + cx.simulate_shared_keystrokes(["v", "i", "{"]).await; + cx.assert_shared_state(indoc! {" + func empty(a string) bool { + if a == \"\" { + « return true + ˇ» } + return false + }"}) + .await; + } + + #[gpui::test] + async fn test_vertical_bars(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + cx.set_state( + indoc! {" + fn boop() { + baz(ˇ|a, b| { bar(|j, k| { })}) + }" + }, + Mode::Normal, + ); + cx.simulate_keystrokes(["c", "i", "|"]); + cx.assert_state( + indoc! {" + fn boop() { + baz(|ˇ| { bar(|j, k| { })}) + }" + }, + Mode::Insert, + ); + cx.simulate_keystrokes(["escape", "1", "8", "|"]); + cx.assert_state( + indoc! {" + fn boop() { + baz(|| { bar(ˇ|j, k| { })}) + }" + }, + Mode::Normal, + ); + + cx.simulate_keystrokes(["v", "a", "|"]); + cx.assert_state( + indoc! {" + fn boop() { + baz(|| { bar(«|j, k| ˇ»{ })}) + }" + }, + Mode::Visual, + ); } #[gpui::test] @@ -834,12 +995,6 @@ mod test { let mut cx = NeovimBackedTestContext::new(cx).await; for (start, end) in SURROUNDING_OBJECTS { - if ((start == &'\'' || start == &'`' || start == &'"') - && !ExemptionFeatures::QuotesSeekForward.supported()) - || (start == &'<' && !ExemptionFeatures::AngleBracketsFreezeNeovim.supported()) - { - continue; - } let marked_string = SURROUNDING_MARKER_STRING .replace('`', &start.to_string()) .replace('\'', &end.to_string()); diff --git a/crates/vim/src/test.rs b/crates/vim/src/test.rs index 4fb87e70a0..9a6976183b 100644 --- a/crates/vim/src/test.rs +++ b/crates/vim/src/test.rs @@ -734,3 +734,26 @@ async fn test_paragraphs_dont_wrap(cx: &mut gpui::TestAppContext) { two"}) .await; } + +#[gpui::test] +async fn test_select_all_issue_2170(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + + cx.set_state( + indoc! {" + defmodule Test do + def test(a, ˇ[_, _] = b), do: IO.puts('hi') + end + "}, + Mode::Normal, + ); + cx.simulate_keystrokes(["g", "a"]); + cx.assert_state( + indoc! {" + defmodule Test do + def test(a, «[ˇ»_, _] = b), do: IO.puts('hi') + end + "}, + Mode::Visual, + ); +} diff --git a/crates/vim/src/test/neovim_backed_test_context.rs b/crates/vim/src/test/neovim_backed_test_context.rs index 324e2e9f45..7944e9297c 100644 --- a/crates/vim/src/test/neovim_backed_test_context.rs +++ b/crates/vim/src/test/neovim_backed_test_context.rs @@ -1,15 +1,12 @@ use editor::scroll::VERTICAL_SCROLL_MARGIN; use indoc::indoc; use settings::SettingsStore; -use std::ops::{Deref, DerefMut, Range}; +use std::ops::{Deref, DerefMut}; use collections::{HashMap, HashSet}; use gpui::{geometry::vector::vec2f, ContextHandle}; -use language::{ - language_settings::{AllLanguageSettings, SoftWrap}, - OffsetRangeExt, -}; -use util::test::{generate_marked_text, marked_text_offsets}; +use language::language_settings::{AllLanguageSettings, SoftWrap}; +use util::test::marked_text_offsets; use super::{neovim_connection::NeovimConnection, NeovimBackedBindingTestContext, VimTestContext}; use crate::state::Mode; @@ -37,10 +34,6 @@ pub enum ExemptionFeatures { AroundSentenceStartingBetweenIncludesWrongWhitespace, // Non empty selection with text objects in visual mode NonEmptyVisualTextObjects, - // Quote style surrounding text objects don't seek forward properly - QuotesSeekForward, - // Neovim freezes up for some reason with angle brackets - AngleBracketsFreezeNeovim, // Sentence Doesn't backtrack when its at the end of the file SentenceAfterPunctuationAtEndOfFile, } @@ -250,25 +243,13 @@ impl<'a> NeovimBackedTestContext<'a> { } pub async fn neovim_state(&mut self) -> String { - generate_marked_text( - self.neovim.text().await.as_str(), - &self.neovim_selections().await[..], - true, - ) + self.neovim.marked_text().await } pub async fn neovim_mode(&mut self) -> Mode { self.neovim.mode().await.unwrap() } - async fn neovim_selections(&mut self) -> Vec> { - let neovim_selections = self.neovim.selections().await; - neovim_selections - .into_iter() - .map(|selection| selection.to_offset(&self.buffer_snapshot())) - .collect() - } - pub async fn assert_state_matches(&mut self) { self.is_dirty = false; let neovim = self.neovim_state().await; diff --git a/crates/vim/src/test/neovim_connection.rs b/crates/vim/src/test/neovim_connection.rs index 38af2d1555..16c718b857 100644 --- a/crates/vim/src/test/neovim_connection.rs +++ b/crates/vim/src/test/neovim_connection.rs @@ -1,9 +1,9 @@ +use std::path::PathBuf; #[cfg(feature = "neovim")] use std::{ cmp, - ops::{Deref, DerefMut}, + ops::{Deref, DerefMut, Range}, }; -use std::{ops::Range, path::PathBuf}; #[cfg(feature = "neovim")] use async_compat::Compat; @@ -12,6 +12,7 @@ use async_trait::async_trait; #[cfg(feature = "neovim")] use gpui::keymap_matcher::Keystroke; +#[cfg(feature = "neovim")] use language::Point; #[cfg(feature = "neovim")] @@ -109,7 +110,12 @@ impl NeovimConnection { // Sends a keystroke to the neovim process. #[cfg(feature = "neovim")] pub async fn send_keystroke(&mut self, keystroke_text: &str) { - let keystroke = Keystroke::parse(keystroke_text).unwrap(); + let mut keystroke = Keystroke::parse(keystroke_text).unwrap(); + + if keystroke.key == "<" { + keystroke.key = "lt".to_string() + } + let special = keystroke.shift || keystroke.ctrl || keystroke.alt @@ -296,7 +302,7 @@ impl NeovimConnection { } #[cfg(feature = "neovim")] - pub async fn state(&mut self) -> (Option, String, Vec>) { + pub async fn state(&mut self) -> (Option, String) { let nvim_buffer = self .nvim .get_current_buf() @@ -405,37 +411,33 @@ impl NeovimConnection { .push(Point::new(selection_row, selection_col)..Point::new(cursor_row, cursor_col)), } + let ranges = encode_ranges(&text, &selections); let state = NeovimData::Get { mode, - state: encode_ranges(&text, &selections), + state: ranges.clone(), }; if self.data.back() != Some(&state) { self.data.push_back(state.clone()); } - (mode, text, selections) + (mode, ranges) } #[cfg(not(feature = "neovim"))] - pub async fn state(&mut self) -> (Option, String, Vec>) { - if let Some(NeovimData::Get { state: text, mode }) = self.data.front() { - let (text, ranges) = parse_state(text); - (*mode, text, ranges) + pub async fn state(&mut self) -> (Option, String) { + if let Some(NeovimData::Get { state: raw, mode }) = self.data.front() { + (*mode, raw.to_string()) } else { panic!("operation does not match recorded script. re-record with --features=neovim"); } } - pub async fn selections(&mut self) -> Vec> { - self.state().await.2 - } - pub async fn mode(&mut self) -> Option { self.state().await.0 } - pub async fn text(&mut self) -> String { + pub async fn marked_text(&mut self) -> String { self.state().await.1 } @@ -527,6 +529,7 @@ impl Handler for NvimHandler { } } +#[cfg(feature = "neovim")] fn parse_state(marked_text: &str) -> (String, Vec>) { let (text, ranges) = util::test::marked_text_ranges(marked_text, true); let point_ranges = ranges diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index 5d6477ff5b..ed244c75fd 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use std::{cmp, sync::Arc}; +use std::sync::Arc; use collections::HashMap; use editor::{ @@ -263,21 +263,13 @@ pub fn visual_object(object: Object, cx: &mut WindowContext) { if let Some(range) = object.range(map, head, around) { if !range.is_empty() { - let expand_both_ways = - if object.always_expands_both_ways() || selection.is_empty() { - true - // contains only one character - } else if let Some((_, start)) = - map.reverse_chars_at(selection.end).next() - { - selection.start == start - } else { - false - }; + let expand_both_ways = object.always_expands_both_ways() + || selection.is_empty() + || movement::right(map, selection.start) == selection.end; if expand_both_ways { - selection.start = cmp::min(selection.start, range.start); - selection.end = cmp::max(selection.end, range.end); + selection.start = range.start; + selection.end = range.end; } else if selection.reversed { selection.start = range.start; } else { diff --git a/crates/vim/test_data/test_G.json b/crates/vim/test_data/test_G.json deleted file mode 100644 index de9e29e4aa..0000000000 --- a/crates/vim/test_data/test_G.json +++ /dev/null @@ -1 +0,0 @@ -[{"Text":"The quick\n\nbrown fox jumps\nover the lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[3,5],"end":[3,5]}}] \ No newline at end of file diff --git a/crates/vim/test_data/test_change_surrounding_character_objects.json b/crates/vim/test_data/test_change_surrounding_character_objects.json index a88f84cf4b..2e60b7729b 100644 --- a/crates/vim/test_data/test_change_surrounding_character_objects.json +++ b/crates/vim/test_data/test_change_surrounding_character_objects.json @@ -1,3 +1,1023 @@ +{"Put":{"state":"ˇTh'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'ˇ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}} +{"Put":{"state":"Th'ˇe ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'ˇ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}} +{"Put":{"state":"Th'e ˇ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'ˇ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}} +{"Put":{"state":"Th'e 'ˇ'qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''ˇ'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}} +{"Put":{"state":"Th'e ''ˇqui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''ˇ'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}} +{"Put":{"state":"Th'e ''quˇi'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''ˇ'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}} +{"Put":{"state":"Th'e ''qui'ˇck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ˇ'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}} +{"Put":{"state":"Th'e ''qui'ck broˇ'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'ˇ'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'ˇfox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'ˇ'er\nthe lazy d'o'g","mode":"Insert"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox juˇmps ov'er\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'ˇ'er\nthe lazy d'o'g","mode":"Insert"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ovˇ'er\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'ˇ'er\nthe lazy d'o'g","mode":"Insert"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'ˇer\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'ˇer\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe ˇlazy d'o'g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'ˇ'g","mode":"Insert"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy dˇ'o'g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'ˇ'g","mode":"Insert"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'ˇo'g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'ˇ'g","mode":"Insert"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'oˇ'g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'ˇ'g","mode":"Insert"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'ˇg"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'ˇg","mode":"Normal"}} +{"Put":{"state":"ˇTh'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'ˇ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}} +{"Put":{"state":"Th'ˇe ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'ˇ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}} +{"Put":{"state":"Th'e ˇ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'ˇ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}} +{"Put":{"state":"Th'e 'ˇ'qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''ˇ'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}} +{"Put":{"state":"Th'e ''ˇqui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''ˇ'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}} +{"Put":{"state":"Th'e ''quˇi'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''ˇ'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}} +{"Put":{"state":"Th'e ''qui'ˇck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ˇ'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}} +{"Put":{"state":"Th'e ''qui'ck broˇ'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'ˇ'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'ˇfox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'ˇ'er\nthe lazy d'o'g","mode":"Insert"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox juˇmps ov'er\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'ˇ'er\nthe lazy d'o'g","mode":"Insert"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ovˇ'er\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'ˇ'er\nthe lazy d'o'g","mode":"Insert"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'ˇer\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'ˇer\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe ˇlazy d'o'g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'ˇ'g","mode":"Insert"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy dˇ'o'g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'ˇ'g","mode":"Insert"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'ˇo'g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'ˇ'g","mode":"Insert"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'oˇ'g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'ˇ'g","mode":"Insert"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'ˇg"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'ˇg","mode":"Normal"}} +{"Put":{"state":"ˇTh'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Thˇ'qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}} +{"Put":{"state":"Th'ˇe ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Thˇ'qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}} +{"Put":{"state":"Th'e ˇ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Thˇ'qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}} +{"Put":{"state":"Th'e 'ˇ'qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e 'ˇck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}} +{"Put":{"state":"Th'e ''ˇqui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e 'ˇck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}} +{"Put":{"state":"Th'e ''quˇi'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e 'ˇck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}} +{"Put":{"state":"Th'e ''qui'ˇck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e ''quiˇwn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}} +{"Put":{"state":"Th'e ''qui'ck broˇ'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck broˇ\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'ˇfox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\nˇer\nthe lazy d'o'g","mode":"Insert"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox juˇmps ov'er\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\nˇer\nthe lazy d'o'g","mode":"Insert"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ovˇ'er\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\nˇer\nthe lazy d'o'g","mode":"Insert"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'ˇer\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'ˇer\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe ˇlazy d'o'g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy dˇg","mode":"Insert"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy dˇ'o'g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy dˇg","mode":"Insert"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'ˇo'g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy dˇg","mode":"Insert"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'oˇ'g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy dˇg","mode":"Insert"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'ˇg"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'ˇg","mode":"Normal"}} +{"Put":{"state":"ˇTh'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Thˇ'qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}} +{"Put":{"state":"Th'ˇe ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Thˇ'qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}} +{"Put":{"state":"Th'e ˇ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Thˇ'qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}} +{"Put":{"state":"Th'e 'ˇ'qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e 'ˇck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}} +{"Put":{"state":"Th'e ''ˇqui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e 'ˇck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}} +{"Put":{"state":"Th'e ''quˇi'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e 'ˇck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}} +{"Put":{"state":"Th'e ''qui'ˇck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e ''quiˇwn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}} +{"Put":{"state":"Th'e ''qui'ck broˇ'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck broˇ\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Insert"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'ˇfox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\nˇer\nthe lazy d'o'g","mode":"Insert"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox juˇmps ov'er\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\nˇer\nthe lazy d'o'g","mode":"Insert"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ovˇ'er\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\nˇer\nthe lazy d'o'g","mode":"Insert"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'ˇer\nthe lazy d'o'g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'ˇer\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe ˇlazy d'o'g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy dˇg","mode":"Insert"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy dˇ'o'g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy dˇg","mode":"Insert"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'ˇo'g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy dˇg","mode":"Insert"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'oˇ'g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy dˇg","mode":"Insert"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'ˇg"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'ˇg","mode":"Normal"}} +{"Put":{"state":"ˇTh`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`ˇ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}} +{"Put":{"state":"Th`ˇe ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`ˇ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}} +{"Put":{"state":"Th`e ˇ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`ˇ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}} +{"Put":{"state":"Th`e `ˇ`qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``ˇ`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}} +{"Put":{"state":"Th`e ``ˇqui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``ˇ`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}} +{"Put":{"state":"Th`e ``quˇi`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``ˇ`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}} +{"Put":{"state":"Th`e ``qui`ˇck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ˇ`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}} +{"Put":{"state":"Th`e ``qui`ck broˇ`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`ˇ`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`ˇfox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`ˇ`er\nthe lazy d`o`g","mode":"Insert"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox juˇmps ov`er\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`ˇ`er\nthe lazy d`o`g","mode":"Insert"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ovˇ`er\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`ˇ`er\nthe lazy d`o`g","mode":"Insert"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`ˇer\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`ˇer\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe ˇlazy d`o`g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`ˇ`g","mode":"Insert"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy dˇ`o`g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`ˇ`g","mode":"Insert"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`ˇo`g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`ˇ`g","mode":"Insert"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`oˇ`g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`ˇ`g","mode":"Insert"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`ˇg"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`ˇg","mode":"Normal"}} +{"Put":{"state":"ˇTh`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`ˇ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}} +{"Put":{"state":"Th`ˇe ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`ˇ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}} +{"Put":{"state":"Th`e ˇ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`ˇ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}} +{"Put":{"state":"Th`e `ˇ`qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``ˇ`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}} +{"Put":{"state":"Th`e ``ˇqui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``ˇ`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}} +{"Put":{"state":"Th`e ``quˇi`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``ˇ`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}} +{"Put":{"state":"Th`e ``qui`ˇck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ˇ`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}} +{"Put":{"state":"Th`e ``qui`ck broˇ`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`ˇ`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`ˇfox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`ˇ`er\nthe lazy d`o`g","mode":"Insert"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox juˇmps ov`er\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`ˇ`er\nthe lazy d`o`g","mode":"Insert"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ovˇ`er\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`ˇ`er\nthe lazy d`o`g","mode":"Insert"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`ˇer\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`ˇer\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe ˇlazy d`o`g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`ˇ`g","mode":"Insert"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy dˇ`o`g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`ˇ`g","mode":"Insert"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`ˇo`g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`ˇ`g","mode":"Insert"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`oˇ`g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`ˇ`g","mode":"Insert"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`ˇg"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`ˇg","mode":"Normal"}} +{"Put":{"state":"ˇTh`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Thˇ`qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}} +{"Put":{"state":"Th`ˇe ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Thˇ`qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}} +{"Put":{"state":"Th`e ˇ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Thˇ`qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}} +{"Put":{"state":"Th`e `ˇ`qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e `ˇck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}} +{"Put":{"state":"Th`e ``ˇqui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e `ˇck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}} +{"Put":{"state":"Th`e ``quˇi`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e `ˇck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}} +{"Put":{"state":"Th`e ``qui`ˇck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e ``quiˇwn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}} +{"Put":{"state":"Th`e ``qui`ck broˇ`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck broˇ\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`ˇfox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\nˇer\nthe lazy d`o`g","mode":"Insert"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox juˇmps ov`er\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\nˇer\nthe lazy d`o`g","mode":"Insert"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ovˇ`er\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\nˇer\nthe lazy d`o`g","mode":"Insert"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`ˇer\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`ˇer\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe ˇlazy d`o`g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy dˇg","mode":"Insert"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy dˇ`o`g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy dˇg","mode":"Insert"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`ˇo`g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy dˇg","mode":"Insert"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`oˇ`g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy dˇg","mode":"Insert"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`ˇg"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`ˇg","mode":"Normal"}} +{"Put":{"state":"ˇTh`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Thˇ`qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}} +{"Put":{"state":"Th`ˇe ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Thˇ`qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}} +{"Put":{"state":"Th`e ˇ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Thˇ`qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}} +{"Put":{"state":"Th`e `ˇ`qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e `ˇck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}} +{"Put":{"state":"Th`e ``ˇqui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e `ˇck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}} +{"Put":{"state":"Th`e ``quˇi`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e `ˇck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}} +{"Put":{"state":"Th`e ``qui`ˇck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e ``quiˇwn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}} +{"Put":{"state":"Th`e ``qui`ck broˇ`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck broˇ\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Insert"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`ˇfox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\nˇer\nthe lazy d`o`g","mode":"Insert"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox juˇmps ov`er\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\nˇer\nthe lazy d`o`g","mode":"Insert"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ovˇ`er\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\nˇer\nthe lazy d`o`g","mode":"Insert"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`ˇer\nthe lazy d`o`g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`ˇer\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe ˇlazy d`o`g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy dˇg","mode":"Insert"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy dˇ`o`g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy dˇg","mode":"Insert"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`ˇo`g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy dˇg","mode":"Insert"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`oˇ`g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy dˇg","mode":"Insert"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`ˇg"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`ˇg","mode":"Normal"}} +{"Put":{"state":"ˇTh\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} +{"Put":{"state":"Th\"ˇe \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e \"ˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"ˇ\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"ˇqui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"ˇ\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"quˇi\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"ˇ\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"qui\"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ˇ\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"qui\"ck broˇ\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"ˇ\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇfox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇ\"er\nthe lazy d\"o\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox juˇmps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇ\"er\nthe lazy d\"o\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ovˇ\"er\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇ\"er\nthe lazy d\"o\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"ˇer\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"ˇer\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe ˇlazy d\"o\"g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"ˇ\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy dˇ\"o\"g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"ˇ\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"ˇo\"g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"ˇ\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"oˇ\"g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"ˇ\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"ˇg"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"ˇg","mode":"Normal"}} +{"Put":{"state":"ˇTh\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} +{"Put":{"state":"Th\"ˇe \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e \"ˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"ˇ\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"ˇqui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"ˇ\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"quˇi\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"ˇ\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"qui\"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ˇ\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"qui\"ck broˇ\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"ˇ\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇfox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇ\"er\nthe lazy d\"o\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox juˇmps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇ\"er\nthe lazy d\"o\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ovˇ\"er\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇ\"er\nthe lazy d\"o\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"ˇer\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"ˇer\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe ˇlazy d\"o\"g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"ˇ\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy dˇ\"o\"g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"ˇ\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"ˇo\"g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"ˇ\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"oˇ\"g"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"ˇ\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"ˇg"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"ˇg","mode":"Normal"}} +{"Put":{"state":"ˇTh\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Thˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} +{"Put":{"state":"Th\"ˇe \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Thˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Thˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e \"ˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"ˇqui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"quˇi\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"qui\"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"quiˇwn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"qui\"ck broˇ\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck broˇ\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇfox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\nˇer\nthe lazy d\"o\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox juˇmps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\nˇer\nthe lazy d\"o\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ovˇ\"er\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\nˇer\nthe lazy d\"o\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"ˇer\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"ˇer\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe ˇlazy d\"o\"g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy dˇg","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy dˇ\"o\"g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy dˇg","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"ˇo\"g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy dˇg","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"oˇ\"g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy dˇg","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"ˇg"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"ˇg","mode":"Normal"}} +{"Put":{"state":"ˇTh\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Thˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} +{"Put":{"state":"Th\"ˇe \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Thˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Thˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e \"ˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"ˇqui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"quˇi\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"qui\"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"quiˇwn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"qui\"ck broˇ\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck broˇ\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇfox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\nˇer\nthe lazy d\"o\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox juˇmps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\nˇer\nthe lazy d\"o\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ovˇ\"er\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\nˇer\nthe lazy d\"o\"g","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"ˇer\nthe lazy d\"o\"g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"ˇer\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe ˇlazy d\"o\"g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy dˇg","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy dˇ\"o\"g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy dˇg","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"ˇo\"g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy dˇg","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"oˇ\"g"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy dˇg","mode":"Insert"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"ˇg"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"ˇg","mode":"Normal"}} {"Put":{"state":"ˇTh)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} {"Key":"c"} {"Key":"i"} @@ -1018,3 +2038,343 @@ {"Key":"a"} {"Key":"}"} {"Get":{"state":"Th}e {}qui{ck bro}wn{\n}fox jumps ov{er\nthe lazy d}o{ˇg","mode":"Normal"}} +{"Put":{"state":"ˇTh>e <>quiwn<\n>fox jumps ovoe <>quiwn<\n>fox jumps ovoˇe <>quiwn<\n>fox jumps ovoe <ˇ>quiwn<\n>fox jumps ovoe ˇ<>quiwn<\n>fox jumps ovoe <ˇ>quiwn<\n>fox jumps ovoe <ˇ>quiwn<\n>fox jumps ovoe <ˇ>quiwn<\n>fox jumps ovoe <>ˇquiwn<\n>fox jumps ovoe <>qui<ˇ>wn<\n>fox jumps ovoe <>quˇiwn<\n>fox jumps ovoe <>qui<ˇ>wn<\n>fox jumps ovoe <>qui<ˇck bro>wn<\n>fox jumps ovoe <>qui<ˇ>wn<\n>fox jumps ovoe <>quiwn<\n>fox jumps ovoe <>qui<ˇ>wn<\n>fox jumps ovoe <>quiwn<\n>ˇfox jumps ovoe <>quiwn<\n>fox jumps ov<ˇ>oe <>quiwn<\n>fox juˇmps ovoe <>quiwn<\n>fox jumps ov<ˇ>oe <>quiwn<\n>fox jumps ovˇoe <>quiwn<\n>fox jumps ov<ˇ>oe <>quiwn<\n>fox jumps ov<ˇer\nthe lazy d>oe <>quiwn<\n>fox jumps ov<ˇ>oe <>quiwn<\n>fox jumps ovoe <>quiwn<\n>fox jumps ov<ˇ>oe <>quiwn<\n>fox jumps ovoe <>quiwn<\n>fox jumps ov<ˇ>oe <>quiwn<\n>fox jumps ovˇoe <>quiwn<\n>fox jumps ovˇoe <>quiwn<\n>fox jumps ove <>quiwn<\n>fox jumps ove <>quiwn<\n>fox jumps ovo<ˇg"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"<"} +{"Get":{"state":"Th>e <>quiwn<\n>fox jumps ovo<ˇg","mode":"Normal"}} +{"Put":{"state":"ˇTh>e <>quiwn<\n>fox jumps ovo"} +{"Get":{"state":"ˇTh>e <>quiwn<\n>fox jumps ovoˇe <>quiwn<\n>fox jumps ovo"} +{"Get":{"state":"Th>e <ˇ>quiwn<\n>fox jumps ovoe ˇ<>quiwn<\n>fox jumps ovo"} +{"Get":{"state":"Th>e <ˇ>quiwn<\n>fox jumps ovoe <ˇ>quiwn<\n>fox jumps ovo"} +{"Get":{"state":"Th>e <ˇ>quiwn<\n>fox jumps ovoe <>ˇquiwn<\n>fox jumps ovo"} +{"Get":{"state":"Th>e <>qui<ˇ>wn<\n>fox jumps ovoe <>quˇiwn<\n>fox jumps ovo"} +{"Get":{"state":"Th>e <>qui<ˇ>wn<\n>fox jumps ovoe <>qui<ˇck bro>wn<\n>fox jumps ovo"} +{"Get":{"state":"Th>e <>qui<ˇ>wn<\n>fox jumps ovoe <>quiwn<\n>fox jumps ovo"} +{"Get":{"state":"Th>e <>qui<ˇ>wn<\n>fox jumps ovoe <>quiwn<\n>ˇfox jumps ovo"} +{"Get":{"state":"Th>e <>quiwn<\n>fox jumps ov<ˇ>oe <>quiwn<\n>fox juˇmps ovo"} +{"Get":{"state":"Th>e <>quiwn<\n>fox jumps ov<ˇ>oe <>quiwn<\n>fox jumps ovˇo"} +{"Get":{"state":"Th>e <>quiwn<\n>fox jumps ov<ˇ>oe <>quiwn<\n>fox jumps ov<ˇer\nthe lazy d>o"} +{"Get":{"state":"Th>e <>quiwn<\n>fox jumps ov<ˇ>oe <>quiwn<\n>fox jumps ovo"} +{"Get":{"state":"Th>e <>quiwn<\n>fox jumps ov<ˇ>oe <>quiwn<\n>fox jumps ovo"} +{"Get":{"state":"Th>e <>quiwn<\n>fox jumps ov<ˇ>oe <>quiwn<\n>fox jumps ovˇo"} +{"Get":{"state":"Th>e <>quiwn<\n>fox jumps ovˇoe <>quiwn<\n>fox jumps ov"} +{"Get":{"state":"Th>e <>quiwn<\n>fox jumps ove <>quiwn<\n>fox jumps ovo<ˇg"}} +{"Key":"c"} +{"Key":"i"} +{"Key":">"} +{"Get":{"state":"Th>e <>quiwn<\n>fox jumps ovo<ˇg","mode":"Normal"}} +{"Put":{"state":"ˇTh>e <>quiwn<\n>fox jumps ovoe <>quiwn<\n>fox jumps ovoˇe <>quiwn<\n>fox jumps ovoe ˇquiwn<\n>fox jumps ovoe ˇ<>quiwn<\n>fox jumps ovoe ˇquiwn<\n>fox jumps ovoe <ˇ>quiwn<\n>fox jumps ovoe ˇquiwn<\n>fox jumps ovoe <>ˇquiwn<\n>fox jumps ovoe <>quiˇwn<\n>fox jumps ovoe <>quˇiwn<\n>fox jumps ovoe <>quiˇwn<\n>fox jumps ovoe <>qui<ˇck bro>wn<\n>fox jumps ovoe <>quiˇwn<\n>fox jumps ovoe <>quiwn<\n>fox jumps ovoe <>quiˇwn<\n>fox jumps ovoe <>quiwn<\n>ˇfox jumps ovoe <>quiwn<\n>fox jumps ovˇoe <>quiwn<\n>fox juˇmps ovoe <>quiwn<\n>fox jumps ovˇoe <>quiwn<\n>fox jumps ovˇoe <>quiwn<\n>fox jumps ovˇoe <>quiwn<\n>fox jumps ov<ˇer\nthe lazy d>oe <>quiwn<\n>fox jumps ovˇoe <>quiwn<\n>fox jumps ovoe <>quiwn<\n>fox jumps ovˇoe <>quiwn<\n>fox jumps ovoe <>quiwn<\n>fox jumps ovˇoe <>quiwn<\n>fox jumps ovˇoe <>quiwn<\n>fox jumps ovˇoe <>quiwn<\n>fox jumps ove <>quiwn<\n>fox jumps ove <>quiwn<\n>fox jumps ovo<ˇg"}} +{"Key":"c"} +{"Key":"a"} +{"Key":"<"} +{"Get":{"state":"Th>e <>quiwn<\n>fox jumps ovo<ˇg","mode":"Normal"}} +{"Put":{"state":"ˇTh>e <>quiwn<\n>fox jumps ovo"} +{"Get":{"state":"ˇTh>e <>quiwn<\n>fox jumps ovoˇe <>quiwn<\n>fox jumps ovo"} +{"Get":{"state":"Th>e ˇquiwn<\n>fox jumps ovoe ˇ<>quiwn<\n>fox jumps ovo"} +{"Get":{"state":"Th>e ˇquiwn<\n>fox jumps ovoe <ˇ>quiwn<\n>fox jumps ovo"} +{"Get":{"state":"Th>e ˇquiwn<\n>fox jumps ovoe <>ˇquiwn<\n>fox jumps ovo"} +{"Get":{"state":"Th>e <>quiˇwn<\n>fox jumps ovoe <>quˇiwn<\n>fox jumps ovo"} +{"Get":{"state":"Th>e <>quiˇwn<\n>fox jumps ovoe <>qui<ˇck bro>wn<\n>fox jumps ovo"} +{"Get":{"state":"Th>e <>quiˇwn<\n>fox jumps ovoe <>quiwn<\n>fox jumps ovo"} +{"Get":{"state":"Th>e <>quiˇwn<\n>fox jumps ovoe <>quiwn<\n>ˇfox jumps ovo"} +{"Get":{"state":"Th>e <>quiwn<\n>fox jumps ovˇoe <>quiwn<\n>fox juˇmps ovo"} +{"Get":{"state":"Th>e <>quiwn<\n>fox jumps ovˇoe <>quiwn<\n>fox jumps ovˇo"} +{"Get":{"state":"Th>e <>quiwn<\n>fox jumps ovˇoe <>quiwn<\n>fox jumps ov<ˇer\nthe lazy d>o"} +{"Get":{"state":"Th>e <>quiwn<\n>fox jumps ovˇoe <>quiwn<\n>fox jumps ovo"} +{"Get":{"state":"Th>e <>quiwn<\n>fox jumps ovˇoe <>quiwn<\n>fox jumps ovo"} +{"Get":{"state":"Th>e <>quiwn<\n>fox jumps ovˇoe <>quiwn<\n>fox jumps ovˇo"} +{"Get":{"state":"Th>e <>quiwn<\n>fox jumps ovˇoe <>quiwn<\n>fox jumps ov"} +{"Get":{"state":"Th>e <>quiwn<\n>fox jumps ove <>quiwn<\n>fox jumps ovo<ˇg"}} +{"Key":"c"} +{"Key":"a"} +{"Key":">"} +{"Get":{"state":"Th>e <>quiwn<\n>fox jumps ovo<ˇg","mode":"Normal"}} diff --git a/crates/vim/test_data/test_delete_e.json b/crates/vim/test_data/test_delete_next_word_end.json similarity index 63% rename from crates/vim/test_data/test_delete_e.json rename to crates/vim/test_data/test_delete_next_word_end.json index ebbad8fc4d..85615e4d58 100644 --- a/crates/vim/test_data/test_delete_e.json +++ b/crates/vim/test_data/test_delete_next_word_end.json @@ -1,11 +1,3 @@ -{"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"} diff --git a/crates/vim/test_data/test_delete_surrounding_character_objects.json b/crates/vim/test_data/test_delete_surrounding_character_objects.json index 81b1f563e1..a468b612d9 100644 --- a/crates/vim/test_data/test_delete_surrounding_character_objects.json +++ b/crates/vim/test_data/test_delete_surrounding_character_objects.json @@ -1,3 +1,1023 @@ +{"Put":{"state":"ˇTh'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'ˇ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'ˇe ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'ˇ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e ˇ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'ˇ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e 'ˇ'qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''ˇ'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''ˇqui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''ˇ'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''quˇi'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''ˇ'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ˇck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ˇ'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ck broˇ'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'ˇ'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'ˇfox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'ˇ'er\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox juˇmps ov'er\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'ˇ'er\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ovˇ'er\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'ˇ'er\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'ˇer\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'ˇer\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe ˇlazy d'o'g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'ˇ'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy dˇ'o'g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'ˇ'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'ˇo'g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'ˇ'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'oˇ'g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'ˇ'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'ˇg"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'ˇg","mode":"Normal"}} +{"Put":{"state":"ˇTh'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'ˇ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'ˇe ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'ˇ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e ˇ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'ˇ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e 'ˇ'qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''ˇ'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''ˇqui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''ˇ'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''quˇi'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''ˇ'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ˇck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ˇ'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ck broˇ'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'ˇ'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'ˇfox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'ˇ'er\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox juˇmps ov'er\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'ˇ'er\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ovˇ'er\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'ˇ'er\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'ˇer\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'ˇer\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe ˇlazy d'o'g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'ˇ'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy dˇ'o'g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'ˇ'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'ˇo'g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'ˇ'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'oˇ'g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'ˇ'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'ˇg"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'ˇg","mode":"Normal"}} +{"Put":{"state":"ˇTh'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Thˇ'qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'ˇe ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Thˇ'qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e ˇ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Thˇ'qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e 'ˇ'qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e 'ˇck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''ˇqui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e 'ˇck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''quˇi'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e 'ˇck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ˇck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e ''quiˇwn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ck broˇ'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck brˇo\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'ˇfox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\nˇer\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox juˇmps ov'er\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\nˇer\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ovˇ'er\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\nˇer\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'ˇer\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'ˇer\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe ˇlazy d'o'g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy dˇg","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy dˇ'o'g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy dˇg","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'ˇo'g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy dˇg","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'oˇ'g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy dˇg","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'ˇg"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'ˇg","mode":"Normal"}} +{"Put":{"state":"ˇTh'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Thˇ'qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'ˇe ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Thˇ'qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e ˇ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Thˇ'qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e 'ˇ'qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e 'ˇck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''ˇqui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e 'ˇck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''quˇi'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e 'ˇck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ˇck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e ''quiˇwn'\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ck broˇ'wn'\n'fox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck brˇo\n'fox jumps ov'er\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'ˇfox jumps ov'er\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\nˇer\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox juˇmps ov'er\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\nˇer\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ovˇ'er\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\nˇer\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'ˇer\nthe lazy d'o'g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'ˇer\nthe lazy d'o'g","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe ˇlazy d'o'g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy dˇg","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy dˇ'o'g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy dˇg","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'ˇo'g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy dˇg","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'oˇ'g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy dˇg","mode":"Normal"}} +{"Put":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'ˇg"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"'"} +{"Get":{"state":"Th'e ''qui'ck bro'wn'\n'fox jumps ov'er\nthe lazy d'o'ˇg","mode":"Normal"}} +{"Put":{"state":"ˇTh`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`ˇ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`ˇe ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`ˇ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e ˇ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`ˇ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e `ˇ`qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``ˇ`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``ˇqui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``ˇ`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``quˇi`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``ˇ`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ˇck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ˇ`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ck broˇ`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`ˇ`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`ˇfox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`ˇ`er\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox juˇmps ov`er\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`ˇ`er\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ovˇ`er\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`ˇ`er\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`ˇer\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`ˇer\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe ˇlazy d`o`g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`ˇ`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy dˇ`o`g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`ˇ`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`ˇo`g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`ˇ`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`oˇ`g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`ˇ`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`ˇg"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`ˇg","mode":"Normal"}} +{"Put":{"state":"ˇTh`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`ˇ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`ˇe ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`ˇ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e ˇ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`ˇ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e `ˇ`qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``ˇ`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``ˇqui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``ˇ`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``quˇi`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``ˇ`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ˇck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ˇ`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ck broˇ`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`ˇ`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`ˇfox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`ˇ`er\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox juˇmps ov`er\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`ˇ`er\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ovˇ`er\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`ˇ`er\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`ˇer\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`ˇer\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe ˇlazy d`o`g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`ˇ`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy dˇ`o`g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`ˇ`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`ˇo`g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`ˇ`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`oˇ`g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`ˇ`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`ˇg"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`ˇg","mode":"Normal"}} +{"Put":{"state":"ˇTh`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Thˇ`qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`ˇe ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Thˇ`qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e ˇ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Thˇ`qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e `ˇ`qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e `ˇck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``ˇqui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e `ˇck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``quˇi`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e `ˇck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ˇck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e ``quiˇwn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ck broˇ`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck brˇo\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`ˇfox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\nˇer\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox juˇmps ov`er\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\nˇer\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ovˇ`er\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\nˇer\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`ˇer\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`ˇer\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe ˇlazy d`o`g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy dˇg","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy dˇ`o`g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy dˇg","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`ˇo`g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy dˇg","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`oˇ`g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy dˇg","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`ˇg"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`ˇg","mode":"Normal"}} +{"Put":{"state":"ˇTh`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Thˇ`qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`ˇe ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Thˇ`qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e ˇ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Thˇ`qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e `ˇ`qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e `ˇck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``ˇqui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e `ˇck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``quˇi`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e `ˇck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ˇck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e ``quiˇwn`\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ck broˇ`wn`\n`fox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck brˇo\n`fox jumps ov`er\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`ˇfox jumps ov`er\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\nˇer\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox juˇmps ov`er\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\nˇer\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ovˇ`er\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\nˇer\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`ˇer\nthe lazy d`o`g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`ˇer\nthe lazy d`o`g","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe ˇlazy d`o`g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy dˇg","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy dˇ`o`g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy dˇg","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`ˇo`g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy dˇg","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`oˇ`g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy dˇg","mode":"Normal"}} +{"Put":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`ˇg"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"`"} +{"Get":{"state":"Th`e ``qui`ck bro`wn`\n`fox jumps ov`er\nthe lazy d`o`ˇg","mode":"Normal"}} +{"Put":{"state":"ˇTh\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"ˇe \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"ˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"ˇ\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"ˇqui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"ˇ\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"quˇi\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"ˇ\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ˇ\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ck broˇ\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"ˇ\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇfox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇ\"er\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox juˇmps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇ\"er\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ovˇ\"er\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇ\"er\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"ˇer\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"ˇer\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe ˇlazy d\"o\"g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"ˇ\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy dˇ\"o\"g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"ˇ\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"ˇo\"g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"ˇ\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"oˇ\"g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"ˇ\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"ˇg"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"ˇg","mode":"Normal"}} +{"Put":{"state":"ˇTh\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"ˇe \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"ˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"ˇ\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"ˇqui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"ˇ\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"quˇi\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"ˇ\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ˇ\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ck broˇ\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"ˇ\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇfox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇ\"er\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox juˇmps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇ\"er\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ovˇ\"er\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇ\"er\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"ˇer\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"ˇer\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe ˇlazy d\"o\"g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"ˇ\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy dˇ\"o\"g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"ˇ\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"ˇo\"g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"ˇ\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"oˇ\"g"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"ˇ\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"ˇg"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"ˇg","mode":"Normal"}} +{"Put":{"state":"ˇTh\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Thˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"ˇe \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Thˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Thˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"ˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"ˇqui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"quˇi\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"quiˇwn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ck broˇ\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck brˇo\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇfox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\nˇer\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox juˇmps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\nˇer\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ovˇ\"er\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\nˇer\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"ˇer\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"ˇer\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe ˇlazy d\"o\"g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy dˇg","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy dˇ\"o\"g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy dˇg","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"ˇo\"g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy dˇg","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"oˇ\"g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy dˇg","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"ˇg"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"ˇg","mode":"Normal"}} +{"Put":{"state":"ˇTh\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Thˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"ˇe \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Thˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Thˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"ˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"ˇqui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"quˇi\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"quiˇwn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ck broˇ\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck brˇo\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇfox jumps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\nˇer\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox juˇmps ov\"er\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\nˇer\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ovˇ\"er\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\nˇer\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"ˇer\nthe lazy d\"o\"g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"ˇer\nthe lazy d\"o\"g","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe ˇlazy d\"o\"g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy dˇg","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy dˇ\"o\"g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy dˇg","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"ˇo\"g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy dˇg","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"oˇ\"g"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy dˇg","mode":"Normal"}} +{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"ˇg"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"ˇg","mode":"Normal"}} {"Put":{"state":"ˇTh)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} {"Key":"d"} {"Key":"i"} @@ -1012,3 +2032,341 @@ {"Key":"a"} {"Key":"}"} {"Get":{"state":"Th}e {}qui{ck bro}wn{\n}fox jumps ov{er\nthe lazy d}o{ˇg","mode":"Normal"}} +{"Put":{"state":"ˇTh>e <>quiwn<\n>fox jumps ovoe <>quiwn<\n>fox jumps ovoˇe <>quiwn<\n>fox jumps ovoe <ˇ>quiwn<\n>fox jumps ovoe ˇ<>quiwn<\n>fox jumps ovoe <ˇ>quiwn<\n>fox jumps ovoe <ˇ>quiwn<\n>fox jumps ovoe <>ˇquiwn<\n>fox jumps ovoe <>qui<ˇ>wn<\n>fox jumps ovoe <>quˇiwn<\n>fox jumps ovoe <>qui<ˇ>wn<\n>fox jumps ovoe <>qui<ˇck bro>wn<\n>fox jumps ovoe <>qui<ˇ>wn<\n>fox jumps ovoe <>quiwn<\n>fox jumps ovoe <>qui<ˇ>wn<\n>fox jumps ovoe <>quiwn<\n>ˇfox jumps ovoe <>quiwn<\n>fox jumps ov<ˇ>oe <>quiwn<\n>fox juˇmps ovoe <>quiwn<\n>fox jumps ov<ˇ>oe <>quiwn<\n>fox jumps ovˇoe <>quiwn<\n>fox jumps ov<ˇ>oe <>quiwn<\n>fox jumps ov<ˇer\nthe lazy d>oe <>quiwn<\n>fox jumps ov<ˇ>oe <>quiwn<\n>fox jumps ovoe <>quiwn<\n>fox jumps ov<ˇ>oe <>quiwn<\n>fox jumps ovoe <>quiwn<\n>fox jumps ov<ˇ>oe <>quiwn<\n>fox jumps ovˇoe <>quiwn<\n>fox jumps ovˇoe <>quiwn<\n>fox jumps ove <>quiwn<\n>fox jumps ove <>quiwn<\n>fox jumps ovo<ˇg"}} +{"Key":"d"} +{"Key":"i"} +{"Key":"<"} +{"Get":{"state":"Th>e <>quiwn<\n>fox jumps ovo<ˇg","mode":"Normal"}} +{"Put":{"state":"ˇTh>e <>quiwn<\n>fox jumps ovo"} +{"Get":{"state":"ˇTh>e <>quiwn<\n>fox jumps ovoˇe <>quiwn<\n>fox jumps ovo"} +{"Get":{"state":"Th>e <ˇ>quiwn<\n>fox jumps ovoe ˇ<>quiwn<\n>fox jumps ovo"} +{"Get":{"state":"Th>e <ˇ>quiwn<\n>fox jumps ovo"} +{"Get":{"state":"Th>e <ˇ>quiwn<\n>fox jumps ovoe <>ˇquiwn<\n>fox jumps ovo"} +{"Get":{"state":"Th>e <>qui<ˇ>wn<\n>fox jumps ovoe <>quˇiwn<\n>fox jumps ovo"} +{"Get":{"state":"Th>e <>qui<ˇ>wn<\n>fox jumps ovoe <>qui<ˇck bro>wn<\n>fox jumps ovo"} +{"Get":{"state":"Th>e <>qui<ˇ>wn<\n>fox jumps ovoe <>quiwn<\n>fox jumps ovo"} +{"Get":{"state":"Th>e <>qui<ˇ>wn<\n>fox jumps ovoe <>quiwn<\n>ˇfox jumps ovo"} +{"Get":{"state":"Th>e <>quiwn<\n>fox jumps ov<ˇ>oe <>quiwn<\n>fox juˇmps ovo"} +{"Get":{"state":"Th>e <>quiwn<\n>fox jumps ov<ˇ>oe <>quiwn<\n>fox jumps ovˇo"} +{"Get":{"state":"Th>e <>quiwn<\n>fox jumps ov<ˇ>oe <>quiwn<\n>fox jumps ov<ˇer\nthe lazy d>o"} +{"Get":{"state":"Th>e <>quiwn<\n>fox jumps ov<ˇ>oe <>quiwn<\n>fox jumps ovo"} +{"Get":{"state":"Th>e <>quiwn<\n>fox jumps ov<ˇ>oe <>quiwn<\n>fox jumps ovo"} +{"Get":{"state":"Th>e <>quiwn<\n>fox jumps ov<ˇ>oe <>quiwn<\n>fox jumps ovˇo"} +{"Get":{"state":"Th>e <>quiwn<\n>fox jumps ovˇoe <>quiwn<\n>fox jumps ov"} +{"Get":{"state":"Th>e <>quiwn<\n>fox jumps ove <>quiwn<\n>fox jumps ovo<ˇg"}} +{"Key":"d"} +{"Key":"i"} +{"Key":">"} +{"Get":{"state":"Th>e <>quiwn<\n>fox jumps ovo<ˇg","mode":"Normal"}} +{"Put":{"state":"ˇTh>e <>quiwn<\n>fox jumps ovoe <>quiwn<\n>fox jumps ovoˇe <>quiwn<\n>fox jumps ovoe ˇquiwn<\n>fox jumps ovoe ˇ<>quiwn<\n>fox jumps ovoe ˇquiwn<\n>fox jumps ovoe <ˇ>quiwn<\n>fox jumps ovoe ˇquiwn<\n>fox jumps ovoe <>ˇquiwn<\n>fox jumps ovoe <>quiˇwn<\n>fox jumps ovoe <>quˇiwn<\n>fox jumps ovoe <>quiˇwn<\n>fox jumps ovoe <>qui<ˇck bro>wn<\n>fox jumps ovoe <>quiˇwn<\n>fox jumps ovoe <>quiwn<\n>fox jumps ovoe <>quiˇwn<\n>fox jumps ovoe <>quiwn<\n>ˇfox jumps ovoe <>quiwn<\n>fox jumps ovˇoe <>quiwn<\n>fox juˇmps ovoe <>quiwn<\n>fox jumps ovˇoe <>quiwn<\n>fox jumps ovˇoe <>quiwn<\n>fox jumps ovˇoe <>quiwn<\n>fox jumps ov<ˇer\nthe lazy d>oe <>quiwn<\n>fox jumps ovˇoe <>quiwn<\n>fox jumps ovoe <>quiwn<\n>fox jumps ovˇoe <>quiwn<\n>fox jumps ovoe <>quiwn<\n>fox jumps ovˇoe <>quiwn<\n>fox jumps ovˇoe <>quiwn<\n>fox jumps ovˇoe <>quiwn<\n>fox jumps ove <>quiwn<\n>fox jumps ove <>quiwn<\n>fox jumps ovo<ˇg"}} +{"Key":"d"} +{"Key":"a"} +{"Key":"<"} +{"Get":{"state":"Th>e <>quiwn<\n>fox jumps ovo<ˇg","mode":"Normal"}} +{"Put":{"state":"ˇTh>e <>quiwn<\n>fox jumps ovo"} +{"Get":{"state":"ˇTh>e <>quiwn<\n>fox jumps ovoˇe <>quiwn<\n>fox jumps ovo"} +{"Get":{"state":"Th>e ˇquiwn<\n>fox jumps ovoe ˇ<>quiwn<\n>fox jumps ovo"} +{"Get":{"state":"Th>e ˇquiwn<\n>fox jumps ovoe <ˇ>quiwn<\n>fox jumps ovo"} +{"Get":{"state":"Th>e ˇquiwn<\n>fox jumps ovoe <>ˇquiwn<\n>fox jumps ovo"} +{"Get":{"state":"Th>e <>quiˇwn<\n>fox jumps ovoe <>quˇiwn<\n>fox jumps ovo"} +{"Get":{"state":"Th>e <>quiˇwn<\n>fox jumps ovoe <>qui<ˇck bro>wn<\n>fox jumps ovo"} +{"Get":{"state":"Th>e <>quiˇwn<\n>fox jumps ovoe <>quiwn<\n>fox jumps ovo"} +{"Get":{"state":"Th>e <>quiˇwn<\n>fox jumps ovoe <>quiwn<\n>ˇfox jumps ovo"} +{"Get":{"state":"Th>e <>quiwn<\n>fox jumps ovˇoe <>quiwn<\n>fox juˇmps ovo"} +{"Get":{"state":"Th>e <>quiwn<\n>fox jumps ovˇoe <>quiwn<\n>fox jumps ovˇo"} +{"Get":{"state":"Th>e <>quiwn<\n>fox jumps ovˇoe <>quiwn<\n>fox jumps ov<ˇer\nthe lazy d>o"} +{"Get":{"state":"Th>e <>quiwn<\n>fox jumps ovˇoe <>quiwn<\n>fox jumps ovo"} +{"Get":{"state":"Th>e <>quiwn<\n>fox jumps ovˇoe <>quiwn<\n>fox jumps ovo"} +{"Get":{"state":"Th>e <>quiwn<\n>fox jumps ovˇoe <>quiwn<\n>fox jumps ovˇo"} +{"Get":{"state":"Th>e <>quiwn<\n>fox jumps ovˇoe <>quiwn<\n>fox jumps ov"} +{"Get":{"state":"Th>e <>quiwn<\n>fox jumps ove <>quiwn<\n>fox jumps ovo<ˇg"}} +{"Key":"d"} +{"Key":"a"} +{"Key":">"} +{"Get":{"state":"Th>e <>quiwn<\n>fox jumps ovo<ˇg","mode":"Normal"}} diff --git a/crates/vim/test_data/test_e.json b/crates/vim/test_data/test_e.json deleted file mode 100644 index 06f80dc245..0000000000 --- a/crates/vim/test_data/test_e.json +++ /dev/null @@ -1,32 +0,0 @@ -{"Put":{"state":"Thˇe quick-brown\n\n\nfox_jumps over\nthe"}} -{"Key":"e"} -{"Get":{"state":"The quicˇk-brown\n\n\nfox_jumps over\nthe","mode":"Normal"}} -{"Key":"e"} -{"Get":{"state":"The quickˇ-brown\n\n\nfox_jumps over\nthe","mode":"Normal"}} -{"Key":"e"} -{"Get":{"state":"The quick-browˇn\n\n\nfox_jumps over\nthe","mode":"Normal"}} -{"Key":"e"} -{"Get":{"state":"The quick-brown\n\n\nfox_jumpˇs over\nthe","mode":"Normal"}} -{"Key":"e"} -{"Get":{"state":"The quick-brown\n\n\nfox_jumps oveˇr\nthe","mode":"Normal"}} -{"Key":"e"} -{"Get":{"state":"The quick-brown\n\n\nfox_jumps over\nthˇe","mode":"Normal"}} -{"Key":"e"} -{"Get":{"state":"The quick-brown\n\n\nfox_jumps over\nthˇe","mode":"Normal"}} -{"Put":{"state":"Thˇe quick-brown\n\n\nfox_jumps over\nthe"}} -{"Key":"shift-e"} -{"Get":{"state":"The quick-browˇn\n\n\nfox_jumps over\nthe","mode":"Normal"}} -{"Put":{"state":"The quicˇk-brown\n\n\nfox_jumps over\nthe"}} -{"Key":"shift-e"} -{"Get":{"state":"The quick-browˇn\n\n\nfox_jumps over\nthe","mode":"Normal"}} -{"Put":{"state":"The quickˇ-brown\n\n\nfox_jumps over\nthe"}} -{"Key":"shift-e"} -{"Get":{"state":"The quick-browˇn\n\n\nfox_jumps over\nthe","mode":"Normal"}} -{"Key":"shift-e"} -{"Get":{"state":"The quick-brown\n\n\nfox_jumpˇs over\nthe","mode":"Normal"}} -{"Key":"shift-e"} -{"Get":{"state":"The quick-brown\n\n\nfox_jumps oveˇr\nthe","mode":"Normal"}} -{"Key":"shift-e"} -{"Get":{"state":"The quick-brown\n\n\nfox_jumps over\nthˇe","mode":"Normal"}} -{"Key":"shift-e"} -{"Get":{"state":"The quick-brown\n\n\nfox_jumps over\nthˇe","mode":"Normal"}} diff --git a/crates/vim/test_data/test_multiline_surrounding_character_objects.json b/crates/vim/test_data/test_multiline_surrounding_character_objects.json index cff3ab80e2..973df647a2 100644 --- a/crates/vim/test_data/test_multiline_surrounding_character_objects.json +++ b/crates/vim/test_data/test_multiline_surrounding_character_objects.json @@ -8,3 +8,8 @@ {"Key":"i"} {"Key":"{"} {"Get":{"state":"func empty(a string) bool {\n if a == \"\" {\n« return true\nˇ» }\n return false\n}","mode":"Visual"}} +{"Put":{"state":"func empty(a string) bool {\n if a == \"\" ˇ{\n return true\n }\n return false\n}"}} +{"Key":"v"} +{"Key":"i"} +{"Key":"{"} +{"Get":{"state":"func empty(a string) bool {\n if a == \"\" {\n« return true\nˇ» }\n return false\n}","mode":"Visual"}} diff --git a/crates/vim/test_data/test_singleline_surrounding_character_objects.json b/crates/vim/test_data/test_singleline_surrounding_character_objects.json new file mode 100644 index 0000000000..f7f95ce697 --- /dev/null +++ b/crates/vim/test_data/test_singleline_surrounding_character_objects.json @@ -0,0 +1,27 @@ +{"SetOption":{"value":"wrap"}} +{"SetOption":{"value":"columns=12"}} +{"Put":{"state":"helˇlo \"world\"!"}} +{"Key":"v"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"hello \"«worldˇ»\"!","mode":"Visual"}} +{"Put":{"state":"hello \"wˇorld\"!"}} +{"Key":"v"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"hello \"«worldˇ»\"!","mode":"Visual"}} +{"Put":{"state":"hello \"wˇorld\"!"}} +{"Key":"v"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"hello« \"world\"ˇ»!","mode":"Visual"}} +{"Put":{"state":"hello \"wˇorld\" !"}} +{"Key":"v"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"hello «\"world\" ˇ»!","mode":"Visual"}} +{"Put":{"state":"hello \"wˇorld\"•\ngoodbye"}} +{"Key":"v"} +{"Key":"a"} +{"Key":"\""} +{"Get":{"state":"hello «\"world\" ˇ»\ngoodbye","mode":"Visual"}} diff --git a/crates/vim/test_data/test_visual_paste.json b/crates/vim/test_data/test_visual_paste.json deleted file mode 100644 index a0ad377378..0000000000 --- a/crates/vim/test_data/test_visual_paste.json +++ /dev/null @@ -1,26 +0,0 @@ -{"Put":{"state":"The quick brown\nfox jˇumps over\nthe lazy dog"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"w"} -{"Key":"y"} -{"Get":{"state":"The quick brown\nfox ˇjumps over\nthe lazy dog","mode":"Normal"}} -{"Key":"p"} -{"Get":{"state":"The quick brown\nfox jjumpˇsumps over\nthe lazy dog","mode":"Normal"}} -{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog"}} -{"Key":"shift-v"} -{"Key":"d"} -{"Get":{"state":"The quick brown\nthe laˇzy dog","mode":"Normal"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"w"} -{"Key":"p"} -{"Get":{"state":"The quick brown\nthe \nˇfox jumps over\n dog","mode":"Normal"}} -{"ReadRegister":{"name":"\"","value":"lazy"}} -{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog"}} -{"Key":"shift-v"} -{"Key":"d"} -{"Get":{"state":"The quick brown\nthe laˇzy dog","mode":"Normal"}} -{"Key":"k"} -{"Key":"shift-v"} -{"Key":"p"} -{"Get":{"state":"ˇfox jumps over\nthe lazy dog","mode":"Normal"}} diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index e879b981ef..a19d2c5b58 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -35,9 +35,9 @@ use gpui::{ CursorStyle, ModifiersChangedEvent, MouseButton, PathPromptOptions, Platform, PromptLevel, WindowBounds, WindowOptions, }, - AnyModelHandle, AnyViewHandle, AnyWeakViewHandle, AnyWindowHandle, AppContext, AsyncAppContext, - Entity, ModelContext, ModelHandle, SizeConstraint, Subscription, Task, View, ViewContext, - ViewHandle, WeakViewHandle, WindowContext, WindowHandle, + AnyModelHandle, AnyViewHandle, AnyWeakViewHandle, AppContext, AsyncAppContext, Entity, + ModelContext, ModelHandle, SizeConstraint, Subscription, Task, View, ViewContext, ViewHandle, + WeakViewHandle, WindowContext, WindowHandle, }; use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ProjectItem}; use itertools::Itertools; @@ -4238,6 +4238,10 @@ async fn join_channel_internal( }) .await?; + let Some(room) = room else { + return anyhow::Ok(true); + }; + room.update(cx, |room, _| room.room_update_completed()) .await; @@ -4295,12 +4299,14 @@ pub fn join_channel( } if let Err(err) = result { - let prompt = active_window.unwrap().prompt( - PromptLevel::Critical, - &format!("Failed to join channel: {}", err), - &["Ok"], - &mut cx, - ); + let prompt = active_window.unwrap().update(&mut cx, |_, cx| { + cx.prompt( + PromptLevel::Critical, + &format!("Failed to join channel: {}", err), + &["Ok"], + ) + }); + if let Some(mut prompt) = prompt { prompt.next().await; } else { @@ -4313,17 +4319,39 @@ pub fn join_channel( }) } -pub fn activate_any_workspace_window(cx: &mut AsyncAppContext) -> Option { +pub async fn get_any_active_workspace( + app_state: Arc, + mut cx: AsyncAppContext, +) -> Result> { + // find an existing workspace to focus and show call controls + let active_window = activate_any_workspace_window(&mut cx); + if active_window.is_none() { + cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), None, cx)) + .await; + } + + let Some(active_window) = activate_any_workspace_window(&mut cx) else { + return Err(anyhow!("could not open zed"))?; + }; + + Ok(active_window) +} + +pub fn activate_any_workspace_window(cx: &mut AsyncAppContext) -> Option> { for window in cx.windows() { - let found = window.update(cx, |cx| { - let is_workspace = cx.root_view().clone().downcast::().is_some(); - if is_workspace { - cx.activate_window(); - } - is_workspace - }); - if found == Some(true) { - return Some(window); + if let Some(workspace) = window + .update(cx, |cx| { + cx.root_view() + .clone() + .downcast::() + .map(|workspace| { + cx.activate_window(); + workspace + }) + }) + .flatten() + { + return Some(workspace); } } None diff --git a/crates/zed/examples/semantic_index_eval.rs b/crates/zed/examples/semantic_index_eval.rs index f16114e906..caf8e5f5c7 100644 --- a/crates/zed/examples/semantic_index_eval.rs +++ b/crates/zed/examples/semantic_index_eval.rs @@ -55,7 +55,7 @@ fn parse_eval() -> anyhow::Result> { .as_path() .parent() .unwrap() - .join("crates/semantic_index/eval"); + .join("zed/crates/semantic_index/eval"); let mut repo_evals: Vec = Vec::new(); for entry in fs::read_dir(eval_folder)? { diff --git a/crates/zed/src/languages/bash/highlights.scm b/crates/zed/src/languages/bash/highlights.scm index a72c5468ed..5cb5dad6a0 100644 --- a/crates/zed/src/languages/bash/highlights.scm +++ b/crates/zed/src/languages/bash/highlights.scm @@ -3,6 +3,7 @@ (raw_string) (heredoc_body) (heredoc_start) + (ansi_c_string) ] @string (command_name) @function diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index a590db098a..5add524414 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -7,6 +7,7 @@ use cli::FORCE_CLI_MODE_ENV_VAR_NAME; use client::{ self, Client, TelemetrySettings, UserStore, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN, }; +use collab_ui::channel_view::ChannelView; use db::kvp::KEY_VALUE_STORE; use editor::Editor; use futures::StreamExt; @@ -240,6 +241,20 @@ fn main() { }) .detach_and_log_err(cx) } + Ok(Some(OpenRequest::OpenChannelNotes { channel_id })) => { + triggered_authentication = true; + let app_state = app_state.clone(); + let client = client.clone(); + cx.spawn(|mut cx| async move { + // ignore errors here, we'll show a generic "not signed in" + let _ = authenticate(client, &cx).await; + let workspace = + workspace::get_any_active_workspace(app_state, cx.clone()).await?; + cx.update(|cx| ChannelView::open(channel_id, workspace, cx)) + .await + }) + .detach_and_log_err(cx) + } Ok(None) | Err(_) => cx .spawn({ let app_state = app_state.clone(); @@ -254,8 +269,10 @@ fn main() { while let Some(request) = open_rx.next().await { match request { OpenRequest::Paths { paths } => { - cx.update(|cx| workspace::open_paths(&paths, &app_state, None, cx)) - .detach(); + cx.update(|cx| { + workspace::open_paths(&paths, &app_state.clone(), None, cx) + }) + .detach(); } OpenRequest::CliConnection { connection } => { cx.spawn(|cx| handle_cli_connection(connection, app_state.clone(), cx)) @@ -266,6 +283,16 @@ fn main() { workspace::join_channel(channel_id, app_state.clone(), None, cx) }) .detach(), + OpenRequest::OpenChannelNotes { channel_id } => { + let app_state = app_state.clone(); + if let Ok(workspace) = + workspace::get_any_active_workspace(app_state, cx.clone()).await + { + cx.update(|cx| { + ChannelView::open(channel_id, workspace, cx).detach(); + }) + } + } } } } diff --git a/crates/zed/src/open_listener.rs b/crates/zed/src/open_listener.rs index 578d8cd69f..e0b360d0d7 100644 --- a/crates/zed/src/open_listener.rs +++ b/crates/zed/src/open_listener.rs @@ -32,6 +32,9 @@ pub enum OpenRequest { JoinChannel { channel_id: u64, }, + OpenChannelNotes { + channel_id: u64, + }, } pub struct OpenListener { @@ -85,7 +88,11 @@ impl OpenListener { if let Some(slug) = parts.next() { if let Some(id_str) = slug.split("-").last() { if let Ok(channel_id) = id_str.parse::() { - return Some(OpenRequest::JoinChannel { channel_id }); + if Some("notes") == parts.next() { + return Some(OpenRequest::OpenChannelNotes { channel_id }); + } else { + return Some(OpenRequest::JoinChannel { channel_id }); + } } } } diff --git a/script/evaluate_semantic_index b/script/evaluate_semantic_index index 8dcb53c399..9ecfe898c5 100755 --- a/script/evaluate_semantic_index +++ b/script/evaluate_semantic_index @@ -1,3 +1,3 @@ #!/bin/bash -RUST_LOG=semantic_index=trace cargo run -p semantic_index --example eval --release +RUST_LOG=semantic_index=trace cargo run --example semantic_index_eval --release diff --git a/styles/src/style_tree/collab_panel.ts b/styles/src/style_tree/collab_panel.ts index 2a7702842a..272b6055ed 100644 --- a/styles/src/style_tree/collab_panel.ts +++ b/styles/src/style_tree/collab_panel.ts @@ -210,6 +210,14 @@ export default function contacts_panel(): any { right: SPACING, }, }, + dragged_over_header: { + margin: { top: SPACING }, + padding: { + left: SPACING, + right: SPACING, + }, + background: background(layer, "hovered"), + }, subheader_row, leave_call: interactive({ base: { @@ -279,7 +287,7 @@ export default function contacts_panel(): any { margin: { left: CHANNEL_SPACING, }, - } + }, }, list_empty_label_container: { margin: { diff --git a/styles/src/style_tree/notification_panel.ts b/styles/src/style_tree/notification_panel.ts index 3b6a87946a..ecafc3c8e6 100644 --- a/styles/src/style_tree/notification_panel.ts +++ b/styles/src/style_tree/notification_panel.ts @@ -1,12 +1,22 @@ import { background, border, text } from "./components" import { icon_button } from "../component/icon_button" -import { useTheme } from "../theme" -import { interactive } from "../element" +import { useTheme, with_opacity } from "../theme" +import { text_button } from "../component" export default function (): any { const theme = useTheme() const layer = theme.middle + const notification_text = { + padding: { top: 4, bottom: 4 }, + ...text(layer, "sans", "base"), + } + + const notification_read_text_color = with_opacity( + theme.middle.base.default.foreground, + 0.6 + ) + return { background: background(layer), avatar: { @@ -31,34 +41,19 @@ export default function (): any { }, }, read_text: { - padding: { top: 4, bottom: 4 }, - ...text(layer, "sans", "disabled"), + ...notification_text, + color: notification_read_text_color, }, - unread_text: { - padding: { top: 4, bottom: 4 }, - ...text(layer, "sans", "base"), - }, - button: interactive({ - base: { - ...text(theme.lowest, "sans", "on", { size: "xs" }), - background: background(theme.lowest, "on"), - padding: 4, - corner_radius: 6, - margin: { left: 6 }, - }, - - state: { - hovered: { - background: background(theme.lowest, "on", "hovered"), - }, - }, + unread_text: notification_text, + button: text_button({ + variant: "ghost", }), timestamp: text(layer, "sans", "base", "disabled"), avatar_container: { padding: { - right: 6, + right: 8, left: 2, - top: 2, + top: 4, bottom: 2, }, }, diff --git a/styles/src/style_tree/search.ts b/styles/src/style_tree/search.ts index b0ac023c09..2317108bde 100644 --- a/styles/src/style_tree/search.ts +++ b/styles/src/style_tree/search.ts @@ -2,7 +2,6 @@ import { with_opacity } from "../theme/color" import { background, border, foreground, text } from "./components" import { interactive, toggleable } from "../element" import { useTheme } from "../theme" -import { text_button } from "../component/text_button" const search_results = () => { const theme = useTheme() @@ -36,7 +35,7 @@ export default function search(): any { left: 10, right: 4, }, - margin: { right: SEARCH_ROW_SPACING } + margin: { right: SEARCH_ROW_SPACING }, } const include_exclude_editor = { @@ -378,7 +377,7 @@ export default function search(): any { modes_container: { padding: { right: SEARCH_ROW_SPACING, - } + }, }, replace_icon: { icon: {