Detailed changes
@@ -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",
@@ -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" }
@@ -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"]
}
}
]
@@ -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"]
}
},
{
@@ -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.
@@ -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<dyn LanguageModel>;
- fn is_authenticated(&self) -> bool;
- async fn embed_batch(&self, spans: Vec<String>) -> Result<Vec<Embedding>>;
+ fn retrieve_credentials(&self, cx: &AppContext) -> Option<String>;
+ async fn embed_batch(
+ &self,
+ spans: Vec<String>,
+ api_key: Option<String>,
+ ) -> Result<Vec<Embedding>>;
fn max_tokens_per_batch(&self) -> usize;
fn rate_limit_expiration(&self) -> Option<Instant>;
}
@@ -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<String> {
+ Some("Dummy Credentials".to_string())
+ }
fn base_model(&self) -> Box<dyn LanguageModel> {
Box::new(DummyLanguageModel {})
}
- fn is_authenticated(&self) -> bool {
- true
- }
fn rate_limit_expiration(&self) -> Option<Instant> {
None
}
- async fn embed_batch(&self, spans: Vec<String>) -> anyhow::Result<Vec<Embedding>> {
+ async fn embed_batch(
+ &self,
+ spans: Vec<String>,
+ api_key: Option<String>,
+ ) -> anyhow::Result<Vec<Embedding>> {
// 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]);
@@ -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<String> = 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<String> {
+ 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<dyn LanguageModel> {
let model: Box<dyn LanguageModel> = 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<String>) -> Result<Vec<Embedding>> {
+ async fn embed_batch(
+ &self,
+ spans: Vec<String>,
+ api_key: Option<String>,
+ ) -> Result<Vec<Embedding>> {
const BACKOFF_SECONDS: [usize; 4] = [3, 5, 15, 45];
const MAX_RETRIES: usize = 4;
@@ -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<proto::ParticipantProject>,
}
+pub struct OneAtATime {
+ cancel: Option<oneshot::Sender<()>>,
+}
+
+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<F, Fut, R>(&mut self, cx: &mut AppContext, f: F) -> Task<Result<Option<R>>>
+ where
+ F: 'static + FnOnce(AsyncAppContext) -> Fut,
+ Fut: Future<Output = Result<R>>,
+ 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<Room>, Vec<Subscription>)>,
pending_room_creation: Option<Shared<Task<Result<ModelHandle<Room>, Arc<anyhow::Error>>>>>,
+ _join_debouncer: OneAtATime,
location: Option<WeakModelHandle<Project>>,
pending_invites: HashSet<u64>,
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<Self>,
- ) -> Task<Result<ModelHandle<Room>>> {
+ ) -> Task<Result<Option<ModelHandle<Room>>>> {
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);
+ }
+}
@@ -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<u64>,
+ pub channel_id: Option<u64>,
live_kit: Option<LiveKitRoom>,
status: RoomStatus,
shared_projects: HashSet<WeakModelHandle<Project>>,
@@ -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<u64>,
@@ -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<Client>,
user_store: ModelHandle<UserStore>,
- cx: &mut AppContext,
- ) -> Task<Result<ModelHandle<Self>>> {
- cx.spawn(|cx| async move {
- Self::from_join_response(
- client.request(proto::JoinChannel { channel_id }).await?,
- client,
- user_store,
- cx,
- )
- })
+ cx: AsyncAppContext,
+ ) -> Result<ModelHandle<Self>> {
+ 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<Client>,
user_store: ModelHandle<UserStore>,
- cx: &mut AppContext,
- ) -> Task<Result<ModelHandle<Self>>> {
- 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<ModelHandle<Self>> {
+ 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],
}
@@ -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;
@@ -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<Client>) {
}
pub struct ChannelBuffer {
- pub(crate) channel: Arc<Channel>,
+ pub channel_id: ChannelId,
connected: bool,
collaborators: HashMap<PeerId, Collaborator>,
user_store: ModelHandle<UserStore>,
+ channel_store: ModelHandle<ChannelStore>,
buffer: ModelHandle<language::Buffer>,
buffer_epoch: u64,
client: Arc<Client>,
@@ -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<Channel>,
client: Arc<Client>,
user_store: ModelHandle<UserStore>,
+ channel_store: ModelHandle<ChannelStore>,
mut cx: AsyncAppContext,
) -> Result<ModelHandle<Self>> {
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<Channel> {
- self.channel.clone()
+ pub fn channel(&self, cx: &AppContext) -> Option<Arc<Channel>> {
+ self.channel_store
+ .read(cx)
+ .channel_for_id(self.channel_id)
+ .cloned()
}
pub(crate) fn disconnect(&mut self, cx: &mut ModelContext<Self>) {
- 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<Self>) {
+ cx.emit(ChannelBufferEvent::ChannelChanged);
+ cx.notify()
+ }
+
pub fn is_connected(&self) -> bool {
self.connected
}
@@ -19,7 +19,7 @@ use time::OffsetDateTime;
use util::{post_inc, ResultExt as _, TryFutureExt};
pub struct ChannelChat {
- channel: Arc<Channel>,
+ pub channel_id: ChannelId,
messages: SumTree<ChannelMessage>,
acknowledged_message_ids: HashSet<u64>,
channel_store: ModelHandle<ChannelStore>,
@@ -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<Channel> {
- &self.channel
+ pub fn channel(&self, cx: &AppContext) -> Option<Arc<Channel>> {
+ self.channel_store
+ .read(cx)
+ .channel_for_id(self.channel_id)
+ .cloned()
}
pub fn client(&self) -> &Arc<Client> {
@@ -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<Self>) -> Task<Result<()>> {
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<Self>) {
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,
})
});
@@ -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<Client>, user_store: ModelHandle<UserStore>, 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<Arc<Channel>>,
channel_participants: HashMap<ChannelId, Vec<Arc<User>>>,
- channels_with_admin_privileges: HashSet<ChannelId>,
outgoing_invites: HashSet<(ChannelId, UserId)>,
update_channels_tx: mpsc::UnboundedSender<proto::UpdateChannels>,
opened_buffers: HashMap<ChannelId, OpenedModelHandle<ChannelBuffer>>,
@@ -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<u64>,
+ pub parent_path: Vec<u64>,
}
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<User>,
@@ -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<Item = (usize, &Arc<Channel>)> {
- 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<Item = (usize, &Arc<Channel>)> {
+ 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<Channel>, &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<Channel>> {
+ 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<Channel>> {
@@ -269,10 +256,11 @@ impl ChannelStore {
) -> Task<Result<ModelHandle<ChannelBuffer>>> {
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<User>] {
@@ -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<Self>,
- ) -> Task<Result<()>> {
- 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<Self>,
- ) -> Task<Result<()>> {
- 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<ChannelId>,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
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<Self>) -> Task<Result<()>> {
+ 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>) {
- 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<ChannelId> {
- 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<ChannelPath> 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([]))
- }
-}
@@ -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<ChannelPath>,
+ channels_ordered: Vec<ChannelId>,
channels_by_id: BTreeMap<ChannelId, Arc<Channel>>,
}
@@ -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<ChannelPath>,
+ channels_ordered: &'a mut Vec<ChannelId>,
channels_by_id: &'a mut BTreeMap<ChannelId, Arc<Channel>>,
}
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<ChannelId, Arc<Channel>>,
-) -> impl 'a + Iterator<Item = Option<&'a str>> {
- path.iter()
- .map(|id| Some(channels_by_id.get(id)?.name.as_str()))
+) -> impl Iterator<Item = &str> {
+ 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(
@@ -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::<proto::GetUsers>().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::<proto::JoinChannelChat>().await.unwrap();
@@ -371,19 +367,13 @@ fn update_channels(
#[track_caller]
fn assert_channels(
channel_store: &ModelHandle<ChannelStore>,
- 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::<Vec<_>>()
});
assert_eq!(actual, expected_channels);
@@ -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,
@@ -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");
@@ -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<String>,
}
-#[derive(FromQueryResult, Debug, PartialEq, Eq, Hash)]
+#[derive(Debug)]
+pub struct MoveChannelResult {
+ pub participants_to_update: HashMap<UserId, ChannelsForUser>,
+ pub participants_to_remove: HashSet<UserId>,
+ pub moved_channels: HashSet<ChannelId>,
+}
+
+#[derive(Debug)]
+pub struct RenameChannelResult {
+ pub channel: Channel,
+ pub participants_to_update: HashMap<UserId, Channel>,
+}
+
+#[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<UserId, ChannelsForUser>,
+ pub participants_to_remove: HashSet<UserId>,
+ pub channels_to_remove: Vec<ChannelId>,
+}
+
+#[derive(Debug)]
+pub struct MembershipUpdated {
+ pub channel_id: ChannelId,
+ pub new_channels: ChannelsForUser,
+ pub removed_channels: Vec<ChannelId>,
+}
+
+#[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<MembershipUpdated>,
+ pub notifications: NotificationBatch,
+}
+
+#[derive(Debug)]
+pub struct RemoveChannelMemberResult {
+ pub membership_update: MembershipUpdated,
+ pub notification_id: Option<NotificationId>,
+}
+
+#[derive(Debug, PartialEq, Eq, Hash)]
pub struct Channel {
pub id: ChannelId,
pub name: String,
pub visibility: ChannelVisibility,
+ pub role: ChannelRole,
+ pub parent_path: Vec<ChannelId>,
+}
+
+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<Channel>,
pub channel_participants: HashMap<ChannelId, Vec<UserId>>,
- pub channels_with_admin_privileges: HashSet<ChannelId>,
pub unseen_buffer_changes: Vec<proto::UnseenChannelBufferChange>,
pub channel_messages: Vec<proto::UnseenChannelMessage>,
}
@@ -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<proto::ChannelRole> for ChannelRole {
@@ -16,7 +16,8 @@ impl Database {
connection: ConnectionId,
) -> Result<proto::JoinChannelBufferResponse> {
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<proto::VectorClockEntry>,
)> {
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?;
@@ -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<ChannelId> {
- self.create_channel(name, None, creator_id).await
+ Ok(self
+ .create_channel(name, None, creator_id)
+ .await?
+ .channel
+ .id)
}
- pub async fn create_channel(
+ #[cfg(test)]
+ pub async fn create_sub_channel(
&self,
name: &str,
- parent: Option<ChannelId>,
+ parent: ChannelId,
creator_id: UserId,
) -> Result<ChannelId> {
+ Ok(self
+ .create_channel(name, Some(parent), creator_id)
+ .await?
+ .channel
+ .id)
+ }
+
+ pub async fn create_channel(
+ &self,
+ name: &str,
+ parent_channel_id: Option<ChannelId>,
+ admin_id: UserId,
+ ) -> Result<CreateChannelResult> {
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<ChannelId>)> {
+ ) -> Result<(JoinRoom, Option<MembershipUpdated>, 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 role = self
- .channel_role_for_user(channel_id, user_id, &*tx)
- .await?;
+ let mut accept_invite_result = None;
- 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<channel::Model> {
+ admin_id: UserId,
+ ) -> Result<SetChannelVisibilityResult> {
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()
+ let previous_members = self
+ .get_channel_participant_details_internal(&channel, &*tx)
+ .await?;
+
+ let mut model = channel.into_active_model();
+ model.visibility = ActiveValue::Set(visibility);
+ let channel = model.update(&*tx).await?;
+
+ let mut participants_to_update: HashMap<UserId, ChannelsForUser> = self
+ .participants_to_notify_for_channel_change(&channel, &*tx)
+ .await?
+ .into_iter()
+ .collect();
+
+ let mut channels_to_remove: Vec<ChannelId> = vec![];
+ let mut participants_to_remove: HashSet<UserId> = HashSet::default();
+ match visibility {
+ ChannelVisibility::Members => {
+ let all_descendents: Vec<ChannelId> = 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);
+ }
+ }
+ }
}
- .update(&*tx)
- .await?;
- Ok(channel)
+ Ok(SetChannelVisibilityResult {
+ participants_to_update,
+ participants_to_remove,
+ channels_to_remove,
+ })
})
.await
}
@@ -202,39 +263,12 @@ impl Database {
user_id: UserId,
) -> Result<(Vec<ChannelId>, Vec<UserId>)> {
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<ChannelId> = 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<UserId> = 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::<Vec<_>>();
+
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<NotificationBatch> {
+ ) -> Result<InviteMemberResult> {
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<Channel> {
+ ) -> Result<RenameChannelResult> {
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<NotificationBatch> {
+ ) -> Result<RespondToChannelInvite> {
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"))?;
+ }
- if rows_affected == 0 {
- Err(anyhow!("no such invitation"))?;
- }
+ None
+ };
- 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<MembershipUpdated> {
+ 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::<Vec<_>>();
+
+ 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<Option<NotificationId>> {
+ ) -> Result<RemoveChannelMemberResult> {
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<Vec<Channel>> {
self.transaction(|tx| async move {
+ let mut role_for_channel: HashMap<ChannelId, ChannelRole> = 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<ChannelsForUser> {
- 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<channel_member::Model>,
+ ancestor_channel: Option<&channel::Model>,
tx: &DatabaseTransaction,
) -> Result<ChannelsForUser> {
- 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<ChannelId, ChannelRole> = 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<ChannelId, ChannelRole> = HashMap::default();
for membership in channel_memberships.iter() {
- role_for_channel.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);
+ roles_by_channel_id.insert(membership.channel_id, membership.role);
}
- let mut channels: Vec<Channel> = Vec::new();
- let mut channels_with_admin_privileges: HashSet<ChannelId> = HashSet::default();
- let mut channels_to_remove: HashSet<u64> = HashSet::default();
-
- let mut rows = channel::Entity::find()
- .filter(channel::Column::Id.is_in(role_for_channel.keys().copied()))
- .stream(&*tx)
- .await?;
+ let mut visible_channel_ids: HashSet<ChannelId> = HashSet::default();
- while let Some(row) = rows.next().await {
- let channel = row?;
- let role = role_for_channel[&channel.id];
+ let channels: Vec<Channel> = descendants
+ .into_iter()
+ .filter_map(|channel| {
+ let parent_role = channel
+ .parent_id()
+ .and_then(|parent_id| roles_by_channel_id.get(&parent_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);
+ 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 {
+ *parent_role
+ };
+ roles_by_channel_id.insert(channel.id, role);
+ role
+ } else {
+ *roles_by_channel_id.get(&channel.id)?
+ };
- 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<u64, u64> = 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 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;
}
- }
- let mut new_edges: Vec<ChannelEdge> = 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;
- } else {
- continue 'outer;
+ 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<Vec<UserId>> {
- 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<Vec<(UserId, ChannelsForUser)>> {
+ 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<channel_member::Model> {
+ ) -> Result<SetMemberRoleResult> {
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()
@@ -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<MessageId>,
) -> Result<Vec<proto::ChannelMessage>> {
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::<ChannelId>::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::<ChannelId, channel::Model>::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<Item = Result<channel_message::Model, sea_orm::DbErr>>,
+ rows: Vec<channel_message::Model>,
tx: &DatabaseTransaction,
) -> Result<Vec<proto::ChannelMessage>> {
- 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::<Vec<_>>();
messages.reverse();
let mut mentions = channel_message_mention::Entity::find()
@@ -203,6 +200,10 @@ impl Database {
nonce: u128,
) -> Result<CreatedChannelMessage> {
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()
{
@@ -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<ChannelId>, proto::Room)> {
+ ) -> Result<(Option<channel::Model>, 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,
@@ -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;
@@ -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<ChannelId> {
+ self.ancestors().last()
+ }
+
+ pub fn ancestors(&self) -> impl Iterator<Item = ChannelId> + '_ {
+ 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<Item = ChannelId> + '_ {
+ self.ancestors().chain(Some(self.id))
+ }
+
+ pub fn path(&self) -> String {
+ format!("{}{}/", self.parent_path, self.id)
+ }
}
impl ActiveModelBehavior for ActiveModel {}
@@ -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 {}
@@ -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<Channel> {
+ 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<Database>, 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,
+ }
+}
@@ -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<Database>) {
- 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<Database>) {
.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<Database>) {
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<Database>) {
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<Database>) {
.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<Database>) {
&[
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<Database>) {
.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<Database>) {
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<Database>) {
// /- 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<Database>) {
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<Database>) {
// 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());
+ .is_none());
- // Stranding a channel should retain it's sub channels
- db.unlink_channel(user_id, projects_id, zed_id)
- .await
- .unwrap();
+ let result = db.get_channels_for_user(user_id).await.unwrap();
+ assert_channel_tree(
+ result.channels,
+ &[
+ (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_dag(
+ assert_channel_tree(
result.channels,
&[
- (zed_id, None),
- (projects_id, None),
- (livestreaming_id, Some(projects_id)),
+ (zed_id, &[]),
+ (projects_id, &[]),
+ (livestreaming_id, &[projects_id]),
],
);
}
@@ -811,44 +464,52 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
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<Database>) {
&[
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<Database>) {
]
);
- 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<Database>) {
&[
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<Database>) {
]
);
- 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<Database>) {
// 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<Database>) {
&[
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<Database>) {
.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<Database>) {
&[
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<Database>) {
);
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<Database>) {
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<Database>) {
.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<Database>) {
+ 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!(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_eq!(most_public, Some(zed_channel))
+ assert!(db
+ .join_channel_chat(zed_channel, guest_connection, guest)
+ .await
+ .is_ok())
}
#[track_caller]
-fn assert_dag(actual: ChannelGraph, expected: &[(ChannelId, Option<ChannelId>)]) {
- let mut actual_map: HashMap<ChannelId, HashSet<ChannelId>> = 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<ChannelId, HashSet<ChannelId>> = 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<Channel>, expected: &[(ChannelId, &[ChannelId])]) {
+ let actual = actual
+ .iter()
+ .map(|channel| (channel.id, channel.parent_path.as_slice()))
+ .collect::<Vec<_>>();
+ pretty_assertions::assert_eq!(
+ actual,
+ expected.to_vec(),
+ "wrong channel ids and parent paths"
+ );
}
@@ -15,18 +15,22 @@ test_both_dbs!(
async fn test_channel_message_retrieval(db: &Arc<Database>) {
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<Database>) {
}
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<Database>) {
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<Database>) {
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<Database>) {
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<Database>) {
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();
@@ -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 mut update = proto::UpdateChannels::default();
- update.channel_invitations.push(proto::Channel {
- id: channel.id.to_proto(),
- visibility: channel.visibility.into(),
- name: channel.name,
- });
+ let update = proto::UpdateChannels {
+ channel_invitations: vec![channel.to_proto()],
+ ..Default::default()
+ };
- 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?;
-
- 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,
- });
- }
+ 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()
+ };
- 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,98 +2444,29 @@ 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) {
- session.peer.send(connection_id, update.clone())?;
- }
- }
-
- Ok(())
-}
-
-async fn link_channel(
- request: proto::LinkChannel,
- response: Response<proto::LinkChannel>,
- 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<proto::UnlinkChannel>,
- 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?;
+ 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()
+ };
- 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(())
}
@@ -2544,58 +2475,50 @@ async fn move_channel(
response: Response<proto::MoveChannel>,
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(());
- }
+ notify_channel_moved(result, session).await?;
- let members_from = db.get_channel_members(from_parent).await?;
- let members_to = db.get_channel_members(to).await?;
+ response.send(Ack {})?;
+ Ok(())
+}
- let update = proto::UpdateChannels {
- delete_edge: vec![proto::ChannelEdge {
- channel_id: channel_id.to_proto(),
- parent_id: from_parent.to_proto(),
- }],
- ..Default::default()
+async fn notify_channel_moved(result: Option<MoveChannelResult>, session: Session) -> Result<()> {
+ let Some(MoveChannelResult {
+ participants_to_remove,
+ participants_to_update,
+ moved_channels,
+ }) = result
+ else {
+ return Ok(());
};
+ let moved_channels: Vec<u64> = moved_channels.iter().map(|id| id.to_proto()).collect();
+
let connection_pool = session.connection_pool().await;
- for member_id in members_from {
- for connection_id in connection_pool.user_connection_ids(member_id) {
+ 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())?;
}
}
- 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) {
+ 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())?;
}
}
-
- response.send(Ack {})?;
-
Ok(())
}
@@ -2620,78 +2543,36 @@ 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()
+ };
- send_notifications(
- &*session.connection_pool().await,
- &session.peer,
- notifications,
- );
- response.send(proto::Ack {})?;
+ for connection_id in connection_pool.user_connection_ids(session.user_id) {
+ session.peer.send(connection_id, update.clone())?;
+ }
+ };
- Ok(())
-}
+ send_notifications(&*connection_pool, &session.peer, notifications);
+
+ response.send(proto::Ack {})?;
-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(())
}
@@ -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<db::Channel>,
) -> 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
@@ -40,3 +40,7 @@ fn room_participants(room: &ModelHandle<Room>, cx: &mut TestAppContext) -> RoomP
RoomParticipants { remote, pending }
})
}
+
+fn channel_id(room: &ModelHandle<Room>, cx: &mut TestAppContext) -> Option<u64> {
+ cx.read(|cx| room.read(cx).channel_id())
+}
@@ -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<Deterministic>,
@@ -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::<usize>(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");
});
}
@@ -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::<Vec<_>>()
- .is_empty())
+ assert!(channels.ordered_channels().collect::<Vec<_>>().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<Deterministic>,
+ 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<Deterministic>,
+ 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<Deterministic>,
@@ -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<Deterministic>,
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::<Vec<_>>()
});
@@ -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::<Vec<_>>()
});
@@ -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::<Vec<_>>()
});
@@ -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<Deterministic>,
+ 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<Deterministic>,
@@ -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)))
@@ -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);
@@ -611,38 +611,6 @@ impl TestClient {
) -> WindowHandle<Workspace> {
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 {
@@ -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"] }
@@ -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<Channel> {
- self.channel_buffer.read(cx).channel()
+ pub fn channel(&self, cx: &AppContext) -> Option<Arc<Channel>> {
+ 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<V> {
- 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<Vector2F> {
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)
{
@@ -263,21 +263,22 @@ impl ChatPanel {
fn set_active_chat(&mut self, chat: ModelHandle<ChannelChat>, cx: &mut ViewContext<Self>) {
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<Self>) {
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<Self>) {
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);
@@ -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<String, UserId>,
mentions: Vec<UserId>,
mentions_task: Option<Task<()>>,
- channel: Option<Arc<Channel>>,
+ channel_id: Option<ChannelId>,
}
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<Channel>, cx: &mut ViewContext<Self>) {
+ pub fn set_channel(
+ &mut self,
+ channel_id: u64,
+ channel_name: Option<String>,
+ cx: &mut ViewContext<Self>,
+ ) {
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<Self>) {
- 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?;
@@ -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<ChannelId>,
-}
-
-#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
-struct StartLinkChannelFor {
- channel_id: ChannelId,
- parent_id: Option<ChannelId>,
-}
-
-#[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<ChannelId>);
-
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<ChannelId>,
- 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<CollabPanel>| {
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<CollabPanel>| {
- 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<CollabPanel>| {
- 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<CollabPanel>| {
- 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,
- })
- }
- },
- );
-
- cx.add_action(
- |panel: &mut CollabPanel, _: &MoveOrLinkToSelected, cx: &mut ViewContext<CollabPanel>| {
- 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)
- }),
- }
- }
- },
- );
+ |panel: &mut CollabPanel, _: &MoveSelected, cx: &mut ViewContext<CollabPanel>| {
+ let Some(clipboard) = panel.channel_clipboard.take() else {
+ return;
+ };
+ let Some(selected_channel) = panel.selected_channel() else {
+ return;
+ };
- cx.add_action(
- |panel: &mut CollabPanel, action: &LinkChannel, cx: &mut ViewContext<CollabPanel>| {
- 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<CollabPanel>| {
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<CollabPanel>| {
- 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<ChannelPath>,
+ location: Option<ChannelId>,
pending_name: Option<String>,
},
Rename {
- location: ChannelPath,
+ location: ChannelId,
pending_name: Option<String>,
},
}
@@ -386,16 +286,23 @@ pub struct CollabPanel {
list_state: ListState<Self>,
subscriptions: Vec<Subscription>,
collapsed_sections: Vec<Section>,
- collapsed_channels: Vec<ChannelPath>,
- drag_target_channel: Option<ChannelData>,
+ collapsed_channels: Vec<ChannelId>,
+ drag_target_channel: ChannelDragTarget,
workspace: WeakViewHandle<Workspace>,
context_menu_on_selected: bool,
}
+#[derive(PartialEq, Eq)]
+enum ChannelDragTarget {
+ None,
+ Root,
+ Channel(ChannelId),
+}
+
#[derive(Serialize, Deserialize)]
struct SerializedCollabPanel {
width: Option<f32>,
- collapsed_channels: Option<Vec<ChannelPath>>,
+ collapsed_channels: Option<Vec<ChannelId>>,
}
#[derive(Debug)]
@@ -440,7 +347,7 @@ enum ListEntry {
Channel {
channel: Arc<Channel>,
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::<AddChannel, _>(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::<AddChannel>(
- 0,
- "Create a channel",
- None,
- tooltip_style.clone(),
- cx,
- ),
- ),
+ Section::Channels => {
+ if cx
+ .global::<DragAndDrop<Workspace>>()
+ .currently_dragged::<Channel>(cx.window())
+ .is_some()
+ && self.drag_target_channel == ChannelDragTarget::Root
+ {
+ is_dragged_over = true;
+ }
+
+ Some(
+ MouseEventHandler::new::<AddChannel, _>(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::<AddChannel>(
+ 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::<DragAndDrop<Workspace>>()
+ .currently_dragged::<Channel>(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::<DragAndDrop<Workspace>>()
+ .currently_dragged::<Channel>(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<Self>,
) -> AnyElement<Self> {
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::<DragAndDrop<Workspace>>()
- .currently_dragged::<DraggedChannel>(cx.window())
+ .currently_dragged::<Channel>(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::<DragAndDrop<Workspace>>()
- .currently_dragged::<DraggedChannel>(cx.window())
+ .currently_dragged::<Channel>(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::<DragAndDrop<Workspace>>()
- .currently_dragged::<DraggedChannel>(cx.window())
+ if let Some((_, dragged_channel)) = cx
+ .global::<DragAndDrop<Workspace>>()
+ .currently_dragged::<Channel>(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<Workspace>| {
+ .as_draggable::<_, Channel>(
+ channel.clone(),
+ move |_, channel, cx: &mut ViewContext<Workspace>| {
let theme = &theme::current(cx).collab_panel;
Flex::<Workspace>::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<Vector2F>,
- path: &ChannelPath,
+ channel: &Channel,
ix: usize,
cx: &mut ViewContext<Self>,
) {
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<Self>,
) {
- 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<Self>) {
- 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>,
) {
- self.toggle_channel_collapsed(&action.location, cx);
+ self.toggle_channel_collapsed(action.location, cx);
}
- fn toggle_channel_collapsed<'a>(
- &mut self,
- path: impl Into<Cow<'a, ChannelPath>>,
- cx: &mut ViewContext<Self>,
- ) {
- let path = path.into();
- match self.collapsed_channels.binary_search(&path) {
+ fn toggle_channel_collapsed<'a>(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
+ 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<Self>) {
@@ -3142,16 +2998,16 @@ impl CollabPanel {
}
fn remove(&mut self, _: &Remove, cx: &mut ViewContext<Self>) {
- 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<Self>) {
- 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<Self>) {
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<Self>) {
- 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<Channel>, &ChannelPath)> {
+ fn selected_channel(&self) -> Option<&Arc<Channel>> {
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 } => {
@@ -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();
@@ -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
})
});
}
@@ -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<Editor>) {
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<Editor>) {
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<Editor>) {
@@ -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);
@@ -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))
@@ -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<Item = (char, Range<usize>)> + '_ {
+ 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<Item = (char, Range<usize>)> + '_ {
+ 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);
}
@@ -373,8 +373,8 @@ pub(crate) struct DiagnosticEndpoint {
#[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Debug)]
pub enum CharKind {
- Punctuation,
Whitespace,
+ Punctuation,
Word,
}
@@ -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<String> {
+ 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;
@@ -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<String>;
+ fn guest_token(&self, room: &str, identity: &str) -> Result<String>;
}
#[derive(Clone)]
@@ -138,4 +139,13 @@ impl Client for LiveKitClient {
token::VideoGrant::to_join(room),
)
}
+
+ fn guest_token(&self, room: &str, identity: &str) -> Result<String> {
+ token::create(
+ &self.key,
+ &self.secret,
+ Some(identity),
+ token::VideoGrant::for_guest(room),
+ )
+ }
}
@@ -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(
@@ -220,96 +220,31 @@ impl NodeRuntime for RealNodeRuntime {
}
}
-pub struct FakeNodeRuntime(Option<PrettierSupport>);
-
-struct PrettierSupport {
- plugins: Vec<&'static str>,
-}
+pub struct FakeNodeRuntime;
impl FakeNodeRuntime {
pub fn new() -> Arc<dyn NodeRuntime> {
- Arc::new(FakeNodeRuntime(None))
- }
-
- pub fn with_prettier_support(plugins: &[&'static str]) -> Arc<dyn NodeRuntime> {
- 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<PathBuf> {
- 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<Output> {
- if let Some(prettier_support) = &self.0 {
- prettier_support
- .run_npm_subcommand(directory, subcommand, args)
- .await
- } else {
- unreachable!()
- }
- }
-
- async fn npm_package_latest_version(&self, name: &str) -> anyhow::Result<String> {
- 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<PathBuf> {
- Ok(PathBuf::from("prettier_fake_node"))
- }
-
- async fn run_npm_subcommand(&self, _: Option<&Path>, _: &str, _: &[&str]) -> Result<Output> {
- unreachable!()
+ unreachable!("Should not run npm subcommand '{subcommand}' with args {args:?}")
}
async fn npm_package_latest_version(&self, name: &str) -> anyhow::Result<String> {
- 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:?}")
}
}
@@ -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<LocateStart>,
fs: Arc<dyn Fs>,
@@ -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),
@@ -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<Self>) {
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<WorktreeId>,
+ _new_language: &Language,
+ _language_settings: &LanguageSettings,
+ _cx: &mut ModelContext<Self>,
+ ) -> Task<anyhow::Result<()>> {
+ return Task::ready(Ok(()));
+ }
+
+ #[cfg(not(any(test, feature = "test-support")))]
fn install_default_formatters(
&self,
worktree: Option<WorktreeId>,
@@ -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
@@ -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 {
@@ -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),
@@ -41,6 +41,7 @@ pub struct EmbeddingQueue {
pending_batch_token_count: usize,
finished_files_tx: channel::Sender<FileToEmbed>,
finished_files_rx: channel::Receiver<FileToEmbed>,
+ api_key: Option<String>,
}
#[derive(Clone)]
@@ -50,7 +51,11 @@ pub struct FileFragmentToEmbed {
}
impl EmbeddingQueue {
- pub fn new(embedding_provider: Arc<dyn EmbeddingProvider>, executor: Arc<Background>) -> Self {
+ pub fn new(
+ embedding_provider: Arc<dyn EmbeddingProvider>,
+ executor: Arc<Background>,
+ api_key: Option<String>,
+ ) -> 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<String>) {
+ 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 {
@@ -124,6 +124,8 @@ pub struct SemanticIndex {
_embedding_task: Task<()>,
_parsing_files_tasks: Vec<Task<()>>,
projects: HashMap<WeakModelHandle<Project>, ProjectState>,
+ api_key: Option<String>,
+ embedding_queue: Arc<Mutex<EmbeddingQueue>>,
}
struct ProjectState {
@@ -269,7 +271,7 @@ pub struct SearchResult {
}
impl SemanticIndex {
- pub fn global(cx: &AppContext) -> Option<ModelHandle<SemanticIndex>> {
+ pub fn global(cx: &mut AppContext) -> Option<ModelHandle<SemanticIndex>> {
if cx.has_global::<ModelHandle<Self>>() {
Some(cx.global::<ModelHandle<SemanticIndex>>().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::<SemanticIndexSettings>(cx).enabled
}
pub fn status(&self, project: &ModelHandle<Project>) -> 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::<SearchResult>::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<Project>,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
- 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<String>,
) -> 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);
@@ -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<dyn LanguageModel> {
Box::new(DummyLanguageModel {})
}
- fn is_authenticated(&self) -> bool {
- true
+ fn retrieve_credentials(&self, _cx: &AppContext) -> Option<String> {
+ 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<String>) -> Result<Vec<Embedding>> {
+ async fn embed_batch(
+ &self,
+ spans: Vec<String>,
+ _api_key: Option<String>,
+ ) -> Result<Vec<Embedding>> {
self.embedding_count
.fetch_add(spans.len(), atomic::Ordering::SeqCst);
@@ -250,6 +250,7 @@ pub struct CollabPanel {
pub add_contact_button: Toggleable<Interactive<IconButton>>,
pub add_channel_button: Toggleable<Interactive<IconButton>>,
pub header_row: ContainedText,
+ pub dragged_over_header: ContainerStyle,
pub subheader_row: Toggleable<Interactive<ContainedText>>,
pub leave_call: Interactive<ContainedText>,
pub contact_row: Toggleable<Interactive<ContainerStyle>>,
@@ -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,
@@ -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"})
@@ -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<Range<DisplayPoint>> {
- let mut matched_ends = 0;
- let mut start = None;
- for (char, mut point) in map.reverse_chars_at(relative_to) {
- if char == start_marker {
- if matched_ends > 0 {
- matched_ends -= 1;
- } else {
- if around {
- start = Some(point)
- } else {
- *point.column_mut() += char.len_utf8() as u32;
- start = Some(point)
+ 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 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 char == '\n' && !search_across_lines {
+ 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 ch == close_marker {
+ if matched_opens == 0 {
+ closing = Some(range);
+ break;
+ }
+ matched_opens -= 1;
+ } else if ch == open_marker {
+ matched_opens += 1;
+ }
}
- let (Some(mut start), Some(mut end)) = (start, end) else {
+ let Some(mut closing) = closing 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;
+ 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;
}
- 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;
- 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 !char.is_whitespace() {
+ }
+ }
+
+ 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;
}
- new_end = point
+ if ch != '\n' {
+ closing.start = range.start
+ }
}
}
- Some(start..end)
+ 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());
@@ -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,
+ );
+}
@@ -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<Range<usize>> {
- 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;
@@ -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<Mode>, String, Vec<Range<Point>>) {
+ pub async fn state(&mut self) -> (Option<Mode>, 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<Mode>, String, Vec<Range<Point>>) {
- 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<Mode>, 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<Range<Point>> {
- self.state().await.2
- }
-
pub async fn mode(&mut self) -> Option<Mode> {
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<Range<Point>>) {
let (text, ranges) = util::test::marked_text_ranges(marked_text, true);
let point_ranges = ranges
@@ -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 {
@@ -1 +0,0 @@
-[{"Text":"The quick\n\nbrown fox jumps\nover the lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[3,5],"end":[3,5]}}]
@@ -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"}
@@ -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"}
@@ -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"}
@@ -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"}}
@@ -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"}}
@@ -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"}}
@@ -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"}}
@@ -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<AnyWindowHandle> {
+pub async fn get_any_active_workspace(
+ app_state: Arc<AppState>,
+ mut cx: AsyncAppContext,
+) -> Result<ViewHandle<Workspace>> {
+ // 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<ViewHandle<Workspace>> {
for window in cx.windows() {
- let found = window.update(cx, |cx| {
- let is_workspace = cx.root_view().clone().downcast::<Workspace>().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::<Workspace>()
+ .map(|workspace| {
+ cx.activate_window();
+ workspace
+ })
+ })
+ .flatten()
+ {
+ return Some(workspace);
}
}
None
@@ -55,7 +55,7 @@ fn parse_eval() -> anyhow::Result<Vec<RepoEval>> {
.as_path()
.parent()
.unwrap()
- .join("crates/semantic_index/eval");
+ .join("zed/crates/semantic_index/eval");
let mut repo_evals: Vec<RepoEval> = Vec::new();
for entry in fs::read_dir(eval_folder)? {
@@ -3,6 +3,7 @@
(raw_string)
(heredoc_body)
(heredoc_start)
+ (ansi_c_string)
] @string
(command_name) @function
@@ -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();
+ })
+ }
+ }
}
}
}
@@ -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::<u64>() {
- return Some(OpenRequest::JoinChannel { channel_id });
+ if Some("notes") == parts.next() {
+ return Some(OpenRequest::OpenChannelNotes { channel_id });
+ } else {
+ return Some(OpenRequest::JoinChannel { channel_id });
+ }
}
}
}
@@ -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
@@ -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: {
@@ -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,
},
},
@@ -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: {