catchup with main

This commit is contained in:
KCaverly 2023-10-25 16:31:00 +02:00
commit 71bc35d241
84 changed files with 6026 additions and 3636 deletions

5
Cargo.lock generated
View file

@ -1575,6 +1575,7 @@ dependencies = [
"serde", "serde",
"serde_derive", "serde_derive",
"settings", "settings",
"smallvec",
"theme", "theme",
"theme_selector", "theme_selector",
"time", "time",
@ -8590,8 +8591,8 @@ dependencies = [
[[package]] [[package]]
name = "tree-sitter-bash" name = "tree-sitter-bash"
version = "0.19.0" version = "0.20.4"
source = "git+https://github.com/tree-sitter/tree-sitter-bash?rev=1b0321ee85701d5036c334a6f04761cdc672e64c#1b0321ee85701d5036c334a6f04761cdc672e64c" source = "git+https://github.com/tree-sitter/tree-sitter-bash?rev=7331995b19b8f8aba2d5e26deb51d2195c18bc94#7331995b19b8f8aba2d5e26deb51d2195c18bc94"
dependencies = [ dependencies = [
"cc", "cc",
"tree-sitter", "tree-sitter",

View file

@ -125,7 +125,7 @@ pretty_assertions = "1.3.0"
git2 = { version = "0.15", default-features = false} git2 = { version = "0.15", default-features = false}
uuid = { version = "1.1.2", features = ["v4"] } 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-c = "0.20.1"
tree-sitter-cpp = { git = "https://github.com/tree-sitter/tree-sitter-cpp", rev="f44509141e7e483323d2ec178f2d2e6c0fc041c1" } 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" } tree-sitter-css = { git = "https://github.com/tree-sitter/tree-sitter-css", rev = "769203d0f9abe1a9a691ac2b9fe4bb4397a73c51" }

View file

@ -370,42 +370,15 @@
{ {
"context": "Pane", "context": "Pane",
"bindings": { "bindings": {
"ctrl-1": [ "ctrl-1": ["pane::ActivateItem", 0],
"pane::ActivateItem", "ctrl-2": ["pane::ActivateItem", 1],
0 "ctrl-3": ["pane::ActivateItem", 2],
], "ctrl-4": ["pane::ActivateItem", 3],
"ctrl-2": [ "ctrl-5": ["pane::ActivateItem", 4],
"pane::ActivateItem", "ctrl-6": ["pane::ActivateItem", 5],
1 "ctrl-7": ["pane::ActivateItem", 6],
], "ctrl-8": ["pane::ActivateItem", 7],
"ctrl-3": [ "ctrl-9": ["pane::ActivateItem", 8],
"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-0": "pane::ActivateLastItem",
"ctrl--": "pane::GoBack", "ctrl--": "pane::GoBack",
"ctrl-_": "pane::GoForward", "ctrl-_": "pane::GoForward",
@ -416,42 +389,15 @@
{ {
"context": "Workspace", "context": "Workspace",
"bindings": { "bindings": {
"cmd-1": [ "cmd-1": ["workspace::ActivatePane", 0],
"workspace::ActivatePane", "cmd-2": ["workspace::ActivatePane", 1],
0 "cmd-3": ["workspace::ActivatePane", 2],
], "cmd-4": ["workspace::ActivatePane", 3],
"cmd-2": [ "cmd-5": ["workspace::ActivatePane", 4],
"workspace::ActivatePane", "cmd-6": ["workspace::ActivatePane", 5],
1 "cmd-7": ["workspace::ActivatePane", 6],
], "cmd-8": ["workspace::ActivatePane", 7],
"cmd-3": [ "cmd-9": ["workspace::ActivatePane", 8],
"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-b": "workspace::ToggleLeftDock",
"cmd-r": "workspace::ToggleRightDock", "cmd-r": "workspace::ToggleRightDock",
"cmd-j": "workspace::ToggleBottomDock", "cmd-j": "workspace::ToggleBottomDock",
@ -494,38 +440,14 @@
}, },
{ {
"bindings": { "bindings": {
"cmd-k cmd-left": [ "cmd-k cmd-left": ["workspace::ActivatePaneInDirection", "Left"],
"workspace::ActivatePaneInDirection", "cmd-k cmd-right": ["workspace::ActivatePaneInDirection", "Right"],
"Left" "cmd-k cmd-up": ["workspace::ActivatePaneInDirection", "Up"],
], "cmd-k cmd-down": ["workspace::ActivatePaneInDirection", "Down"],
"cmd-k cmd-right": [ "cmd-k shift-left": ["workspace::SwapPaneInDirection", "Left"],
"workspace::ActivatePaneInDirection", "cmd-k shift-right": ["workspace::SwapPaneInDirection", "Right"],
"Right" "cmd-k shift-up": ["workspace::SwapPaneInDirection", "Up"],
], "cmd-k shift-down": ["workspace::SwapPaneInDirection", "Down"]
"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 // Bindings from Atom
@ -627,14 +549,6 @@
"space": "collab_panel::InsertSpace" "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", "context": "ChannelModal",
"bindings": { "bindings": {
@ -655,57 +569,21 @@
"cmd-v": "terminal::Paste", "cmd-v": "terminal::Paste",
"cmd-k": "terminal::Clear", "cmd-k": "terminal::Clear",
// Some nice conveniences // Some nice conveniences
"cmd-backspace": [ "cmd-backspace": ["terminal::SendText", "\u0015"],
"terminal::SendText", "cmd-right": ["terminal::SendText", "\u0005"],
"\u0015" "cmd-left": ["terminal::SendText", "\u0001"],
],
"cmd-right": [
"terminal::SendText",
"\u0005"
],
"cmd-left": [
"terminal::SendText",
"\u0001"
],
// Terminal.app compatibility // Terminal.app compatibility
"alt-left": [ "alt-left": ["terminal::SendText", "\u001bb"],
"terminal::SendText", "alt-right": ["terminal::SendText", "\u001bf"],
"\u001bb"
],
"alt-right": [
"terminal::SendText",
"\u001bf"
],
// There are conflicting bindings for these keys in the global context. // There are conflicting bindings for these keys in the global context.
// these bindings override them, remove at your own risk: // these bindings override them, remove at your own risk:
"up": [ "up": ["terminal::SendKeystroke", "up"],
"terminal::SendKeystroke", "pageup": ["terminal::SendKeystroke", "pageup"],
"up" "down": ["terminal::SendKeystroke", "down"],
], "pagedown": ["terminal::SendKeystroke", "pagedown"],
"pageup": [ "escape": ["terminal::SendKeystroke", "escape"],
"terminal::SendKeystroke", "enter": ["terminal::SendKeystroke", "enter"],
"pageup" "ctrl-c": ["terminal::SendKeystroke", "ctrl-c"]
],
"down": [
"terminal::SendKeystroke",
"down"
],
"pagedown": [
"terminal::SendKeystroke",
"pagedown"
],
"escape": [
"terminal::SendKeystroke",
"escape"
],
"enter": [
"terminal::SendKeystroke",
"enter"
],
"ctrl-c": [
"terminal::SendKeystroke",
"ctrl-c"
]
} }
} }
] ]

View file

@ -39,6 +39,7 @@
"w": "vim::NextWordStart", "w": "vim::NextWordStart",
"{": "vim::StartOfParagraph", "{": "vim::StartOfParagraph",
"}": "vim::EndOfParagraph", "}": "vim::EndOfParagraph",
"|": "vim::GoToColumn",
"shift-w": [ "shift-w": [
"vim::NextWordStart", "vim::NextWordStart",
{ {
@ -97,14 +98,8 @@
"ctrl-o": "pane::GoBack", "ctrl-o": "pane::GoBack",
"ctrl-i": "pane::GoForward", "ctrl-i": "pane::GoForward",
"ctrl-]": "editor::GoToDefinition", "ctrl-]": "editor::GoToDefinition",
"escape": [ "escape": ["vim::SwitchMode", "Normal"],
"vim::SwitchMode", "ctrl+[": ["vim::SwitchMode", "Normal"],
"Normal"
],
"ctrl+[": [
"vim::SwitchMode",
"Normal"
],
"v": "vim::ToggleVisual", "v": "vim::ToggleVisual",
"shift-v": "vim::ToggleVisualLine", "shift-v": "vim::ToggleVisualLine",
"ctrl-v": "vim::ToggleVisualBlock", "ctrl-v": "vim::ToggleVisualBlock",
@ -233,123 +228,36 @@
} }
], ],
// Count support // Count support
"1": [ "1": ["vim::Number", 1],
"vim::Number", "2": ["vim::Number", 2],
1 "3": ["vim::Number", 3],
], "4": ["vim::Number", 4],
"2": [ "5": ["vim::Number", 5],
"vim::Number", "6": ["vim::Number", 6],
2 "7": ["vim::Number", 7],
], "8": ["vim::Number", 8],
"3": [ "9": ["vim::Number", 9],
"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) // window related commands (ctrl-w X)
"ctrl-w left": [ "ctrl-w left": ["workspace::ActivatePaneInDirection", "Left"],
"workspace::ActivatePaneInDirection", "ctrl-w right": ["workspace::ActivatePaneInDirection", "Right"],
"Left" "ctrl-w up": ["workspace::ActivatePaneInDirection", "Up"],
], "ctrl-w down": ["workspace::ActivatePaneInDirection", "Down"],
"ctrl-w right": [ "ctrl-w h": ["workspace::ActivatePaneInDirection", "Left"],
"workspace::ActivatePaneInDirection", "ctrl-w l": ["workspace::ActivatePaneInDirection", "Right"],
"Right" "ctrl-w k": ["workspace::ActivatePaneInDirection", "Up"],
], "ctrl-w j": ["workspace::ActivatePaneInDirection", "Down"],
"ctrl-w up": [ "ctrl-w ctrl-h": ["workspace::ActivatePaneInDirection", "Left"],
"workspace::ActivatePaneInDirection", "ctrl-w ctrl-l": ["workspace::ActivatePaneInDirection", "Right"],
"Up" "ctrl-w ctrl-k": ["workspace::ActivatePaneInDirection", "Up"],
], "ctrl-w ctrl-j": ["workspace::ActivatePaneInDirection", "Down"],
"ctrl-w down": [ "ctrl-w shift-left": ["workspace::SwapPaneInDirection", "Left"],
"workspace::ActivatePaneInDirection", "ctrl-w shift-right": ["workspace::SwapPaneInDirection", "Right"],
"Down" "ctrl-w shift-up": ["workspace::SwapPaneInDirection", "Up"],
], "ctrl-w shift-down": ["workspace::SwapPaneInDirection", "Down"],
"ctrl-w h": [ "ctrl-w shift-h": ["workspace::SwapPaneInDirection", "Left"],
"workspace::ActivatePaneInDirection", "ctrl-w shift-l": ["workspace::SwapPaneInDirection", "Right"],
"Left" "ctrl-w shift-k": ["workspace::SwapPaneInDirection", "Up"],
], "ctrl-w shift-j": ["workspace::SwapPaneInDirection", "Down"],
"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 g t": "pane::ActivateNextItem",
"ctrl-w ctrl-g t": "pane::ActivateNextItem", "ctrl-w ctrl-g t": "pane::ActivateNextItem",
"ctrl-w g shift-t": "pane::ActivatePrevItem", "ctrl-w g shift-t": "pane::ActivatePrevItem",
@ -371,14 +279,8 @@
"ctrl-w ctrl-q": "pane::CloseAllItems", "ctrl-w ctrl-q": "pane::CloseAllItems",
"ctrl-w o": "workspace::CloseInactiveTabsAndPanes", "ctrl-w o": "workspace::CloseInactiveTabsAndPanes",
"ctrl-w ctrl-o": "workspace::CloseInactiveTabsAndPanes", "ctrl-w ctrl-o": "workspace::CloseInactiveTabsAndPanes",
"ctrl-w n": [ "ctrl-w n": ["workspace::NewFileInDirection", "Up"],
"workspace::NewFileInDirection", "ctrl-w ctrl-n": ["workspace::NewFileInDirection", "Up"]
"Up"
],
"ctrl-w ctrl-n": [
"workspace::NewFileInDirection",
"Up"
]
} }
}, },
{ {
@ -393,21 +295,12 @@
"context": "Editor && vim_mode == normal && vim_operator == none && !VimWaiting", "context": "Editor && vim_mode == normal && vim_operator == none && !VimWaiting",
"bindings": { "bindings": {
".": "vim::Repeat", ".": "vim::Repeat",
"c": [ "c": ["vim::PushOperator", "Change"],
"vim::PushOperator",
"Change"
],
"shift-c": "vim::ChangeToEndOfLine", "shift-c": "vim::ChangeToEndOfLine",
"d": [ "d": ["vim::PushOperator", "Delete"],
"vim::PushOperator",
"Delete"
],
"shift-d": "vim::DeleteToEndOfLine", "shift-d": "vim::DeleteToEndOfLine",
"shift-j": "vim::JoinLines", "shift-j": "vim::JoinLines",
"y": [ "y": ["vim::PushOperator", "Yank"],
"vim::PushOperator",
"Yank"
],
"shift-y": "vim::YankLine", "shift-y": "vim::YankLine",
"i": "vim::InsertBefore", "i": "vim::InsertBefore",
"shift-i": "vim::InsertFirstNonWhitespace", "shift-i": "vim::InsertFirstNonWhitespace",
@ -443,10 +336,7 @@
"backwards": true "backwards": true
} }
], ],
"r": [ "r": ["vim::PushOperator", "Replace"],
"vim::PushOperator",
"Replace"
],
"s": "vim::Substitute", "s": "vim::Substitute",
"shift-s": "vim::SubstituteLine", "shift-s": "vim::SubstituteLine",
"> >": "editor::Indent", "> >": "editor::Indent",
@ -458,10 +348,7 @@
{ {
"context": "Editor && VimCount", "context": "Editor && VimCount",
"bindings": { "bindings": {
"0": [ "0": ["vim::Number", 0]
"vim::Number",
0
]
} }
}, },
{ {
@ -497,12 +384,15 @@
"'": "vim::Quotes", "'": "vim::Quotes",
"`": "vim::BackQuotes", "`": "vim::BackQuotes",
"\"": "vim::DoubleQuotes", "\"": "vim::DoubleQuotes",
"|": "vim::VerticalBars",
"(": "vim::Parentheses", "(": "vim::Parentheses",
")": "vim::Parentheses", ")": "vim::Parentheses",
"b": "vim::Parentheses",
"[": "vim::SquareBrackets", "[": "vim::SquareBrackets",
"]": "vim::SquareBrackets", "]": "vim::SquareBrackets",
"{": "vim::CurlyBrackets", "{": "vim::CurlyBrackets",
"}": "vim::CurlyBrackets", "}": "vim::CurlyBrackets",
"shift-b": "vim::CurlyBrackets",
"<": "vim::AngleBrackets", "<": "vim::AngleBrackets",
">": "vim::AngleBrackets" ">": "vim::AngleBrackets"
} }
@ -548,22 +438,10 @@
"shift-i": "vim::InsertBefore", "shift-i": "vim::InsertBefore",
"shift-a": "vim::InsertAfter", "shift-a": "vim::InsertAfter",
"shift-j": "vim::JoinLines", "shift-j": "vim::JoinLines",
"r": [ "r": ["vim::PushOperator", "Replace"],
"vim::PushOperator", "ctrl-c": ["vim::SwitchMode", "Normal"],
"Replace" "escape": ["vim::SwitchMode", "Normal"],
], "ctrl+[": ["vim::SwitchMode", "Normal"],
"ctrl-c": [
"vim::SwitchMode",
"Normal"
],
"escape": [
"vim::SwitchMode",
"Normal"
],
"ctrl+[": [
"vim::SwitchMode",
"Normal"
],
">": "editor::Indent", ">": "editor::Indent",
"<": "editor::Outdent", "<": "editor::Outdent",
"i": [ "i": [
@ -602,14 +480,8 @@
"bindings": { "bindings": {
"tab": "vim::Tab", "tab": "vim::Tab",
"enter": "vim::Enter", "enter": "vim::Enter",
"escape": [ "escape": ["vim::SwitchMode", "Normal"],
"vim::SwitchMode", "ctrl+[": ["vim::SwitchMode", "Normal"]
"Normal"
],
"ctrl+[": [
"vim::SwitchMode",
"Normal"
]
} }
}, },
{ {

View file

@ -148,7 +148,7 @@
// Where to dock channels panel. Can be 'left' or 'right'. // Where to dock channels panel. Can be 'left' or 'right'.
"dock": "right", "dock": "right",
// Default width of the channels panel. // Default width of the channels panel.
"default_width": 240 "default_width": 380
}, },
"assistant": { "assistant": {
// Whether to show the assistant panel button in the status bar. // Whether to show the assistant panel button in the status bar.

View file

@ -2,6 +2,7 @@ use std::time::Instant;
use anyhow::Result; use anyhow::Result;
use async_trait::async_trait; use async_trait::async_trait;
use gpui::AppContext;
use ordered_float::OrderedFloat; use ordered_float::OrderedFloat;
use rusqlite::types::{FromSql, FromSqlResult, ToSqlOutput, ValueRef}; use rusqlite::types::{FromSql, FromSqlResult, ToSqlOutput, ValueRef};
use rusqlite::ToSql; use rusqlite::ToSql;
@ -70,8 +71,12 @@ impl Embedding {
#[async_trait] #[async_trait]
pub trait EmbeddingProvider: Sync + Send { pub trait EmbeddingProvider: Sync + Send {
fn base_model(&self) -> Box<dyn LanguageModel>; fn base_model(&self) -> Box<dyn LanguageModel>;
fn is_authenticated(&self) -> bool; fn retrieve_credentials(&self, cx: &AppContext) -> Option<String>;
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>>;
fn max_tokens_per_batch(&self) -> usize; fn max_tokens_per_batch(&self) -> usize;
fn rate_limit_expiration(&self) -> Option<Instant>; fn rate_limit_expiration(&self) -> Option<Instant>;
} }

View file

@ -6,6 +6,7 @@ use crate::{
models::{LanguageModel, TruncationDirection}, models::{LanguageModel, TruncationDirection},
}; };
use async_trait::async_trait; use async_trait::async_trait;
use gpui::AppContext;
use serde::Serialize; use serde::Serialize;
pub struct DummyLanguageModel {} pub struct DummyLanguageModel {}
@ -58,16 +59,20 @@ pub struct DummyEmbeddingProvider {}
#[async_trait] #[async_trait]
impl EmbeddingProvider for DummyEmbeddingProvider { impl EmbeddingProvider for DummyEmbeddingProvider {
fn retrieve_credentials(&self, _cx: &AppContext) -> Option<String> {
Some("Dummy Credentials".to_string())
}
fn base_model(&self) -> Box<dyn LanguageModel> { fn base_model(&self) -> Box<dyn LanguageModel> {
Box::new(DummyLanguageModel {}) Box::new(DummyLanguageModel {})
} }
fn is_authenticated(&self) -> bool {
true
}
fn rate_limit_expiration(&self) -> Option<Instant> { fn rate_limit_expiration(&self) -> Option<Instant> {
None 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. // 1024 is the OpenAI Embeddings size for ada models.
// the model we will likely be starting with. // the model we will likely be starting with.
let dummy_vec = Embedding::from(vec![0.32 as f32; 1536]); let dummy_vec = Embedding::from(vec![0.32 as f32; 1536]);

View file

@ -2,7 +2,7 @@ use anyhow::{anyhow, Result};
use async_trait::async_trait; use async_trait::async_trait;
use futures::AsyncReadExt; use futures::AsyncReadExt;
use gpui::executor::Background; use gpui::executor::Background;
use gpui::serde_json; use gpui::{serde_json, AppContext};
use isahc::http::StatusCode; use isahc::http::StatusCode;
use isahc::prelude::Configurable; use isahc::prelude::Configurable;
use isahc::{AsyncBody, Response}; use isahc::{AsyncBody, Response};
@ -17,11 +17,14 @@ use std::sync::Arc;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use tiktoken_rs::{cl100k_base, CoreBPE}; use tiktoken_rs::{cl100k_base, CoreBPE};
use util::http::{HttpClient, Request}; use util::http::{HttpClient, Request};
use util::ResultExt;
use crate::embedding::{Embedding, EmbeddingProvider}; use crate::embedding::{Embedding, EmbeddingProvider};
use crate::models::LanguageModel; use crate::models::LanguageModel;
use crate::providers::open_ai::OpenAILanguageModel; use crate::providers::open_ai::OpenAILanguageModel;
use super::OPENAI_API_URL;
lazy_static! { lazy_static! {
static ref OPENAI_API_KEY: Option<String> = env::var("OPENAI_API_KEY").ok(); static ref OPENAI_API_KEY: Option<String> = env::var("OPENAI_API_KEY").ok();
static ref OPENAI_BPE_TOKENIZER: CoreBPE = cl100k_base().unwrap(); static ref OPENAI_BPE_TOKENIZER: CoreBPE = cl100k_base().unwrap();
@ -135,13 +138,25 @@ impl OpenAIEmbeddingProvider {
#[async_trait] #[async_trait]
impl EmbeddingProvider for OpenAIEmbeddingProvider { 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> { fn base_model(&self) -> Box<dyn LanguageModel> {
let model: Box<dyn LanguageModel> = Box::new(self.model.clone()); let model: Box<dyn LanguageModel> = Box::new(self.model.clone());
model model
} }
fn is_authenticated(&self) -> bool {
OPENAI_API_KEY.as_ref().is_some()
}
fn max_tokens_per_batch(&self) -> usize { fn max_tokens_per_batch(&self) -> usize {
50000 50000
} }
@ -164,7 +179,11 @@ impl EmbeddingProvider for OpenAIEmbeddingProvider {
// (output, tokens.len()) // (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 BACKOFF_SECONDS: [usize; 4] = [3, 5, 15, 45];
const MAX_RETRIES: usize = 4; const MAX_RETRIES: usize = 4;

View file

@ -10,7 +10,7 @@ use client::{
ZED_ALWAYS_ACTIVE, ZED_ALWAYS_ACTIVE,
}; };
use collections::HashSet; use collections::HashSet;
use futures::{future::Shared, FutureExt}; use futures::{channel::oneshot, future::Shared, Future, FutureExt};
use gpui::{ use gpui::{
AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Subscription, Task, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Subscription, Task,
WeakModelHandle, WeakModelHandle,
@ -37,10 +37,42 @@ pub struct IncomingCall {
pub initial_project: Option<proto::ParticipantProject>, 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. /// Singleton global maintaining the user's participation in a room across workspaces.
pub struct ActiveCall { pub struct ActiveCall {
room: Option<(ModelHandle<Room>, Vec<Subscription>)>, room: Option<(ModelHandle<Room>, Vec<Subscription>)>,
pending_room_creation: Option<Shared<Task<Result<ModelHandle<Room>, Arc<anyhow::Error>>>>>, pending_room_creation: Option<Shared<Task<Result<ModelHandle<Room>, Arc<anyhow::Error>>>>>,
_join_debouncer: OneAtATime,
location: Option<WeakModelHandle<Project>>, location: Option<WeakModelHandle<Project>>,
pending_invites: HashSet<u64>, pending_invites: HashSet<u64>,
incoming_call: ( incoming_call: (
@ -69,6 +101,7 @@ impl ActiveCall {
pending_invites: Default::default(), pending_invites: Default::default(),
incoming_call: watch::channel(), incoming_call: watch::channel(),
_join_debouncer: OneAtATime { cancel: None },
_subscriptions: vec![ _subscriptions: vec![
client.add_request_handler(cx.handle(), Self::handle_incoming_call), client.add_request_handler(cx.handle(), Self::handle_incoming_call),
client.add_message_handler(cx.handle(), Self::handle_call_canceled), client.add_message_handler(cx.handle(), Self::handle_call_canceled),
@ -143,6 +176,10 @@ impl ActiveCall {
} }
cx.notify(); cx.notify();
if self._join_debouncer.running() {
return Task::ready(Ok(()));
}
let room = if let Some(room) = self.room().cloned() { let room = if let Some(room) = self.room().cloned() {
Some(Task::ready(Ok(room)).shared()) Some(Task::ready(Ok(room)).shared())
} else { } else {
@ -259,11 +296,20 @@ impl ActiveCall {
return Task::ready(Err(anyhow!("no incoming call"))); 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 { cx.spawn(|this, mut cx| async move {
let room = join.await?; 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?; .await?;
this.update(&mut cx, |this, cx| { this.update(&mut cx, |this, cx| {
this.report_call_event("accept incoming", cx) this.report_call_event("accept incoming", cx)
@ -290,20 +336,28 @@ impl ActiveCall {
&mut self, &mut self,
channel_id: u64, channel_id: u64,
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) -> Task<Result<ModelHandle<Room>>> { ) -> Task<Result<Option<ModelHandle<Room>>>> {
if let Some(room) = self.room().cloned() { if let Some(room) = self.room().cloned() {
if room.read(cx).channel_id() == Some(channel_id) { if room.read(cx).channel_id() == Some(channel_id) {
return Task::ready(Ok(room)); return Task::ready(Ok(Some(room)));
} else { } else {
room.update(cx, |room, cx| room.clear_state(cx)); 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?; 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?; .await?;
this.update(&mut cx, |this, cx| { this.update(&mut cx, |this, cx| {
this.report_call_event("join channel", 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); 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);
}
}

View file

@ -1,7 +1,6 @@
use crate::{ use crate::{
call_settings::CallSettings, call_settings::CallSettings,
participant::{LocalParticipant, ParticipantLocation, RemoteParticipant, RemoteVideoTrack}, participant::{LocalParticipant, ParticipantLocation, RemoteParticipant, RemoteVideoTrack},
IncomingCall,
}; };
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use audio::{Audio, Sound}; use audio::{Audio, Sound};
@ -55,7 +54,7 @@ pub enum Event {
pub struct Room { pub struct Room {
id: u64, id: u64,
channel_id: Option<u64>, pub channel_id: Option<u64>,
live_kit: Option<LiveKitRoom>, live_kit: Option<LiveKitRoom>,
status: RoomStatus, status: RoomStatus,
shared_projects: HashSet<WeakModelHandle<Project>>, 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( fn new(
id: u64, id: u64,
channel_id: Option<u64>, channel_id: Option<u64>,
@ -181,20 +184,23 @@ impl Room {
}); });
let connect = room.connect(&connection_info.server_url, &connection_info.token); let connect = room.connect(&connection_info.server_url, &connection_info.token);
cx.spawn(|this, mut cx| async move { if connection_info.can_publish {
connect.await?; cx.spawn(|this, mut cx| async move {
connect.await?;
if !cx.read(Self::mute_on_join) { if !cx.read(Self::mute_on_join) {
this.update(&mut cx, |this, cx| this.share_microphone(cx)) this.update(&mut cx, |this, cx| this.share_microphone(cx))
.await?; .await?;
} }
anyhow::Ok(()) anyhow::Ok(())
}) })
.detach_and_log_err(cx); .detach_and_log_err(cx);
}
Some(LiveKitRoom { Some(LiveKitRoom {
room, room,
can_publish: connection_info.can_publish,
screen_track: LocalTrack::None, screen_track: LocalTrack::None,
microphone_track: LocalTrack::None, microphone_track: LocalTrack::None,
next_publish_id: 0, next_publish_id: 0,
@ -284,37 +290,32 @@ impl Room {
}) })
} }
pub(crate) fn join_channel( pub(crate) async fn join_channel(
channel_id: u64, channel_id: u64,
client: Arc<Client>, client: Arc<Client>,
user_store: ModelHandle<UserStore>, user_store: ModelHandle<UserStore>,
cx: &mut AppContext, cx: AsyncAppContext,
) -> Task<Result<ModelHandle<Self>>> { ) -> Result<ModelHandle<Self>> {
cx.spawn(|cx| async move { Self::from_join_response(
Self::from_join_response( client.request(proto::JoinChannel { channel_id }).await?,
client.request(proto::JoinChannel { channel_id }).await?, client,
client, user_store,
user_store, cx,
cx, )
)
})
} }
pub(crate) fn join( pub(crate) async fn join(
call: &IncomingCall, room_id: u64,
client: Arc<Client>, client: Arc<Client>,
user_store: ModelHandle<UserStore>, user_store: ModelHandle<UserStore>,
cx: &mut AppContext, cx: AsyncAppContext,
) -> Task<Result<ModelHandle<Self>>> { ) -> Result<ModelHandle<Self>> {
let id = call.room_id; Self::from_join_response(
cx.spawn(|cx| async move { client.request(proto::JoinRoom { id: room_id }).await?,
Self::from_join_response( client,
client.request(proto::JoinRoom { id }).await?, user_store,
client, cx,
user_store, )
cx,
)
})
} }
pub fn mute_on_join(cx: &AppContext) -> bool { pub fn mute_on_join(cx: &AppContext) -> bool {
@ -1498,6 +1499,7 @@ struct LiveKitRoom {
deafened: bool, deafened: bool,
speaking: bool, speaking: bool,
next_publish_id: usize, next_publish_id: usize,
can_publish: bool,
_maintain_room: Task<()>, _maintain_room: Task<()>,
_maintain_tracks: [Task<()>; 2], _maintain_tracks: [Task<()>; 2],
} }

View file

@ -11,9 +11,7 @@ pub use channel_chat::{
mentions_to_proto, ChannelChat, ChannelChatEvent, ChannelMessage, ChannelMessageId, mentions_to_proto, ChannelChat, ChannelChatEvent, ChannelMessage, ChannelMessageId,
MessageParams, MessageParams,
}; };
pub use channel_store::{ pub use channel_store::{Channel, ChannelEvent, ChannelId, ChannelMembership, ChannelStore};
Channel, ChannelData, ChannelEvent, ChannelId, ChannelMembership, ChannelPath, ChannelStore,
};
#[cfg(test)] #[cfg(test)]
mod channel_store_tests; mod channel_store_tests;

View file

@ -1,4 +1,4 @@
use crate::Channel; use crate::{Channel, ChannelId, ChannelStore};
use anyhow::Result; use anyhow::Result;
use client::{Client, Collaborator, UserStore}; use client::{Client, Collaborator, UserStore};
use collections::HashMap; use collections::HashMap;
@ -19,10 +19,11 @@ pub(crate) fn init(client: &Arc<Client>) {
} }
pub struct ChannelBuffer { pub struct ChannelBuffer {
pub(crate) channel: Arc<Channel>, pub channel_id: ChannelId,
connected: bool, connected: bool,
collaborators: HashMap<PeerId, Collaborator>, collaborators: HashMap<PeerId, Collaborator>,
user_store: ModelHandle<UserStore>, user_store: ModelHandle<UserStore>,
channel_store: ModelHandle<ChannelStore>,
buffer: ModelHandle<language::Buffer>, buffer: ModelHandle<language::Buffer>,
buffer_epoch: u64, buffer_epoch: u64,
client: Arc<Client>, client: Arc<Client>,
@ -34,6 +35,7 @@ pub enum ChannelBufferEvent {
CollaboratorsChanged, CollaboratorsChanged,
Disconnected, Disconnected,
BufferEdited, BufferEdited,
ChannelChanged,
} }
impl Entity for ChannelBuffer { impl Entity for ChannelBuffer {
@ -46,7 +48,7 @@ impl Entity for ChannelBuffer {
} }
self.client self.client
.send(proto::LeaveChannelBuffer { .send(proto::LeaveChannelBuffer {
channel_id: self.channel.id, channel_id: self.channel_id,
}) })
.log_err(); .log_err();
} }
@ -58,6 +60,7 @@ impl ChannelBuffer {
channel: Arc<Channel>, channel: Arc<Channel>,
client: Arc<Client>, client: Arc<Client>,
user_store: ModelHandle<UserStore>, user_store: ModelHandle<UserStore>,
channel_store: ModelHandle<ChannelStore>,
mut cx: AsyncAppContext, mut cx: AsyncAppContext,
) -> Result<ModelHandle<Self>> { ) -> Result<ModelHandle<Self>> {
let response = client let response = client
@ -90,9 +93,10 @@ impl ChannelBuffer {
connected: true, connected: true,
collaborators: Default::default(), collaborators: Default::default(),
acknowledge_task: None, acknowledge_task: None,
channel, channel_id: channel.id,
subscription: Some(subscription.set_model(&cx.handle(), &mut cx.to_async())), subscription: Some(subscription.set_model(&cx.handle(), &mut cx.to_async())),
user_store, user_store,
channel_store,
}; };
this.replace_collaborators(response.collaborators, cx); this.replace_collaborators(response.collaborators, cx);
this this
@ -179,7 +183,7 @@ impl ChannelBuffer {
let operation = language::proto::serialize_operation(operation); let operation = language::proto::serialize_operation(operation);
self.client self.client
.send(proto::UpdateChannelBuffer { .send(proto::UpdateChannelBuffer {
channel_id: self.channel.id, channel_id: self.channel_id,
operations: vec![operation], operations: vec![operation],
}) })
.log_err(); .log_err();
@ -223,12 +227,15 @@ impl ChannelBuffer {
&self.collaborators &self.collaborators
} }
pub fn channel(&self) -> Arc<Channel> { pub fn channel(&self, cx: &AppContext) -> Option<Arc<Channel>> {
self.channel.clone() self.channel_store
.read(cx)
.channel_for_id(self.channel_id)
.cloned()
} }
pub(crate) fn disconnect(&mut self, cx: &mut ModelContext<Self>) { 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 { if self.connected {
self.connected = false; self.connected = false;
self.subscription.take(); 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 { pub fn is_connected(&self) -> bool {
self.connected self.connected
} }

View file

@ -19,7 +19,7 @@ use time::OffsetDateTime;
use util::{post_inc, ResultExt as _, TryFutureExt}; use util::{post_inc, ResultExt as _, TryFutureExt};
pub struct ChannelChat { pub struct ChannelChat {
channel: Arc<Channel>, pub channel_id: ChannelId,
messages: SumTree<ChannelMessage>, messages: SumTree<ChannelMessage>,
acknowledged_message_ids: HashSet<u64>, acknowledged_message_ids: HashSet<u64>,
channel_store: ModelHandle<ChannelStore>, channel_store: ModelHandle<ChannelStore>,
@ -87,7 +87,7 @@ impl Entity for ChannelChat {
fn release(&mut self, _: &mut AppContext) { fn release(&mut self, _: &mut AppContext) {
self.rpc self.rpc
.send(proto::LeaveChannelChat { .send(proto::LeaveChannelChat {
channel_id: self.channel.id, channel_id: self.channel_id,
}) })
.log_err(); .log_err();
} }
@ -112,7 +112,7 @@ impl ChannelChat {
Ok(cx.add_model(|cx| { Ok(cx.add_model(|cx| {
let mut this = Self { let mut this = Self {
channel, channel_id: channel.id,
user_store, user_store,
channel_store, channel_store,
rpc: client, rpc: client,
@ -130,8 +130,11 @@ impl ChannelChat {
})) }))
} }
pub fn channel(&self) -> &Arc<Channel> { pub fn channel(&self, cx: &AppContext) -> Option<Arc<Channel>> {
&self.channel self.channel_store
.read(cx)
.channel_for_id(self.channel_id)
.cloned()
} }
pub fn client(&self) -> &Arc<Client> { pub fn client(&self) -> &Arc<Client> {
@ -153,7 +156,7 @@ impl ChannelChat {
.current_user() .current_user()
.ok_or_else(|| anyhow!("current_user is not present"))?; .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 pending_id = ChannelMessageId::Pending(post_inc(&mut self.next_pending_message_id));
let nonce = self.rng.gen(); let nonce = self.rng.gen();
self.insert_messages( self.insert_messages(
@ -195,7 +198,7 @@ impl ChannelChat {
pub fn remove_message(&mut self, id: u64, cx: &mut ModelContext<Self>) -> Task<Result<()>> { pub fn remove_message(&mut self, id: u64, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
let response = self.rpc.request(proto::RemoveChannelMessage { let response = self.rpc.request(proto::RemoveChannelMessage {
channel_id: self.channel.id, channel_id: self.channel_id,
message_id: id, message_id: id,
}); });
cx.spawn(|this, mut cx| async move { cx.spawn(|this, mut cx| async move {
@ -215,7 +218,7 @@ impl ChannelChat {
let rpc = self.rpc.clone(); let rpc = self.rpc.clone();
let user_store = self.user_store.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()?; let before_message_id = self.first_loaded_message_id()?;
Some(cx.spawn(|this, mut cx| { Some(cx.spawn(|this, mut cx| {
async move { async move {
@ -288,13 +291,13 @@ impl ChannelChat {
{ {
self.rpc self.rpc
.send(proto::AckChannelMessage { .send(proto::AckChannelMessage {
channel_id: self.channel.id, channel_id: self.channel_id,
message_id: latest_message_id, message_id: latest_message_id,
}) })
.ok(); .ok();
self.last_acknowledged_id = Some(latest_message_id); self.last_acknowledged_id = Some(latest_message_id);
self.channel_store.update(cx, |store, cx| { 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>) { pub fn rejoin(&mut self, cx: &mut ModelContext<Self>) {
let user_store = self.user_store.clone(); let user_store = self.user_store.clone();
let rpc = self.rpc.clone(); let rpc = self.rpc.clone();
let channel_id = self.channel.id; let channel_id = self.channel_id;
cx.spawn(|this, mut cx| { cx.spawn(|this, mut cx| {
async move { async move {
let response = rpc.request(proto::JoinChannelChat { channel_id }).await?; let response = rpc.request(proto::JoinChannelChat { channel_id }).await?;
@ -376,7 +379,7 @@ impl ChannelChat {
if self.acknowledged_message_ids.insert(id) { if self.acknowledged_message_ids.insert(id) {
self.rpc self.rpc
.send(proto::AckChannelMessage { .send(proto::AckChannelMessage {
channel_id: self.channel.id, channel_id: self.channel_id,
message_id: id, message_id: id,
}) })
.ok(); .ok();
@ -412,7 +415,7 @@ impl ChannelChat {
this.update(&mut cx, |this, cx| { this.update(&mut cx, |this, cx| {
this.insert_messages(SumTree::from_item(message, &()), cx); this.insert_messages(SumTree::from_item(message, &()), cx);
cx.emit(ChannelChatEvent::NewMessage { cx.emit(ChannelChatEvent::NewMessage {
channel_id: this.channel.id, channel_id: this.channel_id,
message_id, message_id,
}) })
}); });

View file

@ -9,11 +9,10 @@ use db::RELEASE_CHANNEL;
use futures::{channel::mpsc, future::Shared, Future, FutureExt, StreamExt}; use futures::{channel::mpsc, future::Shared, Future, FutureExt, StreamExt};
use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle}; use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle};
use rpc::{ use rpc::{
proto::{self, ChannelEdge, ChannelPermission, ChannelRole, ChannelVisibility}, proto::{self, ChannelVisibility},
TypedEnvelope, TypedEnvelope,
}; };
use serde_derive::{Deserialize, Serialize}; use std::{mem, sync::Arc, time::Duration};
use std::{borrow::Cow, hash::Hash, mem, ops::Deref, sync::Arc, time::Duration};
use util::ResultExt; use util::ResultExt;
pub fn init(client: &Arc<Client>, user_store: ModelHandle<UserStore>, cx: &mut AppContext) { 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 type ChannelId = u64;
pub struct ChannelStore { pub struct ChannelStore {
channel_index: ChannelIndex, pub channel_index: ChannelIndex,
channel_invitations: Vec<Arc<Channel>>, channel_invitations: Vec<Arc<Channel>>,
channel_participants: HashMap<ChannelId, Vec<Arc<User>>>, channel_participants: HashMap<ChannelId, Vec<Arc<User>>>,
channels_with_admin_privileges: HashSet<ChannelId>,
outgoing_invites: HashSet<(ChannelId, UserId)>, outgoing_invites: HashSet<(ChannelId, UserId)>,
update_channels_tx: mpsc::UnboundedSender<proto::UpdateChannels>, update_channels_tx: mpsc::UnboundedSender<proto::UpdateChannels>,
opened_buffers: HashMap<ChannelId, OpenedModelHandle<ChannelBuffer>>, opened_buffers: HashMap<ChannelId, OpenedModelHandle<ChannelBuffer>>,
@ -43,15 +41,15 @@ pub struct ChannelStore {
_update_channels: Task<()>, _update_channels: Task<()>,
} }
pub type ChannelData = (Channel, ChannelPath);
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub struct Channel { pub struct Channel {
pub id: ChannelId, pub id: ChannelId,
pub name: String, pub name: String,
pub visibility: proto::ChannelVisibility, pub visibility: proto::ChannelVisibility,
pub role: proto::ChannelRole,
pub unseen_note_version: Option<(u64, clock::Global)>, pub unseen_note_version: Option<(u64, clock::Global)>,
pub unseen_message_id: Option<u64>, pub unseen_message_id: Option<u64>,
pub parent_path: Vec<u64>,
} }
impl Channel { impl Channel {
@ -72,10 +70,11 @@ impl Channel {
slug.trim_matches(|c| c == '-').to_string() slug.trim_matches(|c| c == '-').to_string()
} }
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Serialize, Deserialize)] pub fn can_edit_notes(&self) -> bool {
pub struct ChannelPath(Arc<[ChannelId]>); self.role == proto::ChannelRole::Member || self.role == proto::ChannelRole::Admin
}
}
pub struct ChannelMembership { pub struct ChannelMembership {
pub user: Arc<User>, pub user: Arc<User>,
@ -161,7 +160,6 @@ impl ChannelStore {
channel_invitations: Vec::default(), channel_invitations: Vec::default(),
channel_index: ChannelIndex::default(), channel_index: ChannelIndex::default(),
channel_participants: Default::default(), channel_participants: Default::default(),
channels_with_admin_privileges: Default::default(),
outgoing_invites: Default::default(), outgoing_invites: Default::default(),
opened_buffers: Default::default(), opened_buffers: Default::default(),
opened_chats: Default::default(), opened_chats: Default::default(),
@ -190,16 +188,6 @@ impl ChannelStore {
self.client.clone() 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 /// Returns the number of unique channels in the store
pub fn channel_count(&self) -> usize { pub fn channel_count(&self) -> usize {
self.channel_index.by_id().len() self.channel_index.by_id().len()
@ -219,20 +207,19 @@ impl ChannelStore {
} }
/// Iterate over all entries in the channel DAG /// Iterate over all entries in the channel DAG
pub fn channel_dag_entries(&self) -> impl '_ + Iterator<Item = (usize, &Arc<Channel>)> { pub fn ordered_channels(&self) -> impl '_ + Iterator<Item = (usize, &Arc<Channel>)> {
self.channel_index.iter().map(move |path| { self.channel_index
let id = path.last().unwrap(); .ordered_channels()
let channel = self.channel_for_id(*id).unwrap(); .iter()
(path.len() - 1, channel) .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)> { pub fn channel_at_index(&self, ix: usize) -> Option<&Arc<Channel>> {
let path = self.channel_index.get(ix)?; let channel_id = self.channel_index.ordered_channels().get(ix)?;
let id = path.last().unwrap(); self.channel_index.by_id().get(channel_id)
let channel = self.channel_for_id(*id).unwrap();
Some((channel, path))
} }
pub fn channel_at(&self, ix: usize) -> Option<&Arc<Channel>> { pub fn channel_at(&self, ix: usize) -> Option<&Arc<Channel>> {
@ -269,10 +256,11 @@ impl ChannelStore {
) -> Task<Result<ModelHandle<ChannelBuffer>>> { ) -> Task<Result<ModelHandle<ChannelBuffer>>> {
let client = self.client.clone(); let client = self.client.clone();
let user_store = self.user_store.clone(); let user_store = self.user_store.clone();
let channel_store = cx.handle();
self.open_channel_resource( self.open_channel_resource(
channel_id, channel_id,
|this| &mut this.opened_buffers, |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, cx,
) )
} }
@ -449,16 +437,11 @@ impl ChannelStore {
.spawn(async move { task.await.map_err(|error| anyhow!("{}", error)) }) .spawn(async move { task.await.map_err(|error| anyhow!("{}", error)) })
} }
pub fn is_user_admin(&self, channel_id: ChannelId) -> bool { pub fn is_channel_admin(&self, channel_id: ChannelId) -> bool {
self.channel_index.iter().any(|path| { let Some(channel) = self.channel_for_id(channel_id) else {
if let Some(ix) = path.iter().position(|id| *id == channel_id) { return false;
path[..=ix] };
.iter() channel.role == proto::ChannelRole::Admin
.any(|id| self.channels_with_admin_privileges.contains(id))
} else {
false
}
})
} }
pub fn channel_participants(&self, channel_id: ChannelId) -> &[Arc<User>] { pub fn channel_participants(&self, channel_id: ChannelId) -> &[Arc<User>] {
@ -485,24 +468,19 @@ impl ChannelStore {
.ok_or_else(|| anyhow!("missing channel in response"))?; .ok_or_else(|| anyhow!("missing channel in response"))?;
let channel_id = channel.id; let channel_id = channel.id;
let parent_edge = if let Some(parent_id) = parent_id { // let parent_edge = if let Some(parent_id) = parent_id {
vec![ChannelEdge { // vec![ChannelEdge {
channel_id: channel.id, // channel_id: channel.id,
parent_id, // parent_id,
}] // }]
} else { // } else {
vec![] // vec![]
}; // };
this.update(&mut cx, |this, cx| { this.update(&mut cx, |this, cx| {
let task = this.update_channels( let task = this.update_channels(
proto::UpdateChannels { proto::UpdateChannels {
channels: vec![channel], channels: vec![channel],
insert_edge: parent_edge,
channel_permissions: vec![ChannelPermission {
channel_id,
role: ChannelRole::Admin.into(),
}],
..Default::default() ..Default::default()
}, },
cx, 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( pub fn move_channel(
&mut self, &mut self,
channel_id: ChannelId, channel_id: ChannelId,
from: ChannelId, to: Option<ChannelId>,
to: ChannelId,
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) -> Task<Result<()>> { ) -> Task<Result<()>> {
let client = self.client.clone(); let client = self.client.clone();
cx.spawn(|_, _| async move { cx.spawn(|_, _| async move {
let _ = client let _ = client
.request(proto::MoveChannel { .request(proto::MoveChannel { channel_id, to })
channel_id,
from,
to,
})
.await?; .await?;
Ok(()) Ok(())
@ -800,6 +741,11 @@ impl ChannelStore {
} }
fn handle_connect(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> { 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(); self.disconnect_channel_buffers_task.take();
for chat in self.opened_chats.values() { for chat in self.opened_chats.values() {
@ -819,7 +765,7 @@ impl ChannelStore {
let channel_buffer = buffer.read(cx); let channel_buffer = buffer.read(cx);
let buffer = channel_buffer.buffer().read(cx); let buffer = channel_buffer.buffer().read(cx);
buffer_versions.push(proto::ChannelBufferVersion { buffer_versions.push(proto::ChannelBufferVersion {
channel_id: channel_buffer.channel().id, channel_id: channel_buffer.channel_id,
epoch: channel_buffer.epoch(), epoch: channel_buffer.epoch(),
version: language::proto::serialize_version(&buffer.version()), version: language::proto::serialize_version(&buffer.version()),
}); });
@ -846,13 +792,13 @@ impl ChannelStore {
}; };
channel_buffer.update(cx, |channel_buffer, cx| { 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 if let Some(remote_buffer) = response
.buffers .buffers
.iter_mut() .iter_mut()
.find(|buffer| buffer.channel_id == channel_id) .find(|buffer| buffer.channel_id == channel_id)
{ {
let channel_id = channel_buffer.channel().id; let channel_id = channel_buffer.channel_id;
let remote_version = let remote_version =
language::proto::deserialize_version(&remote_buffer.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>) { 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(); cx.notify();
self.disconnect_channel_buffers_task.get_or_insert_with(|| { self.disconnect_channel_buffers_task.get_or_insert_with(|| {
@ -958,9 +898,11 @@ impl ChannelStore {
Arc::new(Channel { Arc::new(Channel {
id: channel.id, id: channel.id,
visibility: channel.visibility(), visibility: channel.visibility(),
role: channel.role(),
name: channel.name, name: channel.name,
unseen_note_version: None, unseen_note_version: None,
unseen_message_id: None, unseen_message_id: None,
parent_path: channel.parent_path,
}), }),
), ),
} }
@ -968,8 +910,6 @@ impl ChannelStore {
let channels_changed = !payload.channels.is_empty() let channels_changed = !payload.channels.is_empty()
|| !payload.delete_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_messages.is_empty()
|| !payload.unseen_channel_buffer_changes.is_empty(); || !payload.unseen_channel_buffer_changes.is_empty();
@ -977,12 +917,17 @@ impl ChannelStore {
if !payload.delete_channels.is_empty() { if !payload.delete_channels.is_empty() {
self.channel_index.delete_channels(&payload.delete_channels); self.channel_index.delete_channels(&payload.delete_channels);
self.channel_participants self.channel_participants
.retain(|channel_id, _| !payload.delete_channels.contains(channel_id)); .retain(|channel_id, _| !&payload.delete_channels.contains(channel_id));
self.channels_with_admin_privileges
.retain(|channel_id| !payload.delete_channels.contains(channel_id));
for channel_id in &payload.delete_channels { for channel_id in &payload.delete_channels {
let channel_id = *channel_id; let channel_id = *channel_id;
if payload
.channels
.iter()
.any(|channel| channel.id == channel_id)
{
continue;
}
if let Some(OpenedModelHandle::Open(buffer)) = if let Some(OpenedModelHandle::Open(buffer)) =
self.opened_buffers.remove(&channel_id) self.opened_buffers.remove(&channel_id)
{ {
@ -995,7 +940,16 @@ impl ChannelStore {
let mut index = self.channel_index.bulk_insert(); let mut index = self.channel_index.bulk_insert();
for channel in payload.channels { 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 { for unseen_buffer_change in payload.unseen_channel_buffer_changes {
@ -1013,24 +967,6 @@ impl ChannelStore {
unseen_channel_message.message_id, 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(); 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([]))
}
}

View file

@ -1,14 +1,11 @@
use std::{ops::Deref, sync::Arc};
use crate::{Channel, ChannelId}; use crate::{Channel, ChannelId};
use collections::BTreeMap; use collections::BTreeMap;
use rpc::proto; use rpc::proto;
use std::sync::Arc;
use super::ChannelPath;
#[derive(Default, Debug)] #[derive(Default, Debug)]
pub struct ChannelIndex { pub struct ChannelIndex {
paths: Vec<ChannelPath>, channels_ordered: Vec<ChannelId>,
channels_by_id: BTreeMap<ChannelId, Arc<Channel>>, channels_by_id: BTreeMap<ChannelId, Arc<Channel>>,
} }
@ -17,8 +14,12 @@ impl ChannelIndex {
&self.channels_by_id &self.channels_by_id
} }
pub fn ordered_channels(&self) -> &[ChannelId] {
&self.channels_ordered
}
pub fn clear(&mut self) { pub fn clear(&mut self) {
self.paths.clear(); self.channels_ordered.clear();
self.channels_by_id.clear(); self.channels_by_id.clear();
} }
@ -26,15 +27,13 @@ impl ChannelIndex {
pub fn delete_channels(&mut self, channels: &[ChannelId]) { pub fn delete_channels(&mut self, channels: &[ChannelId]) {
self.channels_by_id self.channels_by_id
.retain(|channel_id, _| !channels.contains(channel_id)); .retain(|channel_id, _| !channels.contains(channel_id));
self.paths.retain(|path| { self.channels_ordered
path.iter() .retain(|channel_id| !channels.contains(channel_id));
.all(|channel_id| self.channels_by_id.contains_key(channel_id))
});
} }
pub fn bulk_insert(&mut self) -> ChannelPathsInsertGuard { pub fn bulk_insert(&mut self) -> ChannelPathsInsertGuard {
ChannelPathsInsertGuard { ChannelPathsInsertGuard {
paths: &mut self.paths, channels_ordered: &mut self.channels_ordered,
channels_by_id: &mut self.channels_by_id, 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 /// A guard for ensuring that the paths index maintains its sort and uniqueness
/// invariants after a series of insertions /// invariants after a series of insertions
#[derive(Debug)] #[derive(Debug)]
pub struct ChannelPathsInsertGuard<'a> { pub struct ChannelPathsInsertGuard<'a> {
paths: &'a mut Vec<ChannelPath>, channels_ordered: &'a mut Vec<ChannelId>,
channels_by_id: &'a mut BTreeMap<ChannelId, Arc<Channel>>, channels_by_id: &'a mut BTreeMap<ChannelId, Arc<Channel>>,
} }
impl<'a> ChannelPathsInsertGuard<'a> { 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) { 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); 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) 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) { if let Some(existing_channel) = self.channels_by_id.get_mut(&channel_proto.id) {
let existing_channel = Arc::make_mut(existing_channel); 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.visibility = channel_proto.visibility();
existing_channel.role = channel_proto.role();
existing_channel.name = channel_proto.name; existing_channel.name = channel_proto.name;
} else { } else {
self.channels_by_id.insert( self.channels_by_id.insert(
@ -132,83 +111,47 @@ impl<'a> ChannelPathsInsertGuard<'a> {
Arc::new(Channel { Arc::new(Channel {
id: channel_proto.id, id: channel_proto.id,
visibility: channel_proto.visibility(), visibility: channel_proto.visibility(),
role: channel_proto.role(),
name: channel_proto.name, name: channel_proto.name,
unseen_note_version: None, unseen_note_version: None,
unseen_message_id: None, unseen_message_id: None,
parent_path: channel_proto.parent_path,
}), }),
); );
self.insert_root(channel_proto.id); self.insert_root(channel_proto.id);
} }
} ret
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)
} }
fn insert_root(&mut self, channel_id: ChannelId) { 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> { impl<'a> Drop for ChannelPathsInsertGuard<'a> {
fn drop(&mut self) { fn drop(&mut self) {
self.paths.sort_by(|a, b| { self.channels_ordered.sort_by(|a, b| {
let a = channel_path_sorting_key(a, &self.channels_by_id); let a = channel_path_sorting_key(*a, &self.channels_by_id);
let b = channel_path_sorting_key(b, &self.channels_by_id); let b = channel_path_sorting_key(*b, &self.channels_by_id);
a.cmp(b) a.cmp(b)
}); });
self.paths.dedup(); self.channels_ordered.dedup();
} }
} }
fn channel_path_sorting_key<'a>( fn channel_path_sorting_key<'a>(
path: &'a [ChannelId], id: ChannelId,
channels_by_id: &'a BTreeMap<ChannelId, Arc<Channel>>, channels_by_id: &'a BTreeMap<ChannelId, Arc<Channel>>,
) -> impl 'a + Iterator<Item = Option<&'a str>> { ) -> impl Iterator<Item = &str> {
path.iter() let (parent_path, name) = channels_by_id
.map(|id| Some(channels_by_id.get(id)?.name.as_str())) .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( fn insert_note_changed(

View file

@ -19,17 +19,17 @@ fn test_update_channels(cx: &mut AppContext) {
id: 1, id: 1,
name: "b".to_string(), name: "b".to_string(),
visibility: proto::ChannelVisibility::Members as i32, visibility: proto::ChannelVisibility::Members as i32,
role: proto::ChannelRole::Admin.into(),
parent_path: Vec::new(),
}, },
proto::Channel { proto::Channel {
id: 2, id: 2,
name: "a".to_string(), name: "a".to_string(),
visibility: proto::ChannelVisibility::Members as i32, 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() ..Default::default()
}, },
cx, cx,
@ -38,8 +38,8 @@ fn test_update_channels(cx: &mut AppContext) {
&channel_store, &channel_store,
&[ &[
// //
(0, "a".to_string(), false), (0, "a".to_string(), proto::ChannelRole::Member),
(0, "b".to_string(), true), (0, "b".to_string(), proto::ChannelRole::Admin),
], ],
cx, cx,
); );
@ -52,21 +52,15 @@ fn test_update_channels(cx: &mut AppContext) {
id: 3, id: 3,
name: "x".to_string(), name: "x".to_string(),
visibility: proto::ChannelVisibility::Members as i32, visibility: proto::ChannelVisibility::Members as i32,
role: proto::ChannelRole::Admin.into(),
parent_path: vec![1],
}, },
proto::Channel { proto::Channel {
id: 4, id: 4,
name: "y".to_string(), name: "y".to_string(),
visibility: proto::ChannelVisibility::Members as i32, visibility: proto::ChannelVisibility::Members as i32,
}, role: proto::ChannelRole::Member.into(),
], parent_path: vec![2],
insert_edge: vec![
proto::ChannelEdge {
parent_id: 1,
channel_id: 3,
},
proto::ChannelEdge {
parent_id: 2,
channel_id: 4,
}, },
], ],
..Default::default() ..Default::default()
@ -76,10 +70,10 @@ fn test_update_channels(cx: &mut AppContext) {
assert_channels( assert_channels(
&channel_store, &channel_store,
&[ &[
(0, "a".to_string(), false), (0, "a".to_string(), proto::ChannelRole::Member),
(1, "y".to_string(), false), (1, "y".to_string(), proto::ChannelRole::Member),
(0, "b".to_string(), true), (0, "b".to_string(), proto::ChannelRole::Admin),
(1, "x".to_string(), true), (1, "x".to_string(), proto::ChannelRole::Admin),
], ],
cx, cx,
); );
@ -97,32 +91,24 @@ fn test_dangling_channel_paths(cx: &mut AppContext) {
id: 0, id: 0,
name: "a".to_string(), name: "a".to_string(),
visibility: proto::ChannelVisibility::Members as i32, visibility: proto::ChannelVisibility::Members as i32,
role: proto::ChannelRole::Admin.into(),
parent_path: vec![],
}, },
proto::Channel { proto::Channel {
id: 1, id: 1,
name: "b".to_string(), name: "b".to_string(),
visibility: proto::ChannelVisibility::Members as i32, visibility: proto::ChannelVisibility::Members as i32,
role: proto::ChannelRole::Admin.into(),
parent_path: vec![0],
}, },
proto::Channel { proto::Channel {
id: 2, id: 2,
name: "c".to_string(), name: "c".to_string(),
visibility: proto::ChannelVisibility::Members as i32, 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() ..Default::default()
}, },
cx, cx,
@ -132,9 +118,9 @@ fn test_dangling_channel_paths(cx: &mut AppContext) {
&channel_store, &channel_store,
&[ &[
// //
(0, "a".to_string(), true), (0, "a".to_string(), proto::ChannelRole::Admin),
(1, "b".to_string(), true), (1, "b".to_string(), proto::ChannelRole::Admin),
(2, "c".to_string(), true), (2, "c".to_string(), proto::ChannelRole::Admin),
], ],
cx, cx,
); );
@ -149,7 +135,11 @@ fn test_dangling_channel_paths(cx: &mut AppContext) {
); );
// Make sure that the 1/2/3 path is gone // 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] #[gpui::test]
@ -166,12 +156,18 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
id: channel_id, id: channel_id,
name: "the-channel".to_string(), name: "the-channel".to_string(),
visibility: proto::ChannelVisibility::Members as i32, visibility: proto::ChannelVisibility::Members as i32,
role: proto::ChannelRole::Member.into(),
parent_path: vec![],
}], }],
..Default::default() ..Default::default()
}); });
cx.foreground().run_until_parked(); cx.foreground().run_until_parked();
cx.read(|cx| { 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(); 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. // Join a channel and populate its existing messages.
let channel = channel_store.update(cx, |store, cx| { 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) store.open_channel_chat(channel_id, cx)
}); });
let join_channel = server.receive::<proto::JoinChannelChat>().await.unwrap(); let join_channel = server.receive::<proto::JoinChannelChat>().await.unwrap();
@ -371,19 +367,13 @@ fn update_channels(
#[track_caller] #[track_caller]
fn assert_channels( fn assert_channels(
channel_store: &ModelHandle<ChannelStore>, channel_store: &ModelHandle<ChannelStore>,
expected_channels: &[(usize, String, bool)], expected_channels: &[(usize, String, proto::ChannelRole)],
cx: &AppContext, cx: &AppContext,
) { ) {
let actual = channel_store.read_with(cx, |store, _| { let actual = channel_store.read_with(cx, |store, _| {
store store
.channel_dag_entries() .ordered_channels()
.map(|(depth, channel)| { .map(|(depth, channel)| (depth, channel.name.to_string(), channel.role))
(
depth,
channel.name.to_string(),
store.is_user_admin(channel.id),
)
})
.collect::<Vec<_>>() .collect::<Vec<_>>()
}); });
assert_eq!(actual, expected_channels); assert_eq!(actual, expected_channels);

View file

@ -193,9 +193,12 @@ CREATE TABLE "channels" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT, "id" INTEGER PRIMARY KEY AUTOINCREMENT,
"name" VARCHAR NOT NULL, "name" VARCHAR NOT NULL,
"created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, "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" ( CREATE TABLE IF NOT EXISTS "channel_chat_participants" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT, "id" INTEGER PRIMARY KEY AUTOINCREMENT,
"user_id" INTEGER NOT NULL REFERENCES users (id), "user_id" INTEGER NOT NULL REFERENCES users (id),
@ -224,12 +227,6 @@ CREATE TABLE "channel_message_mentions" (
PRIMARY KEY(message_id, start_offset) 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" ( CREATE TABLE "channel_members" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT, "id" INTEGER PRIMARY KEY AUTOINCREMENT,
"channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE, "channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE,

View file

@ -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");

View file

@ -13,7 +13,6 @@ use anyhow::anyhow;
use collections::{BTreeMap, HashMap, HashSet}; use collections::{BTreeMap, HashMap, HashSet};
use dashmap::DashMap; use dashmap::DashMap;
use futures::StreamExt; use futures::StreamExt;
use queries::channels::ChannelGraph;
use rand::{prelude::StdRng, Rng, SeedableRng}; use rand::{prelude::StdRng, Rng, SeedableRng};
use rpc::{ use rpc::{
proto::{self}, proto::{self},
@ -435,18 +434,115 @@ pub struct NewUserResult {
pub signup_device_id: Option<String>, 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 struct Channel {
pub id: ChannelId, pub id: ChannelId,
pub name: String, pub name: String,
pub visibility: ChannelVisibility, 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)] #[derive(Debug, PartialEq)]
pub struct ChannelsForUser { pub struct ChannelsForUser {
pub channels: ChannelGraph, pub channels: Vec<Channel>,
pub channel_participants: HashMap<ChannelId, Vec<UserId>>, pub channel_participants: HashMap<ChannelId, Vec<UserId>>,
pub channels_with_admin_privileges: HashSet<ChannelId>,
pub unseen_buffer_changes: Vec<proto::UnseenChannelBufferChange>, pub unseen_buffer_changes: Vec<proto::UnseenChannelBufferChange>,
pub channel_messages: Vec<proto::UnseenChannelMessage>, pub channel_messages: Vec<proto::UnseenChannelMessage>,
} }

View file

@ -84,7 +84,7 @@ id_type!(FlagId);
id_type!(NotificationId); id_type!(NotificationId);
id_type!(NotificationKindId); 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)")] #[sea_orm(rs_type = "String", db_type = "String(None)")]
pub enum ChannelRole { pub enum ChannelRole {
#[sea_orm(string_value = "admin")] #[sea_orm(string_value = "admin")]
@ -116,6 +116,22 @@ impl ChannelRole {
other 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 { impl From<proto::ChannelRole> for ChannelRole {

View file

@ -16,7 +16,8 @@ impl Database {
connection: ConnectionId, connection: ConnectionId,
) -> Result<proto::JoinChannelBufferResponse> { ) -> Result<proto::JoinChannelBufferResponse> {
self.transaction(|tx| async move { 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?; .await?;
let buffer = channel::Model { let buffer = channel::Model {
@ -129,9 +130,11 @@ impl Database {
self.transaction(|tx| async move { self.transaction(|tx| async move {
let mut results = Vec::new(); let mut results = Vec::new();
for client_buffer in buffers { 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 if self
.check_user_is_channel_member(channel_id, user_id, &*tx) .check_user_is_channel_participant(&channel, user_id, &*tx)
.await .await
.is_err() .is_err()
{ {
@ -139,9 +142,9 @@ impl Database {
continue; 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() 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) .all(&*tx)
.await?; .await?;
@ -439,7 +442,8 @@ impl Database {
Vec<proto::VectorClockEntry>, Vec<proto::VectorClockEntry>,
)> { )> {
self.transaction(move |tx| async move { 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?; .await?;
let buffer = buffer::Entity::find() let buffer = buffer::Entity::find()
@ -482,9 +486,7 @@ impl Database {
) )
.await?; .await?;
channel_members = self channel_members = self.get_channel_participants(&channel, &*tx).await?;
.get_channel_participants_internal(channel_id, &*tx)
.await?;
let collaborators = self let collaborators = self
.get_channel_buffer_collaborators_internal(channel_id, &*tx) .get_channel_buffer_collaborators_internal(channel_id, &*tx)
.await?; .await?;

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,4 @@
use super::*; use super::*;
use futures::Stream;
use rpc::Notification; use rpc::Notification;
use sea_orm::TryInsertResult; use sea_orm::TryInsertResult;
use time::OffsetDateTime; use time::OffsetDateTime;
@ -12,7 +11,8 @@ impl Database {
user_id: UserId, user_id: UserId,
) -> Result<()> { ) -> Result<()> {
self.transaction(|tx| async move { 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?; .await?;
channel_chat_participant::ActiveModel { channel_chat_participant::ActiveModel {
id: ActiveValue::NotSet, id: ActiveValue::NotSet,
@ -80,7 +80,8 @@ impl Database {
before_message_id: Option<MessageId>, before_message_id: Option<MessageId>,
) -> Result<Vec<proto::ChannelMessage>> { ) -> Result<Vec<proto::ChannelMessage>> {
self.transaction(|tx| async move { 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?; .await?;
let mut condition = let mut condition =
@ -94,7 +95,7 @@ impl Database {
.filter(condition) .filter(condition)
.order_by_desc(channel_message::Column::Id) .order_by_desc(channel_message::Column::Id)
.limit(count as u64) .limit(count as u64)
.stream(&*tx) .all(&*tx)
.await?; .await?;
self.load_channel_messages(rows, &*tx).await self.load_channel_messages(rows, &*tx).await
@ -111,27 +112,23 @@ impl Database {
let rows = channel_message::Entity::find() let rows = channel_message::Entity::find()
.filter(channel_message::Column::Id.is_in(message_ids.iter().copied())) .filter(channel_message::Column::Id.is_in(message_ids.iter().copied()))
.order_by_desc(channel_message::Column::Id) .order_by_desc(channel_message::Column::Id)
.stream(&*tx) .all(&*tx)
.await?; .await?;
let mut channel_ids = HashSet::<ChannelId>::default(); let mut channels = HashMap::<ChannelId, channel::Model>::default();
let messages = self for row in &rows {
.load_channel_messages( channels.insert(
rows.map(|row| { row.channel_id,
row.map(|row| { self.get_channel_internal(row.channel_id, &*tx).await?,
channel_ids.insert(row.channel_id); );
row }
})
}),
&*tx,
)
.await?;
for channel_id in channel_ids { for (_, channel) in channels {
self.check_user_is_channel_member(channel_id, user_id, &*tx) self.check_user_is_channel_participant(&channel, user_id, &*tx)
.await?; .await?;
} }
let messages = self.load_channel_messages(rows, &*tx).await?;
Ok(messages) Ok(messages)
}) })
.await .await
@ -139,26 +136,26 @@ impl Database {
async fn load_channel_messages( async fn load_channel_messages(
&self, &self,
mut rows: impl Send + Unpin + Stream<Item = Result<channel_message::Model, sea_orm::DbErr>>, rows: Vec<channel_message::Model>,
tx: &DatabaseTransaction, tx: &DatabaseTransaction,
) -> Result<Vec<proto::ChannelMessage>> { ) -> Result<Vec<proto::ChannelMessage>> {
let mut messages = Vec::new(); let mut messages = rows
while let Some(row) = rows.next().await { .into_iter()
let row = row?; .map(|row| {
let nonce = row.nonce.as_u64_pair(); let nonce = row.nonce.as_u64_pair();
messages.push(proto::ChannelMessage { proto::ChannelMessage {
id: row.id.to_proto(), id: row.id.to_proto(),
sender_id: row.sender_id.to_proto(), sender_id: row.sender_id.to_proto(),
body: row.body, body: row.body,
timestamp: row.sent_at.assume_utc().unix_timestamp() as u64, timestamp: row.sent_at.assume_utc().unix_timestamp() as u64,
mentions: vec![], mentions: vec![],
nonce: Some(proto::Nonce { nonce: Some(proto::Nonce {
upper_half: nonce.0, upper_half: nonce.0,
lower_half: nonce.1, lower_half: nonce.1,
}), }),
}); }
} })
drop(rows); .collect::<Vec<_>>();
messages.reverse(); messages.reverse();
let mut mentions = channel_message_mention::Entity::find() let mut mentions = channel_message_mention::Entity::find()
@ -203,6 +200,10 @@ impl Database {
nonce: u128, nonce: u128,
) -> Result<CreatedChannelMessage> { ) -> Result<CreatedChannelMessage> {
self.transaction(|tx| async move { 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() let mut rows = channel_chat_participant::Entity::find()
.filter(channel_chat_participant::Column::ChannelId.eq(channel_id)) .filter(channel_chat_participant::Column::ChannelId.eq(channel_id))
.stream(&*tx) .stream(&*tx)
@ -307,9 +308,7 @@ impl Database {
} }
} }
let mut channel_members = self let mut channel_members = self.get_channel_participants(&channel, &*tx).await?;
.get_channel_participants_internal(channel_id, &*tx)
.await?;
channel_members.retain(|member| !participant_user_ids.contains(member)); channel_members.retain(|member| !participant_user_ids.contains(member));
Ok(CreatedChannelMessage { Ok(CreatedChannelMessage {
@ -482,8 +481,9 @@ impl Database {
.await?; .await?;
if result.rows_affected == 0 { if result.rows_affected == 0 {
let channel = self.get_channel_internal(channel_id, &*tx).await?;
if self if self
.check_user_is_channel_admin(channel_id, user_id, &*tx) .check_user_is_channel_admin(&channel, user_id, &*tx)
.await .await
.is_ok() .is_ok()
{ {

View file

@ -50,12 +50,10 @@ impl Database {
.map(|participant| participant.user_id), .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; let channel_members;
if let Some(channel_id) = channel_id { if let Some(channel) = &channel {
channel_members = self channel_members = self.get_channel_participants(channel, &tx).await?;
.get_channel_participants_internal(channel_id, &tx)
.await?;
} else { } else {
channel_members = Vec::new(); channel_members = Vec::new();
@ -71,7 +69,7 @@ impl Database {
Ok(RefreshedRoom { Ok(RefreshedRoom {
room, room,
channel_id, channel_id: channel.map(|channel| channel.id),
channel_members, channel_members,
stale_participant_user_ids, stale_participant_user_ids,
canceled_calls_to_user_ids, canceled_calls_to_user_ids,
@ -383,7 +381,6 @@ impl Database {
pub(crate) async fn join_channel_room_internal( pub(crate) async fn join_channel_room_internal(
&self, &self,
channel_id: ChannelId,
room_id: RoomId, room_id: RoomId,
user_id: UserId, user_id: UserId,
connection: ConnectionId, connection: ConnectionId,
@ -422,13 +419,12 @@ impl Database {
.exec(&*tx) .exec(&*tx)
.await?; .await?;
let room = self.get_room(room_id, &tx).await?; let (channel, room) = self.get_channel_room(room_id, &tx).await?;
let channel_members = self let channel = channel.ok_or_else(|| anyhow!("no channel for room"))?;
.get_channel_participants_internal(channel_id, &tx) let channel_members = self.get_channel_participants(&channel, &*tx).await?;
.await?;
Ok(JoinRoom { Ok(JoinRoom {
room, room,
channel_id: Some(channel_id), channel_id: Some(channel.id),
channel_members, channel_members,
}) })
} }
@ -722,17 +718,16 @@ impl Database {
}); });
} }
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 { let channel_members = if let Some(channel) = &channel {
self.get_channel_participants_internal(channel_id, &tx) self.get_channel_participants(&channel, &tx).await?
.await?
} else { } else {
Vec::new() Vec::new()
}; };
Ok(RejoinedRoom { Ok(RejoinedRoom {
room, room,
channel_id, channel_id: channel.map(|channel| channel.id),
channel_members, channel_members,
rejoined_projects, rejoined_projects,
reshared_projects, reshared_projects,
@ -874,7 +869,7 @@ impl Database {
.exec(&*tx) .exec(&*tx)
.await?; .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 deleted = if room.participants.is_empty() {
let result = room::Entity::delete_by_id(room_id).exec(&*tx).await?; let result = room::Entity::delete_by_id(room_id).exec(&*tx).await?;
result.rows_affected > 0 result.rows_affected > 0
@ -882,15 +877,14 @@ impl Database {
false false
}; };
let channel_members = if let Some(channel_id) = channel_id { let channel_members = if let Some(channel) = &channel {
self.get_channel_participants_internal(channel_id, &tx) self.get_channel_participants(channel, &tx).await?
.await?
} else { } else {
Vec::new() Vec::new()
}; };
let left_room = LeftRoom { let left_room = LeftRoom {
room, room,
channel_id, channel_id: channel.map(|channel| channel.id),
channel_members, channel_members,
left_projects, left_projects,
canceled_calls_to_user_ids, canceled_calls_to_user_ids,
@ -1078,7 +1072,7 @@ impl Database {
&self, &self,
room_id: RoomId, room_id: RoomId,
tx: &DatabaseTransaction, tx: &DatabaseTransaction,
) -> Result<(Option<ChannelId>, proto::Room)> { ) -> Result<(Option<channel::Model>, proto::Room)> {
let db_room = room::Entity::find_by_id(room_id) let db_room = room::Entity::find_by_id(room_id)
.one(tx) .one(tx)
.await? .await?
@ -1187,9 +1181,16 @@ impl Database {
project_id: db_follower.project_id.to_proto(), 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(( Ok((
db_room.channel_id, channel,
proto::Room { proto::Room {
id: db_room.id.to_proto(), id: db_room.id.to_proto(),
live_kit_room: db_room.live_kit_room, live_kit_room: db_room.live_kit_room,

View file

@ -8,7 +8,6 @@ pub mod channel_chat_participant;
pub mod channel_member; pub mod channel_member;
pub mod channel_message; pub mod channel_message;
pub mod channel_message_mention; pub mod channel_message_mention;
pub mod channel_path;
pub mod contact; pub mod contact;
pub mod feature_flag; pub mod feature_flag;
pub mod follower; pub mod follower;

View file

@ -8,6 +8,28 @@ pub struct Model {
pub id: ChannelId, pub id: ChannelId,
pub name: String, pub name: String,
pub visibility: ChannelVisibility, 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 {} impl ActiveModelBehavior for ActiveModel {}

View file

@ -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 {}

View file

@ -7,11 +7,10 @@ mod message_tests;
use super::*; use super::*;
use gpui::executor::Background; use gpui::executor::Background;
use parking_lot::Mutex; use parking_lot::Mutex;
use rpc::proto::ChannelEdge;
use sea_orm::ConnectionTrait; use sea_orm::ConnectionTrait;
use sqlx::migrate::MigrateDatabase; use sqlx::migrate::MigrateDatabase;
use std::sync::{ use std::sync::{
atomic::{AtomicI32, Ordering::SeqCst}, atomic::{AtomicI32, AtomicU32, Ordering::SeqCst},
Arc, Arc,
}; };
@ -153,29 +152,17 @@ impl Drop for TestDb {
} }
} }
/// The second tuples are (channel_id, parent) fn channel_tree(channels: &[(ChannelId, &[ChannelId], &'static str, ChannelRole)]) -> Vec<Channel> {
fn graph(channels: &[(ChannelId, &'static str)], edges: &[(ChannelId, ChannelId)]) -> ChannelGraph { channels
let mut graph = ChannelGraph { .iter()
channels: vec![], .map(|(id, parent_path, name, role)| Channel {
edges: vec![],
};
for (id, name) in channels {
graph.channels.push(Channel {
id: *id, id: *id,
name: name.to_string(), name: name.to_string(),
visibility: ChannelVisibility::Members, visibility: ChannelVisibility::Members,
role: *role,
parent_path: parent_path.to_vec(),
}) })
} .collect()
for (channel, parent) in edges {
graph.edges.push(ChannelEdge {
channel_id: channel.to_proto(),
parent_id: parent.to_proto(),
})
}
graph
} }
static GITHUB_USER_ID: AtomicI32 = AtomicI32::new(5); static GITHUB_USER_ID: AtomicI32 = AtomicI32::new(5);
@ -193,3 +180,11 @@ async fn new_test_user(db: &Arc<Database>, email: &str) -> UserId {
.unwrap() .unwrap()
.user_id .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,
}
}

File diff suppressed because it is too large Load diff

View file

@ -15,18 +15,22 @@ test_both_dbs!(
async fn test_channel_message_retrieval(db: &Arc<Database>) { async fn test_channel_message_retrieval(db: &Arc<Database>) {
let user = new_test_user(db, "user@example.com").await; 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; let owner_id = db.create_server("test").await.unwrap().0 as u32;
db.join_channel_chat(channel, rpc::ConnectionId { owner_id, id: 0 }, user) db.join_channel_chat(
.await result.channel.id,
.unwrap(); rpc::ConnectionId { owner_id, id: 0 },
user,
)
.await
.unwrap();
let mut all_messages = Vec::new(); let mut all_messages = Vec::new();
for i in 0..10 { for i in 0..10 {
all_messages.push( all_messages.push(
db.create_channel_message( db.create_channel_message(
channel, result.channel.id,
user, user,
&i.to_string(), &i.to_string(),
&[], &[],
@ -41,7 +45,7 @@ async fn test_channel_message_retrieval(db: &Arc<Database>) {
} }
let messages = db let messages = db
.get_channel_messages(channel, user, 3, None) .get_channel_messages(result.channel.id, user, 3, None)
.await .await
.unwrap() .unwrap()
.into_iter() .into_iter()
@ -51,7 +55,7 @@ async fn test_channel_message_retrieval(db: &Arc<Database>) {
let messages = db let messages = db
.get_channel_messages( .get_channel_messages(
channel, result.channel.id,
user, user,
4, 4,
Some(MessageId::from_proto(all_messages[6])), 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_a = new_test_user(db, "user_a@example.com").await;
let user_b = new_test_user(db, "user_b@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 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) db.invite_channel_member(channel, user_b, user_a, ChannelRole::Member)
.await .await
.unwrap(); .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 user = new_test_user(db, "user_a@example.com").await;
let observer = new_test_user(db, "user_b@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_1 = db.create_root_channel("channel", user).await.unwrap();
let channel_2 = db.create_channel("channel-2", None, 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) db.invite_channel_member(channel_1, observer, user, ChannelRole::Member)
.await .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_b = new_test_user(db, "user_b@example.com").await;
let user_c = new_test_user(db, "user_c@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) db.invite_channel_member(channel, user_b, user_a, ChannelRole::Member)
.await .await
.unwrap(); .unwrap();

View file

@ -3,8 +3,11 @@ mod connection_pool;
use crate::{ use crate::{
auth, auth,
db::{ db::{
self, BufferId, ChannelId, ChannelVisibility, ChannelsForUser, CreatedChannelMessage, self, BufferId, ChannelId, ChannelRole, ChannelsForUser, CreateChannelResult,
Database, MessageId, NotificationId, ProjectId, RoomId, ServerId, User, UserId, CreatedChannelMessage, Database, InviteMemberResult, MembershipUpdated, MessageId,
MoveChannelResult, NotificationId, ProjectId, RemoveChannelMemberResult,
RenameChannelResult, RespondToChannelInvite, RoomId, ServerId, SetChannelVisibilityResult,
User, UserId,
}, },
executor::Executor, executor::Executor,
AppState, Result, AppState, Result,
@ -38,8 +41,8 @@ use lazy_static::lazy_static;
use prometheus::{register_int_gauge, IntGauge}; use prometheus::{register_int_gauge, IntGauge};
use rpc::{ use rpc::{
proto::{ proto::{
self, Ack, AnyTypedEnvelope, ChannelEdge, EntityMessage, EnvelopedMessage, self, Ack, AnyTypedEnvelope, EntityMessage, EnvelopedMessage, LiveKitConnectionInfo,
LiveKitConnectionInfo, RequestMessage, UpdateChannelBufferCollaborators, RequestMessage, UpdateChannelBufferCollaborators,
}, },
Connection, ConnectionId, Peer, Receipt, TypedEnvelope, Connection, ConnectionId, Peer, Receipt, TypedEnvelope,
}; };
@ -274,8 +277,6 @@ impl Server {
.add_request_handler(get_channel_messages_by_id) .add_request_handler(get_channel_messages_by_id)
.add_request_handler(get_notifications) .add_request_handler(get_notifications)
.add_request_handler(mark_notification_as_read) .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(move_channel)
.add_request_handler(follow) .add_request_handler(follow)
.add_message_handler(unfollow) .add_message_handler(unfollow)
@ -594,7 +595,7 @@ impl Server {
let mut pool = this.connection_pool.lock(); let mut pool = this.connection_pool.lock();
pool.add_connection(connection_id, user_id, user.admin); 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_contacts_update(contacts, &pool))?;
this.peer.send(connection_id, build_initial_channels_update( this.peer.send(connection_id, build_channels_update(
channels_for_user, channels_for_user,
channel_invites channel_invites
))?; ))?;
@ -951,6 +952,7 @@ async fn create_room(
Some(proto::LiveKitConnectionInfo { Some(proto::LiveKitConnectionInfo {
server_url: live_kit.url().into(), server_url: live_kit.url().into(),
token, token,
can_publish: true,
}) })
}) })
} }
@ -1031,6 +1033,7 @@ async fn join_room(
Some(proto::LiveKitConnectionInfo { Some(proto::LiveKitConnectionInfo {
server_url: live_kit.url().into(), server_url: live_kit.url().into(),
token, token,
can_publish: true,
}) })
} else { } else {
None None
@ -2217,38 +2220,21 @@ async fn create_channel(
let db = session.db().await; let db = session.db().await;
let parent_id = request.parent_id.map(|id| ChannelId::from_proto(id)); 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) .create_channel(&request.name, parent_id, session.user_id)
.await?; .await?;
let channel = proto::Channel {
id: id.to_proto(),
name: request.name,
visibility: proto::ChannelVisibility::Members as i32,
};
response.send(proto::CreateChannelResponse { response.send(proto::CreateChannelResponse {
channel: Some(channel.clone()), channel: Some(channel.to_proto()),
parent_id: request.parent_id, 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; 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) { for connection_id in connection_pool.user_connection_ids(user_id) {
if user_id == session.user_id { if user_id == session.user_id {
continue; continue;
@ -2297,7 +2283,10 @@ async fn invite_channel_member(
let db = session.db().await; let db = session.db().await;
let channel_id = ChannelId::from_proto(request.channel_id); let channel_id = ChannelId::from_proto(request.channel_id);
let invitee_id = UserId::from_proto(request.user_id); let invitee_id = UserId::from_proto(request.user_id);
let notifications = db let InviteMemberResult {
channel,
notifications,
} = db
.invite_channel_member( .invite_channel_member(
channel_id, channel_id,
invitee_id, invitee_id,
@ -2306,21 +2295,17 @@ async fn invite_channel_member(
) )
.await?; .await?;
let channel = db.get_channel(channel_id, session.user_id).await?; let update = proto::UpdateChannels {
channel_invitations: vec![channel.to_proto()],
..Default::default()
};
let mut update = proto::UpdateChannels::default(); let connection_pool = session.connection_pool().await;
update.channel_invitations.push(proto::Channel { for connection_id in connection_pool.user_connection_ids(invitee_id) {
id: channel.id.to_proto(),
visibility: channel.visibility.into(),
name: channel.name,
});
let pool = session.connection_pool().await;
for connection_id in pool.user_connection_ids(invitee_id) {
session.peer.send(connection_id, update.clone())?; session.peer.send(connection_id, update.clone())?;
} }
send_notifications(&*pool, &session.peer, notifications); send_notifications(&*connection_pool, &session.peer, notifications);
response.send(proto::Ack {})?; response.send(proto::Ack {})?;
Ok(()) Ok(())
@ -2335,20 +2320,22 @@ async fn remove_channel_member(
let channel_id = ChannelId::from_proto(request.channel_id); let channel_id = ChannelId::from_proto(request.channel_id);
let member_id = UserId::from_proto(request.user_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) .remove_channel_member(channel_id, member_id, session.user_id)
.await?; .await?;
let mut update = proto::UpdateChannels::default(); let connection_pool = &session.connection_pool().await;
update.delete_channels.push(channel_id.to_proto()); notify_membership_updated(
&connection_pool,
for connection_id in session membership_update,
.connection_pool() member_id,
.await &session.peer,
.user_connection_ids(member_id) );
{ for connection_id in connection_pool.user_connection_ids(member_id) {
session.peer.send(connection_id, update.clone()).trace_err(); if let Some(notification_id) = notification_id {
if let Some(notification_id) = removed_notification_id {
session session
.peer .peer
.send( .send(
@ -2374,22 +2361,27 @@ async fn set_channel_visibility(
let channel_id = ChannelId::from_proto(request.channel_id); let channel_id = ChannelId::from_proto(request.channel_id);
let visibility = request.visibility().into(); 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) .set_channel_visibility(channel_id, visibility, session.user_id)
.await?; .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; let connection_pool = session.connection_pool().await;
for member_id in member_ids { for (user_id, channels) in participants_to_update {
for connection_id in connection_pool.user_connection_ids(member_id) { 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())?; session.peer.send(connection_id, update.clone())?;
} }
} }
@ -2406,7 +2398,7 @@ async fn set_channel_member_role(
let db = session.db().await; let db = session.db().await;
let channel_id = ChannelId::from_proto(request.channel_id); let channel_id = ChannelId::from_proto(request.channel_id);
let member_id = UserId::from_proto(request.user_id); let member_id = UserId::from_proto(request.user_id);
let channel_member = db let result = db
.set_channel_member_role( .set_channel_member_role(
channel_id, channel_id,
session.user_id, session.user_id,
@ -2415,22 +2407,30 @@ async fn set_channel_member_role(
) )
.await?; .await?;
let channel = db.get_channel(channel_id, session.user_id).await?; match result {
db::SetMemberRoleResult::MembershipUpdated(membership_update) => {
let connection_pool = session.connection_pool().await;
notify_membership_updated(
&connection_pool,
membership_update,
member_id,
&session.peer,
)
}
db::SetMemberRoleResult::InviteUpdated(channel) => {
let update = proto::UpdateChannels {
channel_invitations: vec![channel.to_proto()],
..Default::default()
};
let mut update = proto::UpdateChannels::default(); for connection_id in session
if channel_member.accepted { .connection_pool()
update.channel_permissions.push(proto::ChannelPermission { .await
channel_id: channel.id.to_proto(), .user_connection_ids(member_id)
role: request.role, {
}); 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 {})?; response.send(proto::Ack {})?;
@ -2444,26 +2444,25 @@ async fn rename_channel(
) -> Result<()> { ) -> Result<()> {
let db = session.db().await; let db = session.db().await;
let channel_id = ChannelId::from_proto(request.channel_id); 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) .rename_channel(channel_id, session.user_id, &request.name)
.await?; .await?;
let channel = proto::Channel {
id: channel.id.to_proto(),
name: channel.name,
visibility: channel.visibility.into(),
};
response.send(proto::RenameChannelResponse { 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; let connection_pool = session.connection_pool().await;
for member_id in member_ids { for (user_id, channel) in participants_to_update {
for connection_id in connection_pool.user_connection_ids(member_id) { for connection_id in connection_pool.user_connection_ids(user_id) {
let update = proto::UpdateChannels {
channels: vec![channel.to_proto()],
..Default::default()
};
session.peer.send(connection_id, update.clone())?; session.peer.send(connection_id, update.clone())?;
} }
} }
@ -2471,131 +2470,55 @@ async fn rename_channel(
Ok(()) 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?;
let update = proto::UpdateChannels {
delete_edge: vec![proto::ChannelEdge {
channel_id: channel_id.to_proto(),
parent_id: from.to_proto(),
}],
..Default::default()
};
let connection_pool = session.connection_pool().await;
for member_id in members {
for connection_id in connection_pool.user_connection_ids(member_id) {
session.peer.send(connection_id, update.clone())?;
}
}
response.send(Ack {})?;
Ok(())
}
async fn move_channel( async fn move_channel(
request: proto::MoveChannel, request: proto::MoveChannel,
response: Response<proto::MoveChannel>, response: Response<proto::MoveChannel>,
session: Session, session: Session,
) -> Result<()> { ) -> Result<()> {
let db = session.db().await;
let channel_id = ChannelId::from_proto(request.channel_id); let channel_id = ChannelId::from_proto(request.channel_id);
let from_parent = ChannelId::from_proto(request.from); let to = request.to.map(ChannelId::from_proto);
let to = ChannelId::from_proto(request.to);
let channels_to_send = db let result = session
.move_channel(session.user_id, channel_id, from_parent, to) .db()
.await
.move_channel(channel_id, to, session.user_id)
.await?; .await?;
if channels_to_send.is_empty() { notify_channel_moved(result, session).await?;
response.send(Ack {})?;
return Ok(());
}
let members_from = db.get_channel_members(from_parent).await?;
let members_to = db.get_channel_members(to).await?;
let update = proto::UpdateChannels {
delete_edge: vec![proto::ChannelEdge {
channel_id: channel_id.to_proto(),
parent_id: from_parent.to_proto(),
}],
..Default::default()
};
let connection_pool = session.connection_pool().await;
for member_id in members_from {
for connection_id in connection_pool.user_connection_ids(member_id) {
session.peer.send(connection_id, update.clone())?;
}
}
let update = proto::UpdateChannels {
channels: channels_to_send
.channels
.into_iter()
.map(|channel| proto::Channel {
id: channel.id.to_proto(),
visibility: channel.visibility.into(),
name: channel.name,
})
.collect(),
insert_edge: channels_to_send.edges,
..Default::default()
};
for member_id in members_to {
for connection_id in connection_pool.user_connection_ids(member_id) {
session.peer.send(connection_id, update.clone())?;
}
}
response.send(Ack {})?; response.send(Ack {})?;
Ok(())
}
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 (user_id, channels) in participants_to_update {
let mut update = build_channels_update(channels, vec![]);
update.delete_channels = moved_channels.clone();
for connection_id in connection_pool.user_connection_ids(user_id) {
session.peer.send(connection_id, update.clone())?;
}
}
for user_id in participants_to_remove {
let update = proto::UpdateChannels {
delete_channels: moved_channels.clone(),
..Default::default()
};
for connection_id in connection_pool.user_connection_ids(user_id) {
session.peer.send(connection_id, update.clone())?;
}
}
Ok(()) Ok(())
} }
@ -2620,81 +2543,39 @@ async fn respond_to_channel_invite(
) -> Result<()> { ) -> Result<()> {
let db = session.db().await; let db = session.db().await;
let channel_id = ChannelId::from_proto(request.channel_id); 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) .respond_to_channel_invite(channel_id, session.user_id, request.accept)
.await?; .await?;
if request.accept { let connection_pool = session.connection_pool().await;
channel_membership_updated(db, channel_id, &session).await?; if let Some(membership_update) = membership_update {
notify_membership_updated(
&connection_pool,
membership_update,
session.user_id,
&session.peer,
);
} else { } else {
let mut update = proto::UpdateChannels::default(); let update = proto::UpdateChannels {
update remove_channel_invitations: vec![channel_id.to_proto()],
.remove_channel_invitations ..Default::default()
.push(channel_id.to_proto()); };
session.peer.send(session.connection_id, update)?;
} for connection_id in connection_pool.user_connection_ids(session.user_id) {
session.peer.send(connection_id, update.clone())?;
}
};
send_notifications(&*connection_pool, &session.peer, notifications);
send_notifications(
&*session.connection_pool().await,
&session.peer,
notifications,
);
response.send(proto::Ack {})?; response.send(proto::Ack {})?;
Ok(()) Ok(())
} }
async fn channel_membership_updated(
db: tokio::sync::MutexGuard<'_, DbHandle>,
channel_id: ChannelId,
session: &Session,
) -> Result<(), crate::Error> {
let mut update = proto::UpdateChannels::default();
update
.remove_channel_invitations
.push(channel_id.to_proto());
let result = db.get_channel_for_user(channel_id, session.user_id).await?;
update.channels.extend(
result
.channels
.channels
.into_iter()
.map(|channel| proto::Channel {
id: channel.id.to_proto(),
visibility: channel.visibility.into(),
name: channel.name,
}),
);
update.unseen_channel_messages = result.channel_messages;
update.unseen_channel_buffer_changes = result.unseen_buffer_changes;
update.insert_edge = result.channels.edges;
update
.channel_participants
.extend(
result
.channel_participants
.into_iter()
.map(|(channel_id, user_ids)| proto::ChannelParticipants {
channel_id: channel_id.to_proto(),
participant_user_ids: user_ids.into_iter().map(UserId::to_proto).collect(),
}),
);
update
.channel_permissions
.extend(
result
.channels_with_admin_privileges
.into_iter()
.map(|channel_id| proto::ChannelPermission {
channel_id: channel_id.to_proto(),
role: proto::ChannelRole::Admin.into(),
}),
);
session.peer.send(session.connection_id, update)?;
Ok(())
}
async fn join_channel( async fn join_channel(
request: proto::JoinChannel, request: proto::JoinChannel,
response: Response<proto::JoinChannel>, response: Response<proto::JoinChannel>,
@ -2727,7 +2608,7 @@ async fn join_channel_internal(
leave_room_for_session(&session).await?; leave_room_for_session(&session).await?;
let db = session.db().await; let db = session.db().await;
let (joined_room, joined_channel) = db let (joined_room, membership_updated, role) = db
.join_channel( .join_channel(
channel_id, channel_id,
session.user_id, session.user_id,
@ -2737,16 +2618,32 @@ async fn join_channel_internal(
.await?; .await?;
let live_kit_connection_info = session.live_kit_client.as_ref().and_then(|live_kit| { let live_kit_connection_info = session.live_kit_client.as_ref().and_then(|live_kit| {
let token = live_kit let (can_publish, token) = if role == ChannelRole::Guest {
.room_token( (
&joined_room.room.live_kit_room, false,
&session.user_id.to_string(), 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 { Some(LiveKitConnectionInfo {
server_url: live_kit.url().into(), server_url: live_kit.url().into(),
token, token,
can_publish,
}) })
}); });
@ -2756,8 +2653,14 @@ async fn join_channel_internal(
live_kit_connection_info, live_kit_connection_info,
})?; })?;
if let Some(joined_channel) = joined_channel { let connection_pool = session.connection_pool().await;
channel_membership_updated(db, joined_channel, &session).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); 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, channels: ChannelsForUser,
channel_invites: Vec<db::Channel>, channel_invites: Vec<db::Channel>,
) -> proto::UpdateChannels { ) -> proto::UpdateChannels {
let mut update = proto::UpdateChannels::default(); let mut update = proto::UpdateChannels::default();
for channel in channels.channels.channels { for channel in channels.channels {
update.channels.push(proto::Channel { update.channels.push(channel.to_proto());
id: channel.id.to_proto(),
name: channel.name,
visibility: channel.visibility.into(),
});
} }
update.unseen_channel_buffer_changes = channels.unseen_buffer_changes; update.unseen_channel_buffer_changes = channels.unseen_buffer_changes;
update.unseen_channel_messages = channels.channel_messages; update.unseen_channel_messages = channels.channel_messages;
update.insert_edge = channels.channels.edges;
for (channel_id, participants) in channels.channel_participants { for (channel_id, participants) in channels.channel_participants {
update 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 { for channel in channel_invites {
update.channel_invitations.push(proto::Channel { update.channel_invitations.push(channel.to_proto());
id: channel.id.to_proto(),
name: channel.name,
// TODO: Visibility
visibility: ChannelVisibility::Public.into(),
});
} }
update update

View file

@ -40,3 +40,7 @@ fn room_participants(room: &ModelHandle<Room>, cx: &mut TestAppContext) -> RoomP
RoomParticipants { remote, pending } RoomParticipants { remote, pending }
}) })
} }
fn channel_id(room: &ModelHandle<Room>, cx: &mut TestAppContext) -> Option<u64> {
cx.read(|cx| room.read(cx).channel_id())
}

View file

@ -3,7 +3,7 @@ use crate::{
tests::TestServer, tests::TestServer,
}; };
use call::ActiveCall; use call::ActiveCall;
use channel::{Channel, ACKNOWLEDGE_DEBOUNCE_INTERVAL}; use channel::ACKNOWLEDGE_DEBOUNCE_INTERVAL;
use client::ParticipantIndex; use client::ParticipantIndex;
use client::{Collaborator, UserId}; use client::{Collaborator, UserId};
use collab_ui::channel_view::ChannelView; use collab_ui::channel_view::ChannelView;
@ -11,10 +11,7 @@ use collections::HashMap;
use editor::{Anchor, Editor, ToOffset}; use editor::{Anchor, Editor, ToOffset};
use futures::future; use futures::future;
use gpui::{executor::Deterministic, ModelHandle, TestAppContext, ViewContext}; use gpui::{executor::Deterministic, ModelHandle, TestAppContext, ViewContext};
use rpc::{ use rpc::{proto::PeerId, RECEIVE_TIMEOUT};
proto::{self, PeerId},
RECEIVE_TIMEOUT,
};
use serde_json::json; use serde_json::json;
use std::{ops::Range, sync::Arc}; use std::{ops::Range, sync::Arc};
@ -410,11 +407,8 @@ async fn test_channel_buffer_disconnect(
server.disconnect_client(client_a.peer_id().unwrap()); server.disconnect_client(client_a.peer_id().unwrap());
deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
channel_buffer_a.update(cx_a, |buffer, _| { channel_buffer_a.update(cx_a, |buffer, cx| {
assert_eq!( assert_eq!(buffer.channel(cx).unwrap().name, "the-channel");
buffer.channel().as_ref(),
&channel(channel_id, "the-channel")
);
assert!(!buffer.is_connected()); assert!(!buffer.is_connected());
}); });
@ -435,25 +429,12 @@ async fn test_channel_buffer_disconnect(
deterministic.run_until_parked(); deterministic.run_until_parked();
// Channel buffer observed the deletion // Channel buffer observed the deletion
channel_buffer_b.update(cx_b, |buffer, _| { channel_buffer_b.update(cx_b, |buffer, cx| {
assert_eq!( assert!(buffer.channel(cx).is_none());
buffer.channel().as_ref(),
&channel(channel_id, "the-channel")
);
assert!(!buffer.is_connected()); 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] #[gpui::test]
async fn test_rejoin_channel_buffer( async fn test_rejoin_channel_buffer(
deterministic: Arc<Deterministic>, deterministic: Arc<Deterministic>,
@ -698,7 +679,7 @@ async fn test_following_to_channel_notes_without_a_shared_project(
.await .await
.unwrap(); .unwrap();
channel_view_1_a.update(cx_a, |notes, cx| { 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| { notes.editor.update(cx, |editor, cx| {
editor.insert("Hello from A.", cx); editor.insert("Hello from A.", cx);
editor.change_selections(None, cx, |selections| { 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") .expect("active item is not a channel view")
}); });
channel_view_1_b.read_with(cx_b, |notes, cx| { 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); let editor = notes.editor.read(cx);
assert_eq!(editor.text(cx), "Hello from A."); assert_eq!(editor.text(cx), "Hello from A.");
assert_eq!(editor.selections.ranges::<usize>(cx), &[3..4]); 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 .await
.unwrap(); .unwrap();
channel_view_2_a.read_with(cx_a, |notes, cx| { 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. // 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") .expect("active item is not a channel view")
}); });
channel_view_2_b.read_with(cx_b, |notes, cx| { 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");
}); });
} }

View file

@ -1,10 +1,12 @@
use crate::{ use crate::{
db::{self, UserId},
rpc::RECONNECT_TIMEOUT, rpc::RECONNECT_TIMEOUT,
tests::{room_participants, RoomParticipants, TestServer}, tests::{room_participants, RoomParticipants, TestServer},
}; };
use call::ActiveCall; use call::ActiveCall;
use channel::{ChannelId, ChannelMembership, ChannelStore}; use channel::{ChannelId, ChannelMembership, ChannelStore};
use client::User; use client::User;
use futures::future::try_join_all;
use gpui::{executor::Deterministic, ModelHandle, TestAppContext}; use gpui::{executor::Deterministic, ModelHandle, TestAppContext};
use rpc::{ use rpc::{
proto::{self, ChannelRole}, proto::{self, ChannelRole},
@ -47,22 +49,19 @@ async fn test_core_channels(
id: channel_a_id, id: channel_a_id,
name: "channel-a".to_string(), name: "channel-a".to_string(),
depth: 0, depth: 0,
user_is_admin: true, role: ChannelRole::Admin,
}, },
ExpectedChannel { ExpectedChannel {
id: channel_b_id, id: channel_b_id,
name: "channel-b".to_string(), name: "channel-b".to_string(),
depth: 1, depth: 1,
user_is_admin: true, role: ChannelRole::Admin,
}, },
], ],
); );
client_b.channel_store().read_with(cx_b, |channels, _| { client_b.channel_store().read_with(cx_b, |channels, _| {
assert!(channels assert!(channels.ordered_channels().collect::<Vec<_>>().is_empty())
.channel_dag_entries()
.collect::<Vec<_>>()
.is_empty())
}); });
// Invite client B to channel A as client A. // Invite client B to channel A as client A.
@ -94,7 +93,7 @@ async fn test_core_channels(
id: channel_a_id, id: channel_a_id,
name: "channel-a".to_string(), name: "channel-a".to_string(),
depth: 0, depth: 0,
user_is_admin: false, role: ChannelRole::Member,
}], }],
); );
@ -141,13 +140,13 @@ async fn test_core_channels(
ExpectedChannel { ExpectedChannel {
id: channel_a_id, id: channel_a_id,
name: "channel-a".to_string(), name: "channel-a".to_string(),
user_is_admin: false, role: ChannelRole::Member,
depth: 0, depth: 0,
}, },
ExpectedChannel { ExpectedChannel {
id: channel_b_id, id: channel_b_id,
name: "channel-b".to_string(), name: "channel-b".to_string(),
user_is_admin: false, role: ChannelRole::Member,
depth: 1, depth: 1,
}, },
], ],
@ -169,19 +168,19 @@ async fn test_core_channels(
ExpectedChannel { ExpectedChannel {
id: channel_a_id, id: channel_a_id,
name: "channel-a".to_string(), name: "channel-a".to_string(),
user_is_admin: false, role: ChannelRole::Member,
depth: 0, depth: 0,
}, },
ExpectedChannel { ExpectedChannel {
id: channel_b_id, id: channel_b_id,
name: "channel-b".to_string(), name: "channel-b".to_string(),
user_is_admin: false, role: ChannelRole::Member,
depth: 1, depth: 1,
}, },
ExpectedChannel { ExpectedChannel {
id: channel_c_id, id: channel_c_id,
name: "channel-c".to_string(), name: "channel-c".to_string(),
user_is_admin: false, role: ChannelRole::Member,
depth: 2, depth: 2,
}, },
], ],
@ -213,19 +212,19 @@ async fn test_core_channels(
id: channel_a_id, id: channel_a_id,
name: "channel-a".to_string(), name: "channel-a".to_string(),
depth: 0, depth: 0,
user_is_admin: true, role: ChannelRole::Admin,
}, },
ExpectedChannel { ExpectedChannel {
id: channel_b_id, id: channel_b_id,
name: "channel-b".to_string(), name: "channel-b".to_string(),
depth: 1, depth: 1,
user_is_admin: true, role: ChannelRole::Admin,
}, },
ExpectedChannel { ExpectedChannel {
id: channel_c_id, id: channel_c_id,
name: "channel-c".to_string(), name: "channel-c".to_string(),
depth: 2, depth: 2,
user_is_admin: true, role: ChannelRole::Admin,
}, },
], ],
); );
@ -247,7 +246,7 @@ async fn test_core_channels(
id: channel_a_id, id: channel_a_id,
name: "channel-a".to_string(), name: "channel-a".to_string(),
depth: 0, depth: 0,
user_is_admin: true, role: ChannelRole::Admin,
}], }],
); );
assert_channels( assert_channels(
@ -257,7 +256,7 @@ async fn test_core_channels(
id: channel_a_id, id: channel_a_id,
name: "channel-a".to_string(), name: "channel-a".to_string(),
depth: 0, depth: 0,
user_is_admin: true, role: ChannelRole::Admin,
}], }],
); );
@ -280,18 +279,27 @@ async fn test_core_channels(
id: channel_a_id, id: channel_a_id,
name: "channel-a".to_string(), name: "channel-a".to_string(),
depth: 0, depth: 0,
user_is_admin: true, role: ChannelRole::Admin,
}], }],
); );
// Client B no longer has access to the channel // Client B no longer has access to the channel
assert_channels(client_b.channel_store(), cx_b, &[]); assert_channels(client_b.channel_store(), cx_b, &[]);
// When disconnected, client A sees no channels.
server.forbid_connections(); server.forbid_connections();
server.disconnect_client(client_a.peer_id().unwrap()); server.disconnect_client(client_a.peer_id().unwrap());
deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); 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(); server.allow_connections();
deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
@ -300,9 +308,9 @@ async fn test_core_channels(
cx_a, cx_a,
&[ExpectedChannel { &[ExpectedChannel {
id: channel_a_id, id: channel_a_id,
name: "channel-a".to_string(), name: "channel-a-renamed".to_string(),
depth: 0, depth: 0,
user_is_admin: true, role: ChannelRole::Admin,
}], }],
); );
} }
@ -410,7 +418,7 @@ async fn test_channel_room(
id: zed_id, id: zed_id,
name: "zed".to_string(), name: "zed".to_string(),
depth: 0, depth: 0,
user_is_admin: false, role: ChannelRole::Member,
}], }],
); );
client_b.channel_store().read_with(cx_b, |channels, _| { client_b.channel_store().read_with(cx_b, |channels, _| {
@ -643,7 +651,7 @@ async fn test_permissions_update_while_invited(
depth: 0, depth: 0,
id: rust_id, id: rust_id,
name: "rust".to_string(), name: "rust".to_string(),
user_is_admin: false, role: ChannelRole::Member,
}], }],
); );
assert_channels(client_b.channel_store(), cx_b, &[]); assert_channels(client_b.channel_store(), cx_b, &[]);
@ -671,7 +679,7 @@ async fn test_permissions_update_while_invited(
depth: 0, depth: 0,
id: rust_id, id: rust_id,
name: "rust".to_string(), name: "rust".to_string(),
user_is_admin: false, role: ChannelRole::Member,
}], }],
); );
assert_channels(client_b.channel_store(), cx_b, &[]); assert_channels(client_b.channel_store(), cx_b, &[]);
@ -711,7 +719,7 @@ async fn test_channel_rename(
depth: 0, depth: 0,
id: rust_id, id: rust_id,
name: "rust-archive".to_string(), name: "rust-archive".to_string(),
user_is_admin: true, role: ChannelRole::Admin,
}], }],
); );
@ -723,7 +731,7 @@ async fn test_channel_rename(
depth: 0, depth: 0,
id: rust_id, id: rust_id,
name: "rust-archive".to_string(), name: "rust-archive".to_string(),
user_is_admin: false, role: ChannelRole::Member,
}], }],
); );
} }
@ -846,7 +854,7 @@ async fn test_lost_channel_creation(
depth: 0, depth: 0,
id: channel_id, id: channel_id,
name: "x".to_string(), name: "x".to_string(),
user_is_admin: false, role: ChannelRole::Member,
}], }],
); );
@ -870,13 +878,13 @@ async fn test_lost_channel_creation(
depth: 0, depth: 0,
id: channel_id, id: channel_id,
name: "x".to_string(), name: "x".to_string(),
user_is_admin: true, role: ChannelRole::Admin,
}, },
ExpectedChannel { ExpectedChannel {
depth: 1, depth: 1,
id: subchannel_id, id: subchannel_id,
name: "subchannel".to_string(), name: "subchannel".to_string(),
user_is_admin: true, role: ChannelRole::Admin,
}, },
], ],
); );
@ -901,17 +909,327 @@ async fn test_lost_channel_creation(
depth: 0, depth: 0,
id: channel_id, id: channel_id,
name: "x".to_string(), name: "x".to_string(),
user_is_admin: false, role: ChannelRole::Member,
}, },
ExpectedChannel { ExpectedChannel {
depth: 1, depth: 1,
id: subchannel_id, id: subchannel_id,
name: "subchannel".to_string(), 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] #[gpui::test]
async fn test_guest_access( async fn test_guest_access(
deterministic: Arc<Deterministic>, deterministic: Arc<Deterministic>,
@ -925,44 +1243,79 @@ async fn test_guest_access(
let client_b = server.create_client(cx_b, "user_b").await; let client_b = server.create_client(cx_b, "user_b").await;
let channels = server 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; .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); 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 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 .await
.is_err()); .is_err());
// Make channels A and B public
client_a client_a
.channel_store() .channel_store()
.update(cx_a, |channel_store, cx| { .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 .await
.unwrap(); .unwrap();
// Client B joins channel A as a guest
active_call_b 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 .await
.unwrap(); .unwrap();
deterministic.run_until_parked(); deterministic.run_until_parked();
assert_channels_list_shape(
assert!(client_b client_a.channel_store(),
.channel_store() cx_a,
.update(cx_b, |channel_store, _| channel_store &[(channel_a, 0), (channel_b, 1)],
.channel_for_id(channel_a_id) );
.is_some())); 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, _| { 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.len(), 1);
assert_eq!(participants[0].id, client_b.user_id().unwrap()); 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] #[gpui::test]
@ -1030,14 +1383,14 @@ async fn test_invite_access(
async fn test_channel_moving( async fn test_channel_moving(
deterministic: Arc<Deterministic>, deterministic: Arc<Deterministic>,
cx_a: &mut TestAppContext, cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext, _cx_b: &mut TestAppContext,
cx_c: &mut TestAppContext, _cx_c: &mut TestAppContext,
) { ) {
deterministic.forbid_parking(); deterministic.forbid_parking();
let mut server = TestServer::start(&deterministic).await; let mut server = TestServer::start(&deterministic).await;
let client_a = server.create_client(cx_a, "user_a").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_b = server.create_client(cx_b, "user_b").await;
let client_c = server.create_client(cx_c, "user_c").await; // let client_c = server.create_client(cx_c, "user_c").await;
let channels = server let channels = server
.make_channel_tree( .make_channel_tree(
@ -1071,7 +1424,7 @@ async fn test_channel_moving(
client_a client_a
.channel_store() .channel_store()
.update(cx_a, |channel_store, cx| { .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 .await
.unwrap(); .unwrap();
@ -1089,188 +1442,6 @@ async fn test_channel_moving(
(channel_d_id, 2), (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)] #[derive(Debug, PartialEq)]
@ -1278,7 +1449,7 @@ struct ExpectedChannel {
depth: usize, depth: usize,
id: ChannelId, id: ChannelId,
name: String, name: String,
user_is_admin: bool, role: ChannelRole,
} }
#[track_caller] #[track_caller]
@ -1295,7 +1466,7 @@ fn assert_channel_invitations(
depth: 0, depth: 0,
name: channel.name.clone(), name: channel.name.clone(),
id: channel.id, id: channel.id,
user_is_admin: store.is_user_admin(channel.id), role: channel.role,
}) })
.collect::<Vec<_>>() .collect::<Vec<_>>()
}); });
@ -1310,12 +1481,12 @@ fn assert_channels(
) { ) {
let actual = channel_store.read_with(cx, |store, _| { let actual = channel_store.read_with(cx, |store, _| {
store store
.channel_dag_entries() .ordered_channels()
.map(|(depth, channel)| ExpectedChannel { .map(|(depth, channel)| ExpectedChannel {
depth, depth,
name: channel.name.clone(), name: channel.name.clone(),
id: channel.id, id: channel.id,
user_is_admin: store.is_user_admin(channel.id), role: channel.role,
}) })
.collect::<Vec<_>>() .collect::<Vec<_>>()
}); });
@ -1332,7 +1503,7 @@ fn assert_channels_list_shape(
let actual = channel_store.read_with(cx, |store, _| { let actual = channel_store.read_with(cx, |store, _| {
store store
.channel_dag_entries() .ordered_channels()
.map(|(depth, channel)| (channel.id, depth)) .map(|(depth, channel)| (channel.id, depth))
.collect::<Vec<_>>() .collect::<Vec<_>>()
}); });

View file

@ -1,6 +1,6 @@
use crate::{ use crate::{
rpc::{CLEANUP_TIMEOUT, RECONNECT_TIMEOUT}, 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 call::{room, ActiveCall, ParticipantLocation, Room};
use client::{User, RECEIVE_TIMEOUT}; 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)] #[gpui::test(iterations = 10)]
async fn test_room_uniqueness( async fn test_room_uniqueness(
deterministic: Arc<Deterministic>, deterministic: Arc<Deterministic>,
@ -4555,11 +4668,7 @@ async fn test_prettier_formatting_buffer(
.insert_tree(&directory, json!({ "a.rs": buffer_text })) .insert_tree(&directory, json!({ "a.rs": buffer_text }))
.await; .await;
let (project_a, worktree_id) = client_a.build_local_project(&directory, cx_a).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 prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
let suffix = project.enable_test_prettier(&[test_plugin]);
project.languages().add(language);
suffix
});
let buffer_a = cx_a let buffer_a = cx_a
.background() .background()
.spawn(project_a.update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx))) .spawn(project_a.update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))

View file

@ -48,7 +48,7 @@ impl RandomizedTest for RandomChannelBufferTest {
let db = &server.app_state.db; let db = &server.app_state.db;
for ix in 0..CHANNEL_COUNT { for ix in 0..CHANNEL_COUNT {
let id = db let id = db
.create_channel(&format!("channel-{ix}"), None, users[0].user_id) .create_root_channel(&format!("channel-{ix}"), users[0].user_id)
.await .await
.unwrap(); .unwrap();
for user in &users[1..] { for user in &users[1..] {
@ -83,7 +83,7 @@ impl RandomizedTest for RandomChannelBufferTest {
match rng.gen_range(0..100_u32) { match rng.gen_range(0..100_u32) {
0..=29 => { 0..=29 => {
let channel_name = client.channel_store().read_with(cx, |store, cx| { 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) { if store.has_open_channel_buffer(channel.id, cx) {
None None
} else { } else {
@ -98,15 +98,16 @@ impl RandomizedTest for RandomChannelBufferTest {
30..=40 => { 30..=40 => {
if let Some(buffer) = channel_buffers.iter().choose(rng) { 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 }; break ChannelBufferOperation::LeaveChannelNotes { channel_name };
} }
} }
_ => { _ => {
if let Some(buffer) = channel_buffers.iter().choose(rng) { if let Some(buffer) = channel_buffers.iter().choose(rng) {
break buffer.read_with(cx, |b, _| { break buffer.read_with(cx, |b, cx| {
let channel_name = b.channel().name.clone(); let channel_name = b.channel(cx).unwrap().name.clone();
let edits = b let edits = b
.buffer() .buffer()
.read_with(cx, |buffer, _| buffer.get_random_edits(rng, 3)); .read_with(cx, |buffer, _| buffer.get_random_edits(rng, 3));
@ -130,7 +131,7 @@ impl RandomizedTest for RandomChannelBufferTest {
ChannelBufferOperation::JoinChannelNotes { channel_name } => { ChannelBufferOperation::JoinChannelNotes { channel_name } => {
let buffer = client.channel_store().update(cx, |store, cx| { let buffer = client.channel_store().update(cx, |store, cx| {
let channel_id = store let channel_id = store
.channel_dag_entries() .ordered_channels()
.find(|(_, c)| c.name == channel_name) .find(|(_, c)| c.name == channel_name)
.unwrap() .unwrap()
.1 .1
@ -153,7 +154,7 @@ impl RandomizedTest for RandomChannelBufferTest {
let buffer = cx.update(|cx| { let buffer = cx.update(|cx| {
let mut left_buffer = Err(TestError::Inapplicable); let mut left_buffer = Err(TestError::Inapplicable);
client.channel_buffers().retain(|buffer| { 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()); left_buffer = Ok(buffer.clone());
false false
} else { } else {
@ -179,7 +180,9 @@ impl RandomizedTest for RandomChannelBufferTest {
client client
.channel_buffers() .channel_buffers()
.iter() .iter()
.find(|buffer| buffer.read(cx).channel().name == channel_name) .find(|buffer| {
buffer.read(cx).channel(cx).unwrap().name == channel_name
})
.cloned() .cloned()
}) })
.ok_or_else(|| TestError::Inapplicable)?; .ok_or_else(|| TestError::Inapplicable)?;
@ -250,7 +253,7 @@ impl RandomizedTest for RandomChannelBufferTest {
if let Some(channel_buffer) = client if let Some(channel_buffer) = client
.channel_buffers() .channel_buffers()
.iter() .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); let channel_buffer = channel_buffer.read(cx);

View file

@ -611,38 +611,6 @@ impl TestClient {
) -> WindowHandle<Workspace> { ) -> WindowHandle<Workspace> {
cx.add_window(|cx| Workspace::new(0, project.clone(), self.app_state.clone(), cx)) 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 { impl Drop for TestClient {

View file

@ -61,6 +61,7 @@ postage.workspace = true
serde.workspace = true serde.workspace = true
serde_derive.workspace = true serde_derive.workspace = true
time.workspace = true time.workspace = true
smallvec.workspace = true
[dev-dependencies] [dev-dependencies]
call = { path = "../call", features = ["test-support"] } call = { path = "../call", features = ["test-support"] }

View file

@ -15,13 +15,14 @@ use gpui::{
ViewContext, ViewHandle, ViewContext, ViewHandle,
}; };
use project::Project; use project::Project;
use smallvec::SmallVec;
use std::{ use std::{
any::{Any, TypeId}, any::{Any, TypeId},
sync::Arc, sync::Arc,
}; };
use util::ResultExt; use util::ResultExt;
use workspace::{ use workspace::{
item::{FollowableItem, Item, ItemHandle}, item::{FollowableItem, Item, ItemEvent, ItemHandle},
register_followable_item, register_followable_item,
searchable::SearchableItemHandle, searchable::SearchableItemHandle,
ItemNavHistory, Pane, SaveIntent, ViewId, Workspace, WorkspaceId, ItemNavHistory, Pane, SaveIntent, ViewId, Workspace, WorkspaceId,
@ -140,6 +141,12 @@ impl ChannelView {
editor.set_collaboration_hub(Box::new(ChannelBufferCollaborationHub( editor.set_collaboration_hub(Box::new(ChannelBufferCollaborationHub(
channel_buffer.clone(), channel_buffer.clone(),
))); )));
editor.set_read_only(
!channel_buffer
.read(cx)
.channel(cx)
.is_some_and(|c| c.can_edit_notes()),
);
editor editor
}); });
let _editor_event_subscription = cx.subscribe(&editor, |_, _, e, cx| cx.emit(e.clone())); 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> { pub fn channel(&self, cx: &AppContext) -> Option<Arc<Channel>> {
self.channel_buffer.read(cx).channel() self.channel_buffer.read(cx).channel(cx)
} }
fn handle_channel_buffer_event( fn handle_channel_buffer_event(
@ -172,6 +179,13 @@ impl ChannelView {
editor.set_read_only(true); editor.set_read_only(true);
cx.notify(); 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 => { ChannelBufferEvent::BufferEdited => {
if cx.is_self_focused() || self.editor.is_focused(cx) { if cx.is_self_focused() || self.editor.is_focused(cx) {
self.acknowledge_buffer_version(cx); self.acknowledge_buffer_version(cx);
@ -179,7 +193,7 @@ impl ChannelView {
self.channel_store.update(cx, |store, cx| { self.channel_store.update(cx, |store, cx| {
let channel_buffer = self.channel_buffer.read(cx); let channel_buffer = self.channel_buffer.read(cx);
store.notes_changed( store.notes_changed(
channel_buffer.channel().id, channel_buffer.channel_id,
channel_buffer.epoch(), channel_buffer.epoch(),
&channel_buffer.buffer().read(cx).version(), &channel_buffer.buffer().read(cx).version(),
cx, cx,
@ -187,7 +201,7 @@ impl ChannelView {
}); });
} }
} }
_ => {} ChannelBufferEvent::CollaboratorsChanged => {}
} }
} }
@ -195,7 +209,7 @@ impl ChannelView {
self.channel_store.update(cx, |store, cx| { self.channel_store.update(cx, |store, cx| {
let channel_buffer = self.channel_buffer.read(cx); let channel_buffer = self.channel_buffer.read(cx);
store.acknowledge_notes_version( store.acknowledge_notes_version(
channel_buffer.channel().id, channel_buffer.channel_id,
channel_buffer.epoch(), channel_buffer.epoch(),
&channel_buffer.buffer().read(cx).version(), &channel_buffer.buffer().read(cx).version(),
cx, cx,
@ -250,11 +264,17 @@ impl Item for ChannelView {
style: &theme::Tab, style: &theme::Tab,
cx: &gpui::AppContext, cx: &gpui::AppContext,
) -> AnyElement<V> { ) -> AnyElement<V> {
let channel_name = &self.channel_buffer.read(cx).channel().name; let label = if let Some(channel) = self.channel(cx) {
let label = if self.channel_buffer.read(cx).is_connected() { match (
format!("#{}", channel_name) 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 { } else {
format!("#{} (disconnected)", channel_name) format!("channel notes (disconnected)")
}; };
Label::new(label, style.label.to_owned()).into_any() 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> { fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option<Vector2F> {
self.editor.read(cx).pixel_position_of_cursor(cx) 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 { impl FollowableItem for ChannelView {
@ -313,7 +337,7 @@ impl FollowableItem for ChannelView {
Some(proto::view::Variant::ChannelView( Some(proto::view::Variant::ChannelView(
proto::view::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)) = editor: if let Some(proto::view::Variant::Editor(proto)) =
self.editor.read(cx).to_state_proto(cx) self.editor.read(cx).to_state_proto(cx)
{ {

View file

@ -263,21 +263,22 @@ impl ChatPanel {
fn set_active_chat(&mut self, chat: ModelHandle<ChannelChat>, cx: &mut ViewContext<Self>) { 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) { if self.active_chat.as_ref().map(|e| &e.0) != Some(&chat) {
self.markdown_data.clear(); let channel_id = chat.read(cx).channel_id;
let id = { {
self.markdown_data.clear();
let chat = chat.read(cx); let chat = chat.read(cx);
let channel = chat.channel().clone();
self.message_list.reset(chat.message_count()); 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| { 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); let subscription = cx.subscribe(&chat, Self::channel_did_change);
self.active_chat = Some((chat, subscription)); self.active_chat = Some((chat, subscription));
self.acknowledge_last_message(cx); self.acknowledge_last_message(cx);
self.channel_select.update(cx, |select, 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); select.set_selected_index(ix, cx);
} }
}); });
@ -361,7 +362,8 @@ impl ChatPanel {
let is_admin = self let is_admin = self
.channel_store .channel_store
.read(cx) .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 last_message = active_chat.message(ix.saturating_sub(1));
let this_message = active_chat.message(ix).clone(); let this_message = active_chat.message(ix).clone();
let is_continuation = last_message.id != this_message.id let is_continuation = last_message.id != this_message.id
@ -676,7 +678,7 @@ impl ChatPanel {
.active_chat .active_chat
.as_ref() .as_ref()
.and_then(|(chat, _)| { .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()))) .then(|| Task::ready(anyhow::Ok(chat.clone())))
}) })
.unwrap_or_else(|| { .unwrap_or_else(|| {
@ -714,7 +716,7 @@ impl ChatPanel {
fn open_notes(&mut self, _: &OpenChannelNotes, cx: &mut ViewContext<Self>) { fn open_notes(&mut self, _: &OpenChannelNotes, cx: &mut ViewContext<Self>) {
if let Some((chat, _)) = &self.active_chat { 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) { if let Some(workspace) = self.workspace.upgrade(cx) {
ChannelView::open(channel_id, workspace, cx).detach(); ChannelView::open(channel_id, workspace, cx).detach();
} }
@ -723,7 +725,7 @@ impl ChatPanel {
fn join_call(&mut self, _: &JoinCall, cx: &mut ViewContext<Self>) { fn join_call(&mut self, _: &JoinCall, cx: &mut ViewContext<Self>) {
if let Some((chat, _)) = &self.active_chat { 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) ActiveCall::global(cx)
.update(cx, |call, cx| call.join_channel(channel_id, cx)) .update(cx, |call, cx| call.join_channel(channel_id, cx))
.detach_and_log_err(cx); .detach_and_log_err(cx);

View file

@ -1,4 +1,4 @@
use channel::{Channel, ChannelMembership, ChannelStore, MessageParams}; use channel::{ChannelId, ChannelMembership, ChannelStore, MessageParams};
use client::UserId; use client::UserId;
use collections::HashMap; use collections::HashMap;
use editor::{AnchorRangeExt, Editor}; use editor::{AnchorRangeExt, Editor};
@ -30,7 +30,7 @@ pub struct MessageEditor {
users: HashMap<String, UserId>, users: HashMap<String, UserId>,
mentions: Vec<UserId>, mentions: Vec<UserId>,
mentions_task: Option<Task<()>>, mentions_task: Option<Task<()>>,
channel: Option<Arc<Channel>>, channel_id: Option<ChannelId>,
} }
impl MessageEditor { impl MessageEditor {
@ -68,24 +68,33 @@ impl MessageEditor {
editor, editor,
channel_store, channel_store,
users: HashMap::default(), users: HashMap::default(),
channel: None, channel_id: None,
mentions: Vec::new(), mentions: Vec::new(),
mentions_task: None, 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| { 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); self.refresh_users(cx);
} }
pub fn refresh_users(&mut self, cx: &mut ViewContext<Self>) { 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| { 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 { cx.spawn(|this, mut cx| async move {
let members = members.await?; let members = members.await?;

View file

@ -9,7 +9,7 @@ use crate::{
}; };
use anyhow::Result; use anyhow::Result;
use call::ActiveCall; use call::ActiveCall;
use channel::{Channel, ChannelData, ChannelEvent, ChannelId, ChannelPath, ChannelStore}; use channel::{Channel, ChannelEvent, ChannelId, ChannelStore};
use channel_modal::ChannelModal; use channel_modal::ChannelModal;
use client::{ use client::{
proto::{self, PeerId}, proto::{self, PeerId},
@ -55,17 +55,17 @@ use workspace::{
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
struct ToggleCollapse { struct ToggleCollapse {
location: ChannelPath, location: ChannelId,
} }
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
struct NewChannel { struct NewChannel {
location: ChannelPath, location: ChannelId,
} }
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
struct RenameChannel { struct RenameChannel {
location: ChannelPath, channel_id: ChannelId,
} }
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
@ -111,18 +111,6 @@ pub struct CopyChannelLink {
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
struct StartMoveChannelFor { struct StartMoveChannelFor {
channel_id: ChannelId, 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)] #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
@ -130,14 +118,6 @@ struct MoveChannel {
to: ChannelId, to: ChannelId,
} }
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
struct UnlinkChannel {
channel_id: ChannelId,
parent_id: ChannelId,
}
type DraggedChannel = (Channel, Option<ChannelId>);
actions!( actions!(
collab_panel, collab_panel,
[ [
@ -147,8 +127,7 @@ actions!(
CollapseSelectedChannel, CollapseSelectedChannel,
ExpandSelectedChannel, ExpandSelectedChannel,
StartMoveChannel, StartMoveChannel,
StartLinkChannel, MoveSelected,
MoveOrLinkToSelected,
InsertSpace, InsertSpace,
] ]
); );
@ -166,11 +145,8 @@ impl_actions!(
JoinChannelCall, JoinChannelCall,
JoinChannelChat, JoinChannelChat,
CopyChannelLink, CopyChannelLink,
LinkChannel,
StartMoveChannelFor, StartMoveChannelFor,
StartLinkChannelFor,
MoveChannel, MoveChannel,
UnlinkChannel,
ToggleSelectedIx ToggleSelectedIx
] ]
); );
@ -178,14 +154,6 @@ impl_actions!(
#[derive(Debug, Copy, Clone, PartialEq, Eq)] #[derive(Debug, Copy, Clone, PartialEq, Eq)]
struct ChannelMoveClipboard { struct ChannelMoveClipboard {
channel_id: ChannelId, 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"; const COLLABORATION_PANEL_KEY: &'static str = "CollaborationPanel";
@ -232,87 +200,35 @@ pub fn init(cx: &mut AppContext) {
_: &mut ViewContext<CollabPanel>| { _: &mut ViewContext<CollabPanel>| {
panel.channel_clipboard = Some(ChannelMoveClipboard { panel.channel_clipboard = Some(ChannelMoveClipboard {
channel_id: action.channel_id, 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( cx.add_action(
|panel: &mut CollabPanel, _: &StartMoveChannel, _: &mut ViewContext<CollabPanel>| { |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 { panel.channel_clipboard = Some(ChannelMoveClipboard {
channel_id: path.channel_id(), channel_id: channel.id,
parent_id: path.parent_id(),
intent: ClipboardIntent::Move,
}) })
} }
}, },
); );
cx.add_action( cx.add_action(
|panel: &mut CollabPanel, _: &StartLinkChannel, _: &mut ViewContext<CollabPanel>| { |panel: &mut CollabPanel, _: &MoveSelected, cx: &mut ViewContext<CollabPanel>| {
if let Some((_, path)) = panel.selected_channel() { let Some(clipboard) = panel.channel_clipboard.take() else {
panel.channel_clipboard = Some(ChannelMoveClipboard { return;
channel_id: path.channel_id(), };
parent_id: path.parent_id(), let Some(selected_channel) = panel.selected_channel() else {
intent: ClipboardIntent::Link, return;
}) };
}
},
);
cx.add_action( panel
|panel: &mut CollabPanel, _: &MoveOrLinkToSelected, cx: &mut ViewContext<CollabPanel>| { .channel_store
let clipboard = panel.channel_clipboard.take(); .update(cx, |channel_store, cx| {
if let Some(((selected_channel, _), clipboard)) = channel_store.move_channel(clipboard.channel_id, Some(selected_channel.id), cx)
panel.selected_channel().zip(clipboard)
{
match clipboard.intent {
ClipboardIntent::Move if clipboard.parent_id.is_some() => {
let parent_id = clipboard.parent_id.unwrap();
panel.channel_store.update(cx, |channel_store, cx| {
channel_store
.move_channel(
clipboard.channel_id,
parent_id,
selected_channel.id,
cx,
)
.detach_and_log_err(cx)
})
}
_ => panel.channel_store.update(cx, |channel_store, cx| {
channel_store
.link_channel(clipboard.channel_id, selected_channel.id, cx)
.detach_and_log_err(cx)
}),
}
}
},
);
cx.add_action(
|panel: &mut CollabPanel, action: &LinkChannel, cx: &mut ViewContext<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)
}) })
} .detach_and_log_err(cx)
}, },
); );
@ -320,39 +236,23 @@ pub fn init(cx: &mut AppContext) {
|panel: &mut CollabPanel, action: &MoveChannel, cx: &mut ViewContext<CollabPanel>| { |panel: &mut CollabPanel, action: &MoveChannel, cx: &mut ViewContext<CollabPanel>| {
if let Some(clipboard) = panel.channel_clipboard.take() { if let Some(clipboard) = panel.channel_clipboard.take() {
panel.channel_store.update(cx, |channel_store, cx| { panel.channel_store.update(cx, |channel_store, cx| {
if let Some(parent) = clipboard.parent_id { channel_store
channel_store .move_channel(clipboard.channel_id, Some(action.to), cx)
.move_channel(clipboard.channel_id, parent, action.to, cx) .detach_and_log_err(cx)
.detach_and_log_err(cx)
} else {
channel_store
.link_channel(clipboard.channel_id, 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)] #[derive(Debug)]
pub enum ChannelEditingState { pub enum ChannelEditingState {
Create { Create {
location: Option<ChannelPath>, location: Option<ChannelId>,
pending_name: Option<String>, pending_name: Option<String>,
}, },
Rename { Rename {
location: ChannelPath, location: ChannelId,
pending_name: Option<String>, pending_name: Option<String>,
}, },
} }
@ -386,16 +286,23 @@ pub struct CollabPanel {
list_state: ListState<Self>, list_state: ListState<Self>,
subscriptions: Vec<Subscription>, subscriptions: Vec<Subscription>,
collapsed_sections: Vec<Section>, collapsed_sections: Vec<Section>,
collapsed_channels: Vec<ChannelPath>, collapsed_channels: Vec<ChannelId>,
drag_target_channel: Option<ChannelData>, drag_target_channel: ChannelDragTarget,
workspace: WeakViewHandle<Workspace>, workspace: WeakViewHandle<Workspace>,
context_menu_on_selected: bool, context_menu_on_selected: bool,
} }
#[derive(PartialEq, Eq)]
enum ChannelDragTarget {
None,
Root,
Channel(ChannelId),
}
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
struct SerializedCollabPanel { struct SerializedCollabPanel {
width: Option<f32>, width: Option<f32>,
collapsed_channels: Option<Vec<ChannelPath>>, collapsed_channels: Option<Vec<ChannelId>>,
} }
#[derive(Debug)] #[derive(Debug)]
@ -440,7 +347,7 @@ enum ListEntry {
Channel { Channel {
channel: Arc<Channel>, channel: Arc<Channel>,
depth: usize, depth: usize,
path: ChannelPath, has_children: bool,
}, },
ChannelNotes { ChannelNotes {
channel_id: ChannelId, channel_id: ChannelId,
@ -575,14 +482,14 @@ impl CollabPanel {
ListEntry::Channel { ListEntry::Channel {
channel, channel,
depth, depth,
path, has_children,
} => { } => {
let channel_row = this.render_channel( let channel_row = this.render_channel(
&*channel, &*channel,
*depth, *depth,
path.to_owned(),
&theme, &theme,
is_selected, is_selected,
*has_children,
ix, ix,
cx, cx,
); );
@ -677,7 +584,7 @@ impl CollabPanel {
workspace: workspace.weak_handle(), workspace: workspace.weak_handle(),
client: workspace.app_state().client.clone(), client: workspace.app_state().client.clone(),
context_menu_on_selected: true, context_menu_on_selected: true,
drag_target_channel: None, drag_target_channel: ChannelDragTarget::None,
list_state, list_state,
}; };
@ -941,7 +848,7 @@ impl CollabPanel {
if channel_store.channel_count() > 0 || self.channel_editing_state.is_some() { if channel_store.channel_count() > 0 || self.channel_editing_state.is_some() {
self.match_candidates.clear(); self.match_candidates.clear();
self.match_candidates self.match_candidates
.extend(channel_store.channel_dag_entries().enumerate().map( .extend(channel_store.ordered_channels().enumerate().map(
|(ix, (_, channel))| StringMatchCandidate { |(ix, (_, channel))| StringMatchCandidate {
id: ix, id: ix,
string: channel.name.clone(), string: channel.name.clone(),
@ -963,48 +870,52 @@ impl CollabPanel {
} }
let mut collapse_depth = None; let mut collapse_depth = None;
for mat in matches { for mat in matches {
let (channel, path) = channel_store let channel = channel_store.channel_at_index(mat.candidate_id).unwrap();
.channel_dag_entry_at(mat.candidate_id) let depth = channel.parent_path.len();
.unwrap();
let depth = path.len() - 1;
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); collapse_depth = Some(depth);
} else if let Some(collapsed_depth) = collapse_depth { } else if let Some(collapsed_depth) = collapse_depth {
if depth > collapsed_depth { if depth > collapsed_depth {
continue; continue;
} }
if self.is_channel_collapsed(path) { if self.is_channel_collapsed(channel.id) {
collapse_depth = Some(depth); collapse_depth = Some(depth);
} else { } else {
collapse_depth = None; 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 { match &self.channel_editing_state {
Some(ChannelEditingState::Create { 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 { self.entries.push(ListEntry::Channel {
channel: channel.clone(), channel: channel.clone(),
depth, depth,
path: path.clone(), has_children: false,
}); });
self.entries self.entries
.push(ListEntry::ChannelEditor { depth: depth + 1 }); .push(ListEntry::ChannelEditor { depth: depth + 1 });
} }
Some(ChannelEditingState::Rename { 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::ChannelEditor { depth });
} }
_ => { _ => {
self.entries.push(ListEntry::Channel { self.entries.push(ListEntry::Channel {
channel: channel.clone(), channel: channel.clone(),
depth, depth,
path: path.clone(), has_children,
}); });
} }
} }
@ -1546,6 +1457,7 @@ impl CollabPanel {
let mut channel_link = None; let mut channel_link = None;
let mut channel_tooltip_text = None; let mut channel_tooltip_text = None;
let mut channel_icon = None; let mut channel_icon = None;
let mut is_dragged_over = false;
let text = match section { let text = match section {
Section::ActiveCall => { Section::ActiveCall => {
@ -1629,26 +1541,37 @@ impl CollabPanel {
cx, cx,
), ),
), ),
Section::Channels => Some( Section::Channels => {
MouseEventHandler::new::<AddChannel, _>(0, cx, |state, _| { if cx
render_icon_button( .global::<DragAndDrop<Workspace>>()
theme .currently_dragged::<Channel>(cx.window())
.collab_panel .is_some()
.add_contact_button && self.drag_target_channel == ChannelDragTarget::Root
.style_for(is_selected, state), {
"icons/plus.svg", is_dragged_over = true;
) }
})
.with_cursor_style(CursorStyle::PointingHand) Some(
.on_click(MouseButton::Left, |_, this, cx| this.new_root_channel(cx)) MouseEventHandler::new::<AddChannel, _>(0, cx, |state, _| {
.with_tooltip::<AddChannel>( render_icon_button(
0, theme
"Create a channel", .collab_panel
None, .add_contact_button
tooltip_style.clone(), .style_for(is_selected, state),
cx, "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, _ => None,
}; };
@ -1719,9 +1642,37 @@ impl CollabPanel {
.constrained() .constrained()
.with_height(theme.collab_panel.row_height) .with_height(theme.collab_panel.row_height)
.contained() .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 { if can_collapse {
result = result result = result
.with_cursor_style(CursorStyle::PointingHand) .with_cursor_style(CursorStyle::PointingHand)
@ -1972,24 +1923,23 @@ impl CollabPanel {
&self, &self,
channel: &Channel, channel: &Channel,
depth: usize, depth: usize,
path: ChannelPath,
theme: &theme::Theme, theme: &theme::Theme,
is_selected: bool, is_selected: bool,
has_children: bool,
ix: usize, ix: usize,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> AnyElement<Self> { ) -> AnyElement<Self> {
let channel_id = channel.id; let channel_id = channel.id;
let collab_theme = &theme.collab_panel; let collab_theme = &theme.collab_panel;
let has_children = self.channel_store.read(cx).has_children(channel_id);
let is_public = self let is_public = self
.channel_store .channel_store
.read(cx) .read(cx)
.channel_for_id(channel_id) .channel_for_id(channel_id)
.map(|channel| channel.visibility) .map(|channel| channel.visibility)
== Some(proto::ChannelVisibility::Public); == Some(proto::ChannelVisibility::Public);
let other_selected = let other_selected = self.selected_channel().map(|channel| channel.id) == Some(channel.id);
self.selected_channel().map(|channel| channel.0.id) == Some(channel.id); let disclosed =
let disclosed = has_children.then(|| !self.collapsed_channels.binary_search(&path).is_ok()); has_children.then(|| !self.collapsed_channels.binary_search(&channel.id).is_ok());
let is_active = iife!({ let is_active = iife!({
let call_channel = ActiveCall::global(cx) let call_channel = ActiveCall::global(cx)
@ -2012,13 +1962,9 @@ impl CollabPanel {
let mut is_dragged_over = false; let mut is_dragged_over = false;
if cx if cx
.global::<DragAndDrop<Workspace>>() .global::<DragAndDrop<Workspace>>()
.currently_dragged::<DraggedChannel>(cx.window()) .currently_dragged::<Channel>(cx.window())
.is_some() .is_some()
&& self && self.drag_target_channel == ChannelDragTarget::Channel(channel_id)
.drag_target_channel
.as_ref()
.filter(|(_, dragged_path)| path.starts_with(dragged_path))
.is_some()
{ {
is_dragged_over = true; is_dragged_over = true;
} }
@ -2201,7 +2147,7 @@ impl CollabPanel {
.disclosable( .disclosable(
disclosed, disclosed,
Box::new(ToggleCollapse { Box::new(ToggleCollapse {
location: path.clone(), location: channel.id.clone(),
}), }),
) )
.with_id(ix) .with_id(ix)
@ -2221,7 +2167,7 @@ impl CollabPanel {
) )
}) })
.on_click(MouseButton::Left, move |_, this, cx| { .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 { if is_active {
this.open_channel_notes(&OpenChannelNotes { channel_id }, cx) this.open_channel_notes(&OpenChannelNotes { channel_id }, cx)
} else { } else {
@ -2230,76 +2176,43 @@ impl CollabPanel {
} }
}) })
.on_click(MouseButton::Right, { .on_click(MouseButton::Right, {
let path = path.clone(); let channel = channel.clone();
move |e, this, cx| { 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 if let Some((_, dragged_channel)) = cx
.global::<DragAndDrop<Workspace>>() .global::<DragAndDrop<Workspace>>()
.currently_dragged::<DraggedChannel>(cx.window()) .currently_dragged::<Channel>(cx.window())
{ {
if e.modifiers.alt { this.channel_store
this.channel_store.update(cx, |channel_store, cx| { .update(cx, |channel_store, cx| {
channel_store channel_store.move_channel(dragged_channel.id, Some(channel_id), cx)
.link_channel(dragged_channel.0.id, channel_id, cx)
.detach_and_log_err(cx)
}) })
} else { .detach_and_log_err(cx)
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)
})
}
} }
}) })
.on_move({ .on_move({
let channel = channel.clone(); let channel = channel.clone();
let path = path.clone();
move |_, this, cx| { move |_, this, cx| {
if let Some((_, _dragged_channel)) = if let Some((_, dragged_channel)) = cx
cx.global::<DragAndDrop<Workspace>>() .global::<DragAndDrop<Workspace>>()
.currently_dragged::<DraggedChannel>(cx.window()) .currently_dragged::<Channel>(cx.window())
{ {
match &this.drag_target_channel { if channel.id != dragged_channel.id {
Some(current_target) this.drag_target_channel = ChannelDragTarget::Channel(channel.id);
if current_target.0 == channel && current_target.1 == path =>
{
return
}
_ => {
this.drag_target_channel = Some((channel.clone(), path.clone()));
cx.notify();
}
} }
cx.notify()
} }
} }
}) })
.as_draggable( .as_draggable::<_, Channel>(
(channel.clone(), path.parent_id()), channel.clone(),
move |modifiers, (channel, _), cx: &mut ViewContext<Workspace>| { move |_, channel, cx: &mut ViewContext<Workspace>| {
let theme = &theme::current(cx).collab_panel; let theme = &theme::current(cx).collab_panel;
Flex::<Workspace>::row() 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( .with_child(
Svg::new("icons/hash.svg") Svg::new("icons/hash.svg")
.with_color(theme.channel_hash.color) .with_color(theme.channel_hash.color)
@ -2631,39 +2544,29 @@ impl CollabPanel {
} }
fn has_subchannels(&self, ix: usize) -> bool { fn has_subchannels(&self, ix: usize) -> bool {
self.entries self.entries.get(ix).map_or(false, |entry| {
.get(ix) if let ListEntry::Channel { has_children, .. } = entry {
.zip(self.entries.get(ix + 1)) *has_children
.map(|entries| match entries { } else {
( false
ListEntry::Channel { }
path: this_path, .. })
},
ListEntry::Channel {
path: next_path, ..
},
) => next_path.starts_with(this_path),
_ => false,
})
.unwrap_or(false)
} }
fn deploy_channel_context_menu( fn deploy_channel_context_menu(
&mut self, &mut self,
position: Option<Vector2F>, position: Option<Vector2F>,
path: &ChannelPath, channel: &Channel,
ix: usize, ix: usize,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) { ) {
self.context_menu_on_selected = position.is_none(); self.context_menu_on_selected = position.is_none();
let channel_name = self.channel_clipboard.as_ref().and_then(|channel| { let clipboard_channel_name = self.channel_clipboard.as_ref().and_then(|clipboard| {
let channel_name = self self.channel_store
.channel_store
.read(cx) .read(cx)
.channel_for_id(channel.channel_id) .channel_for_id(clipboard.channel_id)
.map(|channel| channel.name.clone())?; .map(|channel| channel.name.clone())
Some(channel_name)
}); });
self.context_menu.update(cx, |context_menu, cx| { self.context_menu.update(cx, |context_menu, cx| {
@ -2687,7 +2590,7 @@ impl CollabPanel {
)); ));
if self.has_subchannels(ix) { 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" "Expand Subchannels"
} else { } else {
"Collapse Subchannels" "Collapse Subchannels"
@ -2695,7 +2598,7 @@ impl CollabPanel {
items.push(ContextMenuItem::action( items.push(ContextMenuItem::action(
expand_action_name, expand_action_name,
ToggleCollapse { ToggleCollapse {
location: path.clone(), location: channel.id,
}, },
)); ));
} }
@ -2703,84 +2606,52 @@ impl CollabPanel {
items.push(ContextMenuItem::action( items.push(ContextMenuItem::action(
"Open Notes", "Open Notes",
OpenChannelNotes { OpenChannelNotes {
channel_id: path.channel_id(), channel_id: channel.id,
}, },
)); ));
items.push(ContextMenuItem::action( items.push(ContextMenuItem::action(
"Open Chat", "Open Chat",
JoinChannelChat { JoinChannelChat {
channel_id: path.channel_id(), channel_id: channel.id,
}, },
)); ));
items.push(ContextMenuItem::action( items.push(ContextMenuItem::action(
"Copy Channel Link", "Copy Channel Link",
CopyChannelLink { CopyChannelLink {
channel_id: path.channel_id(), channel_id: channel.id,
}, },
)); ));
if self.channel_store.read(cx).is_user_admin(path.channel_id()) { if self.channel_store.read(cx).is_channel_admin(channel.id) {
let parent_id = path.parent_id();
items.extend([ items.extend([
ContextMenuItem::Separator, ContextMenuItem::Separator,
ContextMenuItem::action( ContextMenuItem::action(
"New Subchannel", "New Subchannel",
NewChannel { NewChannel {
location: path.clone(), location: channel.id,
}, },
), ),
ContextMenuItem::action( ContextMenuItem::action(
"Rename", "Rename",
RenameChannel { 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( ContextMenuItem::action(
"Move this channel", "Move this channel",
StartMoveChannelFor { StartMoveChannelFor {
channel_id: path.channel_id(), channel_id: channel.id,
parent_id,
},
),
ContextMenuItem::action(
"Link this channel",
StartLinkChannelFor {
channel_id: path.channel_id(),
parent_id,
}, },
), ),
]); ]);
if let Some(channel_name) = channel_name { if let Some(channel_name) = clipboard_channel_name {
items.push(ContextMenuItem::Separator); items.push(ContextMenuItem::Separator);
items.push(ContextMenuItem::action( items.push(ContextMenuItem::action(
format!("Move '#{}' here", channel_name), format!("Move '#{}' here", channel_name),
MoveChannel { MoveChannel { to: channel.id },
to: path.channel_id(),
},
));
items.push(ContextMenuItem::action(
format!("Link '#{}' here", channel_name),
LinkChannel {
to: path.channel_id(),
},
)); ));
} }
@ -2789,20 +2660,20 @@ impl CollabPanel {
ContextMenuItem::action( ContextMenuItem::action(
"Invite Members", "Invite Members",
InviteMembers { InviteMembers {
channel_id: path.channel_id(), channel_id: channel.id,
}, },
), ),
ContextMenuItem::action( ContextMenuItem::action(
"Manage Members", "Manage Members",
ManageMembers { ManageMembers {
channel_id: path.channel_id(), channel_id: channel.id,
}, },
), ),
ContextMenuItem::Separator, ContextMenuItem::Separator,
ContextMenuItem::action( ContextMenuItem::action(
"Delete", "Delete",
RemoveChannel { RemoveChannel {
channel_id: path.channel_id(), channel_id: channel.id,
}, },
), ),
]); ]);
@ -2973,11 +2844,7 @@ impl CollabPanel {
self.channel_store self.channel_store
.update(cx, |channel_store, cx| { .update(cx, |channel_store, cx| {
channel_store.create_channel( channel_store.create_channel(&channel_name, *location, cx)
&channel_name,
location.as_ref().map(|location| location.channel_id()),
cx,
)
}) })
.detach(); .detach();
cx.notify(); cx.notify();
@ -2994,7 +2861,7 @@ impl CollabPanel {
self.channel_store self.channel_store
.update(cx, |channel_store, cx| { .update(cx, |channel_store, cx| {
channel_store.rename(location.channel_id(), &channel_name, cx) channel_store.rename(*location, &channel_name, cx)
}) })
.detach(); .detach();
cx.notify(); cx.notify();
@ -3021,33 +2888,27 @@ impl CollabPanel {
_: &CollapseSelectedChannel, _: &CollapseSelectedChannel,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) { ) {
let Some((_, path)) = self let Some(channel_id) = self.selected_channel().map(|channel| channel.id) else {
.selected_channel()
.map(|(channel, parent)| (channel.id, parent))
else {
return; return;
}; };
if self.is_channel_collapsed(&path) { if self.is_channel_collapsed(channel_id) {
return; 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>) { fn expand_selected_channel(&mut self, _: &ExpandSelectedChannel, cx: &mut ViewContext<Self>) {
let Some((_, path)) = self let Some(id) = self.selected_channel().map(|channel| channel.id) else {
.selected_channel()
.map(|(channel, parent)| (channel.id, parent))
else {
return; return;
}; };
if !self.is_channel_collapsed(&path) { if !self.is_channel_collapsed(id) {
return; return;
} }
self.toggle_channel_collapsed(path.to_owned(), cx) self.toggle_channel_collapsed(id, cx)
} }
fn toggle_channel_collapsed_action( fn toggle_channel_collapsed_action(
@ -3055,21 +2916,16 @@ impl CollabPanel {
action: &ToggleCollapse, action: &ToggleCollapse,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) { ) {
self.toggle_channel_collapsed(&action.location, cx); self.toggle_channel_collapsed(action.location, cx);
} }
fn toggle_channel_collapsed<'a>( fn toggle_channel_collapsed<'a>(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
&mut self, match self.collapsed_channels.binary_search(&channel_id) {
path: impl Into<Cow<'a, ChannelPath>>,
cx: &mut ViewContext<Self>,
) {
let path = path.into();
match self.collapsed_channels.binary_search(&path) {
Ok(ix) => { Ok(ix) => {
self.collapsed_channels.remove(ix); self.collapsed_channels.remove(ix);
} }
Err(ix) => { Err(ix) => {
self.collapsed_channels.insert(ix, path.into_owned()); self.collapsed_channels.insert(ix, channel_id);
} }
}; };
self.serialize(cx); self.serialize(cx);
@ -3078,8 +2934,8 @@ impl CollabPanel {
cx.focus_self(); cx.focus_self();
} }
fn is_channel_collapsed(&self, path: &ChannelPath) -> bool { fn is_channel_collapsed(&self, channel_id: ChannelId) -> bool {
self.collapsed_channels.binary_search(path).is_ok() self.collapsed_channels.binary_search(&channel_id).is_ok()
} }
fn leave_call(cx: &mut ViewContext<Self>) { fn leave_call(cx: &mut ViewContext<Self>) {
@ -3142,16 +2998,16 @@ impl CollabPanel {
} }
fn remove(&mut self, _: &Remove, cx: &mut ViewContext<Self>) { 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) self.remove_channel(channel.id, cx)
} }
} }
fn rename_selected_channel(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext<Self>) { 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( self.rename_channel(
&RenameChannel { &RenameChannel {
location: parent.to_owned(), channel_id: channel.id,
}, },
cx, cx,
); );
@ -3160,15 +3016,12 @@ impl CollabPanel {
fn rename_channel(&mut self, action: &RenameChannel, cx: &mut ViewContext<Self>) { fn rename_channel(&mut self, action: &RenameChannel, cx: &mut ViewContext<Self>) {
let channel_store = self.channel_store.read(cx); 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; return;
} }
if let Some(channel) = channel_store if let Some(channel) = channel_store.channel_for_id(action.channel_id).cloned() {
.channel_for_id(action.location.channel_id())
.cloned()
{
self.channel_editing_state = Some(ChannelEditingState::Rename { self.channel_editing_state = Some(ChannelEditingState::Rename {
location: action.location.to_owned(), location: action.channel_id.to_owned(),
pending_name: None, pending_name: None,
}); });
self.channel_name_editor.update(cx, |editor, cx| { 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>) { 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; 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 self.selection
.and_then(|ix| self.entries.get(ix)) .and_then(|ix| self.entries.get(ix))
.and_then(|entry| match entry { .and_then(|entry| match entry {
ListEntry::Channel { ListEntry::Channel { channel, .. } => Some(channel),
channel,
path: parent,
..
} => Some((channel, parent)),
_ => None, _ => None,
}) })
} }
@ -3620,19 +3469,13 @@ impl PartialEq for ListEntry {
} }
} }
ListEntry::Channel { ListEntry::Channel {
channel: channel_1, channel: channel_1, ..
depth: depth_1,
path: parent_1,
} => { } => {
if let ListEntry::Channel { if let ListEntry::Channel {
channel: channel_2, channel: channel_2, ..
depth: depth_2,
path: parent_2,
} = other } = other
{ {
return channel_1.id == channel_2.id return channel_1.id == channel_2.id;
&& depth_1 == depth_2
&& parent_1 == parent_2;
} }
} }
ListEntry::ChannelNotes { channel_id } => { ListEntry::ChannelNotes { channel_id } => {

View file

@ -88,8 +88,10 @@ impl View for CollabTitlebarItem {
.zip(peer_id) .zip(peer_id)
.zip(ActiveCall::global(cx).read(cx).room().cloned()) .zip(ActiveCall::global(cx).read(cx).room().cloned())
{ {
right_container if room.read(cx).can_publish() {
.add_children(self.render_in_call_share_unshare_button(&workspace, &theme, cx)); right_container
.add_children(self.render_in_call_share_unshare_button(&workspace, &theme, cx));
}
right_container.add_child(self.render_leave_call(&theme, cx)); right_container.add_child(self.render_leave_call(&theme, cx));
let muted = room.read(cx).is_muted(cx); let muted = room.read(cx).is_muted(cx);
let speaking = room.read(cx).is_speaking(); 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), self.render_current_user(&workspace, &theme, &user, peer_id, muted, speaking, cx),
); );
left_container.add_children(self.render_collaborators(&workspace, &theme, &room, 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_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(); let status = workspace.read(cx).client().status();

View file

@ -477,7 +477,7 @@ impl NotificationPanel {
return panel.read_with(cx, |panel, cx| { return panel.read_with(cx, |panel, cx| {
panel.is_scrolled_to_bottom() panel.is_scrolled_to_bottom()
&& panel.active_chat().map_or(false, |chat| { && panel.active_chat().map_or(false, |chat| {
chat.read(cx).channel().id == *channel_id chat.read(cx).channel_id == *channel_id
}) })
}); });
} }

View file

@ -966,8 +966,11 @@ impl CompletionsMenu {
) { ) {
if self.selected_item > 0 { if self.selected_item > 0 {
self.selected_item -= 1; 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.list.scroll_to(ScrollTarget::Show(self.selected_item));
self.attempt_resolve_selected_completion_documentation(project, cx); self.attempt_resolve_selected_completion_documentation(project, cx);
cx.notify(); cx.notify();
} }
@ -979,8 +982,10 @@ impl CompletionsMenu {
) { ) {
if self.selected_item + 1 < self.matches.len() { if self.selected_item + 1 < self.matches.len() {
self.selected_item += 1; 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); self.attempt_resolve_selected_completion_documentation(project, cx);
cx.notify(); cx.notify();
} }
@ -1532,17 +1537,23 @@ impl CodeActionsMenu {
fn select_prev(&mut self, cx: &mut ViewContext<Editor>) { fn select_prev(&mut self, cx: &mut ViewContext<Editor>) {
if self.selected_item > 0 { if self.selected_item > 0 {
self.selected_item -= 1; self.selected_item -= 1;
} else {
self.selected_item = self.actions.len() - 1;
self.list.scroll_to(ScrollTarget::Show(self.selected_item)); 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>) { fn select_next(&mut self, cx: &mut ViewContext<Editor>) {
if self.selected_item + 1 < self.actions.len() { if self.selected_item + 1 < self.actions.len() {
self.selected_item += 1; self.selected_item += 1;
self.list.scroll_to(ScrollTarget::Show(self.selected_item)); 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>) { fn select_last(&mut self, cx: &mut ViewContext<Editor>) {
@ -6542,7 +6553,7 @@ impl Editor {
{ {
if selections if selections
.iter() .iter()
.find(|selection| selection.equals(&offset_range)) .find(|selection| selection.range().overlaps(&offset_range))
.is_none() .is_none()
{ {
next_selected_range = Some(offset_range); next_selected_range = Some(offset_range);

View file

@ -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; let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
project.update(cx, |project, _| { project.update(cx, |project, _| {
project.enable_test_prettier(&[]);
project.languages().add(Arc::new(language)); project.languages().add(Arc::new(language));
}); });
let buffer = project 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; fs.insert_file("/file.rs", Default::default()).await;
let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
let prettier_format_suffix = project.update(cx, |project, _| { let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
let suffix = project.enable_test_prettier(&[test_plugin]); project.update(cx, |project, _| {
project.languages().add(Arc::new(language)); project.languages().add(Arc::new(language));
suffix
}); });
let buffer = project let buffer = project
.update(cx, |project, cx| project.open_local_buffer("/file.rs", cx)) .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx))

View file

@ -369,6 +369,30 @@ pub fn find_boundary(
map.clip_point(offset.to_display_point(map), Bias::Right) 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 { pub fn is_inside_word(map: &DisplaySnapshot, point: DisplayPoint) -> bool {
let raw_point = point.to_point(map); let raw_point = point.to_point(map);
let scope = map.buffer_snapshot.language_scope_at(raw_point); 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); let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
assert_eq!( assert_eq!(
surrounding_word(&snapshot, display_points[1]), 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ˇ ˇ ˇipsum", cx);
assert("lorem\nˇˇˇ\nipsum", cx); assert("lorem\nˇˇˇ\nipsum", cx);
assert("lorem\nˇˇipsumˇ", cx); assert("lorem\nˇˇipsumˇ", cx);
assert("lorem,ˇˇ ˇipsum", cx); assert("loremˇ,ˇˇ ipsum", cx);
assert("ˇloremˇˇ, ipsum", cx); assert("ˇloremˇˇ, ipsum", cx);
} }

View file

@ -373,8 +373,8 @@ pub(crate) struct DiagnosticEndpoint {
#[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Debug)] #[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Debug)]
pub enum CharKind { pub enum CharKind {
Punctuation,
Whitespace, Whitespace,
Punctuation,
Word, Word,
} }

View file

@ -306,6 +306,16 @@ impl live_kit_server::api::Client for TestApiClient {
token::VideoGrant::to_join(room), 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; pub type Sid = String;

View file

@ -12,6 +12,7 @@ pub trait Client: Send + Sync {
async fn delete_room(&self, name: String) -> Result<()>; async fn delete_room(&self, name: String) -> Result<()>;
async fn remove_participant(&self, room: String, identity: String) -> Result<()>; async fn remove_participant(&self, room: String, identity: String) -> Result<()>;
fn room_token(&self, room: &str, identity: &str) -> Result<String>; fn room_token(&self, room: &str, identity: &str) -> Result<String>;
fn guest_token(&self, room: &str, identity: &str) -> Result<String>;
} }
#[derive(Clone)] #[derive(Clone)]
@ -138,4 +139,13 @@ impl Client for LiveKitClient {
token::VideoGrant::to_join(room), 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),
)
}
} }

View file

@ -57,6 +57,15 @@ impl<'a> VideoGrant<'a> {
..Default::default() ..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( pub fn create(

View file

@ -220,96 +220,31 @@ impl NodeRuntime for RealNodeRuntime {
} }
} }
pub struct FakeNodeRuntime(Option<PrettierSupport>); pub struct FakeNodeRuntime;
struct PrettierSupport {
plugins: Vec<&'static str>,
}
impl FakeNodeRuntime { impl FakeNodeRuntime {
pub fn new() -> Arc<dyn NodeRuntime> { pub fn new() -> Arc<dyn NodeRuntime> {
Arc::new(FakeNodeRuntime(None)) Arc::new(Self)
}
pub fn with_prettier_support(plugins: &[&'static str]) -> Arc<dyn NodeRuntime> {
Arc::new(FakeNodeRuntime(Some(PrettierSupport::new(plugins))))
} }
} }
#[async_trait::async_trait] #[async_trait::async_trait]
impl NodeRuntime for FakeNodeRuntime { impl NodeRuntime for FakeNodeRuntime {
async fn binary_path(&self) -> anyhow::Result<PathBuf> { async fn binary_path(&self) -> anyhow::Result<PathBuf> {
if let Some(prettier_support) = &self.0 { unreachable!()
prettier_support.binary_path().await
} else {
unreachable!()
}
} }
async fn run_npm_subcommand( async fn run_npm_subcommand(
&self, &self,
directory: Option<&Path>, _: Option<&Path>,
subcommand: &str, subcommand: &str,
args: &[&str], args: &[&str],
) -> anyhow::Result<Output> { ) -> anyhow::Result<Output> {
if let Some(prettier_support) = &self.0 { unreachable!("Should not run npm subcommand '{subcommand}' with args {args:?}")
prettier_support
.run_npm_subcommand(directory, subcommand, args)
.await
} else {
unreachable!()
}
} }
async fn npm_package_latest_version(&self, name: &str) -> anyhow::Result<String> { async fn npm_package_latest_version(&self, name: &str) -> anyhow::Result<String> {
if let Some(prettier_support) = &self.0 { unreachable!("Should not query npm package '{name}' for latest version")
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!()
}
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}")
}
} }
async fn npm_install_packages( async fn npm_install_packages(
@ -317,32 +252,6 @@ impl NodeRuntime for PrettierSupport {
_: &Path, _: &Path,
packages: &[(&str, &str)], packages: &[(&str, &str)],
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
assert_eq!( unreachable!("Should not install packages {packages:?}")
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(())
} }
} }

View file

@ -44,6 +44,9 @@ pub const PRETTIER_SERVER_JS: &str = include_str!("./prettier_server.js");
const PRETTIER_PACKAGE_NAME: &str = "prettier"; const PRETTIER_PACKAGE_NAME: &str = "prettier";
const TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME: &str = "prettier-plugin-tailwindcss"; 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 { impl Prettier {
pub const CONFIG_FILE_NAMES: &'static [&'static str] = &[ pub const CONFIG_FILE_NAMES: &'static [&'static str] = &[
".prettierrc", ".prettierrc",
@ -60,9 +63,6 @@ impl Prettier {
".editorconfig", ".editorconfig",
]; ];
#[cfg(any(test, feature = "test-support"))]
pub const FORMAT_SUFFIX: &str = "\nformatted by test prettier";
pub async fn locate( pub async fn locate(
starting_path: Option<LocateStart>, starting_path: Option<LocateStart>,
fs: Arc<dyn Fs>, fs: Arc<dyn Fs>,
@ -349,7 +349,7 @@ impl Prettier {
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
Self::Test(_) => Ok(buffer Self::Test(_) => Ok(buffer
.read_with(cx, |buffer, cx| { .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) buffer.diff(formatted_text, cx)
}) })
.await), .await),

View file

@ -53,7 +53,7 @@ use lsp::{
use lsp_command::*; use lsp_command::*;
use node_runtime::NodeRuntime; use node_runtime::NodeRuntime;
use postage::watch; use postage::watch;
use prettier::{LocateStart, Prettier, PRETTIER_SERVER_FILE, PRETTIER_SERVER_JS}; use prettier::{LocateStart, Prettier};
use project_settings::{LspSettings, ProjectSettings}; use project_settings::{LspSettings, ProjectSettings};
use rand::prelude::*; use rand::prelude::*;
use search::SearchQuery; use search::SearchQuery;
@ -79,16 +79,15 @@ use std::{
time::{Duration, Instant}, time::{Duration, Instant},
}; };
use terminals::Terminals; use terminals::Terminals;
use text::{Anchor, LineEnding, Rope}; use text::Anchor;
use util::{ use util::{
debug_panic, defer, debug_panic, defer, http::HttpClient, merge_json_value_into,
http::HttpClient, paths::LOCAL_SETTINGS_RELATIVE_PATH, post_inc, ResultExt, TryFutureExt as _,
merge_json_value_into,
paths::{DEFAULT_PRETTIER_DIR, LOCAL_SETTINGS_RELATIVE_PATH},
post_inc, ResultExt, TryFutureExt as _,
}; };
pub use fs::*; pub use fs::*;
#[cfg(any(test, feature = "test-support"))]
pub use prettier::FORMAT_SUFFIX as TEST_PRETTIER_FORMAT_SUFFIX;
pub use worktree::*; pub use worktree::*;
pub trait Item { pub trait Item {
@ -836,16 +835,6 @@ impl Project {
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>) { fn on_settings_changed(&mut self, cx: &mut ModelContext<Self>) {
let mut language_servers_to_start = Vec::new(); let mut language_servers_to_start = Vec::new();
let mut language_formatters_to_check = 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( fn install_default_formatters(
&self, &self,
worktree: Option<WorktreeId>, worktree: Option<WorktreeId>,
@ -8519,7 +8520,7 @@ impl Project {
return Task::ready(Ok(())); 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 let already_running_prettier = self
.prettier_instances .prettier_instances
.get(&(worktree, default_prettier_dir.to_path_buf())) .get(&(worktree, default_prettier_dir.to_path_buf()))
@ -8528,10 +8529,10 @@ impl Project {
let fs = Arc::clone(&self.fs); let fs = Arc::clone(&self.fs);
cx.background() cx.background()
.spawn(async move { .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 // method creates parent directory if it doesn't exist
fs.save(&prettier_wrapper_path, &Rope::from(PRETTIER_SERVER_JS), LineEnding::Unix).await fs.save(&prettier_wrapper_path, &text::Rope::from(prettier::PRETTIER_SERVER_JS), text::LineEnding::Unix).await
.with_context(|| format!("writing {PRETTIER_SERVER_FILE} file at {prettier_wrapper_path:?}"))?; .with_context(|| format!("writing {} file at {prettier_wrapper_path:?}", prettier::PRETTIER_SERVER_FILE))?;
let packages_to_versions = future::try_join_all( let packages_to_versions = future::try_join_all(
prettier_plugins prettier_plugins

View file

@ -171,8 +171,6 @@ message Envelope {
AckChannelMessage ack_channel_message = 143; AckChannelMessage ack_channel_message = 143;
GetChannelMessagesById get_channel_messages_by_id = 144; GetChannelMessagesById get_channel_messages_by_id = 144;
LinkChannel link_channel = 145;
UnlinkChannel unlink_channel = 146;
MoveChannel move_channel = 147; MoveChannel move_channel = 147;
SetChannelVisibility set_channel_visibility = 148; SetChannelVisibility set_channel_visibility = 148;
@ -342,6 +340,7 @@ message RoomUpdated {
message LiveKitConnectionInfo { message LiveKitConnectionInfo {
string server_url = 1; string server_url = 1;
string token = 2; string token = 2;
bool can_publish = 3;
} }
message ShareProject { message ShareProject {
@ -971,13 +970,10 @@ message LspDiskBasedDiagnosticsUpdated {}
message UpdateChannels { message UpdateChannels {
repeated Channel channels = 1; repeated Channel channels = 1;
repeated ChannelEdge insert_edge = 2;
repeated ChannelEdge delete_edge = 3;
repeated uint64 delete_channels = 4; repeated uint64 delete_channels = 4;
repeated Channel channel_invitations = 5; repeated Channel channel_invitations = 5;
repeated uint64 remove_channel_invitations = 6; repeated uint64 remove_channel_invitations = 6;
repeated ChannelParticipants channel_participants = 7; repeated ChannelParticipants channel_participants = 7;
repeated ChannelPermission channel_permissions = 8;
repeated UnseenChannelMessage unseen_channel_messages = 9; repeated UnseenChannelMessage unseen_channel_messages = 9;
repeated UnseenChannelBufferChange unseen_channel_buffer_changes = 10; repeated UnseenChannelBufferChange unseen_channel_buffer_changes = 10;
} }
@ -993,11 +989,6 @@ message UnseenChannelBufferChange {
repeated VectorClockEntry version = 3; repeated VectorClockEntry version = 3;
} }
message ChannelEdge {
uint64 channel_id = 1;
uint64 parent_id = 2;
}
message ChannelPermission { message ChannelPermission {
uint64 channel_id = 1; uint64 channel_id = 1;
ChannelRole role = 3; ChannelRole role = 3;
@ -1137,20 +1128,9 @@ message GetChannelMessagesById {
repeated uint64 message_ids = 1; 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 { message MoveChannel {
uint64 channel_id = 1; uint64 channel_id = 1;
uint64 from = 2; optional uint64 to = 2;
uint64 to = 3;
} }
message JoinChannelBuffer { message JoinChannelBuffer {
@ -1585,6 +1565,8 @@ message Channel {
uint64 id = 1; uint64 id = 1;
string name = 2; string name = 2;
ChannelVisibility visibility = 3; ChannelVisibility visibility = 3;
ChannelRole role = 4;
repeated uint64 parent_path = 5;
} }
message Contact { message Contact {

View file

@ -210,7 +210,6 @@ messages!(
(LeaveChannelChat, Foreground), (LeaveChannelChat, Foreground),
(LeaveProject, Foreground), (LeaveProject, Foreground),
(LeaveRoom, Foreground), (LeaveRoom, Foreground),
(LinkChannel, Foreground),
(MarkNotificationRead, Foreground), (MarkNotificationRead, Foreground),
(MoveChannel, Foreground), (MoveChannel, Foreground),
(OnTypeFormatting, Background), (OnTypeFormatting, Background),
@ -263,7 +262,6 @@ messages!(
(SynchronizeBuffersResponse, Foreground), (SynchronizeBuffersResponse, Foreground),
(Test, Foreground), (Test, Foreground),
(Unfollow, Foreground), (Unfollow, Foreground),
(UnlinkChannel, Foreground),
(UnshareProject, Foreground), (UnshareProject, Foreground),
(UpdateBuffer, Foreground), (UpdateBuffer, Foreground),
(UpdateBufferFile, Foreground), (UpdateBufferFile, Foreground),
@ -327,7 +325,6 @@ request_messages!(
(JoinRoom, JoinRoomResponse), (JoinRoom, JoinRoomResponse),
(LeaveChannelBuffer, Ack), (LeaveChannelBuffer, Ack),
(LeaveRoom, Ack), (LeaveRoom, Ack),
(LinkChannel, Ack),
(MarkNotificationRead, Ack), (MarkNotificationRead, Ack),
(MoveChannel, Ack), (MoveChannel, Ack),
(OnTypeFormatting, OnTypeFormattingResponse), (OnTypeFormatting, OnTypeFormattingResponse),
@ -362,7 +359,6 @@ request_messages!(
(ShareProject, ShareProjectResponse), (ShareProject, ShareProjectResponse),
(SynchronizeBuffers, SynchronizeBuffersResponse), (SynchronizeBuffers, SynchronizeBuffersResponse),
(Test, Test), (Test, Test),
(UnlinkChannel, Ack),
(UpdateBuffer, Ack), (UpdateBuffer, Ack),
(UpdateParticipantLocation, Ack), (UpdateParticipantLocation, Ack),
(UpdateProject, Ack), (UpdateProject, Ack),

View file

@ -41,6 +41,7 @@ pub struct EmbeddingQueue {
pending_batch_token_count: usize, pending_batch_token_count: usize,
finished_files_tx: channel::Sender<FileToEmbed>, finished_files_tx: channel::Sender<FileToEmbed>,
finished_files_rx: channel::Receiver<FileToEmbed>, finished_files_rx: channel::Receiver<FileToEmbed>,
api_key: Option<String>,
} }
#[derive(Clone)] #[derive(Clone)]
@ -50,7 +51,11 @@ pub struct FileFragmentToEmbed {
} }
impl EmbeddingQueue { 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(); let (finished_files_tx, finished_files_rx) = channel::unbounded();
Self { Self {
embedding_provider, embedding_provider,
@ -59,9 +64,14 @@ impl EmbeddingQueue {
pending_batch_token_count: 0, pending_batch_token_count: 0,
finished_files_tx, finished_files_tx,
finished_files_rx, 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) { pub fn push(&mut self, file: FileToEmbed) {
if file.spans.is_empty() { if file.spans.is_empty() {
self.finished_files_tx.try_send(file).unwrap(); self.finished_files_tx.try_send(file).unwrap();
@ -108,6 +118,7 @@ impl EmbeddingQueue {
let finished_files_tx = self.finished_files_tx.clone(); let finished_files_tx = self.finished_files_tx.clone();
let embedding_provider = self.embedding_provider.clone(); let embedding_provider = self.embedding_provider.clone();
let api_key = self.api_key.clone();
self.executor self.executor
.spawn(async move { .spawn(async move {
@ -132,7 +143,7 @@ impl EmbeddingQueue {
return; return;
}; };
match embedding_provider.embed_batch(spans).await { match embedding_provider.embed_batch(spans, api_key).await {
Ok(embeddings) => { Ok(embeddings) => {
let mut embeddings = embeddings.into_iter(); let mut embeddings = embeddings.into_iter();
for fragment in batch { for fragment in batch {

View file

@ -124,6 +124,8 @@ pub struct SemanticIndex {
_embedding_task: Task<()>, _embedding_task: Task<()>,
_parsing_files_tasks: Vec<Task<()>>, _parsing_files_tasks: Vec<Task<()>>,
projects: HashMap<WeakModelHandle<Project>, ProjectState>, projects: HashMap<WeakModelHandle<Project>, ProjectState>,
api_key: Option<String>,
embedding_queue: Arc<Mutex<EmbeddingQueue>>,
} }
struct ProjectState { struct ProjectState {
@ -269,7 +271,7 @@ pub struct SearchResult {
} }
impl SemanticIndex { 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>>() { if cx.has_global::<ModelHandle<Self>>() {
Some(cx.global::<ModelHandle<SemanticIndex>>().clone()) Some(cx.global::<ModelHandle<SemanticIndex>>().clone())
} else { } 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 { pub fn enabled(cx: &AppContext) -> bool {
settings::get::<SemanticIndexSettings>(cx).enabled settings::get::<SemanticIndexSettings>(cx).enabled
} }
pub fn status(&self, project: &ModelHandle<Project>) -> SemanticIndexStatus { pub fn status(&self, project: &ModelHandle<Project>) -> SemanticIndexStatus {
if !self.embedding_provider.is_authenticated() { if !self.is_authenticated() {
return SemanticIndexStatus::NotAuthenticated; return SemanticIndexStatus::NotAuthenticated;
} }
@ -324,7 +340,7 @@ impl SemanticIndex {
Ok(cx.add_model(|cx| { Ok(cx.add_model(|cx| {
let t0 = Instant::now(); let t0 = Instant::now();
let embedding_queue = 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 _embedding_task = cx.background().spawn({
let embedded_files = embedding_queue.finished_files(); let embedded_files = embedding_queue.finished_files();
let db = db.clone(); let db = db.clone();
@ -389,6 +405,8 @@ impl SemanticIndex {
_embedding_task, _embedding_task,
_parsing_files_tasks, _parsing_files_tasks,
projects: Default::default(), projects: Default::default(),
api_key: None,
embedding_queue
} }
})) }))
} }
@ -703,12 +721,13 @@ impl SemanticIndex {
let index = self.index_project(project.clone(), cx); let index = self.index_project(project.clone(), cx);
let embedding_provider = self.embedding_provider.clone(); let embedding_provider = self.embedding_provider.clone();
let api_key = self.api_key.clone();
cx.spawn(|this, mut cx| async move { cx.spawn(|this, mut cx| async move {
index.await?; index.await?;
let t0 = Instant::now(); let t0 = Instant::now();
let query = embedding_provider let query = embedding_provider
.embed_batch(vec![query]) .embed_batch(vec![query], api_key)
.await? .await?
.pop() .pop()
.ok_or_else(|| anyhow!("could not embed query"))?; .ok_or_else(|| anyhow!("could not embed query"))?;
@ -926,6 +945,7 @@ impl SemanticIndex {
let fs = self.fs.clone(); let fs = self.fs.clone();
let db_path = self.db.path().clone(); let db_path = self.db.path().clone();
let background = cx.background().clone(); let background = cx.background().clone();
let api_key = self.api_key.clone();
cx.background().spawn(async move { cx.background().spawn(async move {
let db = VectorDatabase::new(fs, db_path.clone(), background).await?; let db = VectorDatabase::new(fs, db_path.clone(), background).await?;
let mut results = Vec::<SearchResult>::new(); let mut results = Vec::<SearchResult>::new();
@ -940,10 +960,15 @@ impl SemanticIndex {
.parse_file_with_template(None, &snapshot.text(), language) .parse_file_with_template(None, &snapshot.text(), language)
.log_err() .log_err()
.unwrap_or_default(); .unwrap_or_default();
if Self::embed_spans(&mut spans, embedding_provider.as_ref(), &db) if Self::embed_spans(
.await &mut spans,
.log_err() embedding_provider.as_ref(),
.is_some() &db,
api_key.clone(),
)
.await
.log_err()
.is_some()
{ {
for span in spans { for span in spans {
let similarity = span.embedding.unwrap().similarity(&query); let similarity = span.embedding.unwrap().similarity(&query);
@ -983,8 +1008,11 @@ impl SemanticIndex {
project: ModelHandle<Project>, project: ModelHandle<Project>,
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) -> Task<Result<()>> { ) -> Task<Result<()>> {
if !self.embedding_provider.is_authenticated() { if self.api_key.is_none() {
return Task::ready(Err(anyhow!("user is not authenticated"))); 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()) { if !self.projects.contains_key(&project.downgrade()) {
@ -1165,6 +1193,7 @@ impl SemanticIndex {
spans: &mut [Span], spans: &mut [Span],
embedding_provider: &dyn EmbeddingProvider, embedding_provider: &dyn EmbeddingProvider,
db: &VectorDatabase, db: &VectorDatabase,
api_key: Option<String>,
) -> Result<()> { ) -> Result<()> {
let mut batch = Vec::new(); let mut batch = Vec::new();
let mut batch_tokens = 0; let mut batch_tokens = 0;
@ -1187,7 +1216,7 @@ impl SemanticIndex {
if batch_tokens + span.token_count > embedding_provider.max_tokens_per_batch() { if batch_tokens + span.token_count > embedding_provider.max_tokens_per_batch() {
let batch_embeddings = embedding_provider let batch_embeddings = embedding_provider
.embed_batch(mem::take(&mut batch)) .embed_batch(mem::take(&mut batch), api_key.clone())
.await?; .await?;
embeddings.extend(batch_embeddings); embeddings.extend(batch_embeddings);
batch_tokens = 0; batch_tokens = 0;
@ -1199,7 +1228,7 @@ impl SemanticIndex {
if !batch.is_empty() { if !batch.is_empty() {
let batch_embeddings = embedding_provider let batch_embeddings = embedding_provider
.embed_batch(mem::take(&mut batch)) .embed_batch(mem::take(&mut batch), api_key)
.await?; .await?;
embeddings.extend(batch_embeddings); embeddings.extend(batch_embeddings);

View file

@ -11,7 +11,7 @@ use ai::{
}; };
use anyhow::Result; use anyhow::Result;
use async_trait::async_trait; 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 language::{Language, LanguageConfig, LanguageRegistry, ToOffset};
use parking_lot::Mutex; use parking_lot::Mutex;
use pretty_assertions::assert_eq; 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 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 { for file in &files {
queue.push(file.clone()); queue.push(file.clone());
} }
@ -1288,8 +1288,8 @@ impl EmbeddingProvider for FakeEmbeddingProvider {
fn base_model(&self) -> Box<dyn LanguageModel> { fn base_model(&self) -> Box<dyn LanguageModel> {
Box::new(DummyLanguageModel {}) Box::new(DummyLanguageModel {})
} }
fn is_authenticated(&self) -> bool { fn retrieve_credentials(&self, _cx: &AppContext) -> Option<String> {
true Some("Fake Credentials".to_string())
} }
fn max_tokens_per_batch(&self) -> usize { fn max_tokens_per_batch(&self) -> usize {
1000 1000
@ -1299,7 +1299,11 @@ impl EmbeddingProvider for FakeEmbeddingProvider {
None 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 self.embedding_count
.fetch_add(spans.len(), atomic::Ordering::SeqCst); .fetch_add(spans.len(), atomic::Ordering::SeqCst);

View file

@ -250,6 +250,7 @@ pub struct CollabPanel {
pub add_contact_button: Toggleable<Interactive<IconButton>>, pub add_contact_button: Toggleable<Interactive<IconButton>>,
pub add_channel_button: Toggleable<Interactive<IconButton>>, pub add_channel_button: Toggleable<Interactive<IconButton>>,
pub header_row: ContainedText, pub header_row: ContainedText,
pub dragged_over_header: ContainerStyle,
pub subheader_row: Toggleable<Interactive<ContainedText>>, pub subheader_row: Toggleable<Interactive<ContainedText>>,
pub leave_call: Interactive<ContainedText>, pub leave_call: Interactive<ContainedText>,
pub contact_row: Toggleable<Interactive<ContainerStyle>>, pub contact_row: Toggleable<Interactive<ContainerStyle>>,

View file

@ -40,6 +40,7 @@ pub enum Motion {
NextLineStart, NextLineStart,
StartOfLineDownward, StartOfLineDownward,
EndOfLineDownward, EndOfLineDownward,
GoToColumn,
} }
#[derive(Clone, Deserialize, PartialEq)] #[derive(Clone, Deserialize, PartialEq)]
@ -119,6 +120,7 @@ actions!(
NextLineStart, NextLineStart,
StartOfLineDownward, StartOfLineDownward,
EndOfLineDownward, EndOfLineDownward,
GoToColumn,
] ]
); );
impl_actions!( impl_actions!(
@ -215,6 +217,7 @@ pub fn init(cx: &mut AppContext) {
cx.add_action(|_: &mut Workspace, &EndOfLineDownward, cx: _| { cx.add_action(|_: &mut Workspace, &EndOfLineDownward, cx: _| {
motion(Motion::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: _| { cx.add_action(|_: &mut Workspace, action: &RepeatFind, cx: _| {
repeat_motion(action.backwards, cx) repeat_motion(action.backwards, cx)
}) })
@ -292,6 +295,7 @@ impl Motion {
| Right | Right
| StartOfLine { .. } | StartOfLine { .. }
| EndOfLineDownward | EndOfLineDownward
| GoToColumn
| NextWordStart { .. } | NextWordStart { .. }
| PreviousWordStart { .. } | PreviousWordStart { .. }
| FirstNonWhitespace { .. } | FirstNonWhitespace { .. }
@ -317,6 +321,7 @@ impl Motion {
| EndOfParagraph | EndOfParagraph
| StartOfLineDownward | StartOfLineDownward
| EndOfLineDownward | EndOfLineDownward
| GoToColumn
| NextWordStart { .. } | NextWordStart { .. }
| PreviousWordStart { .. } | PreviousWordStart { .. }
| FirstNonWhitespace { .. } | FirstNonWhitespace { .. }
@ -346,6 +351,7 @@ impl Motion {
| StartOfLineDownward | StartOfLineDownward
| StartOfParagraph | StartOfParagraph
| EndOfParagraph | EndOfParagraph
| GoToColumn
| NextWordStart { .. } | NextWordStart { .. }
| PreviousWordStart { .. } | PreviousWordStart { .. }
| FirstNonWhitespace { .. } | FirstNonWhitespace { .. }
@ -429,6 +435,7 @@ impl Motion {
NextLineStart => (next_line_start(map, point, times), SelectionGoal::None), NextLineStart => (next_line_start(map, point, times), SelectionGoal::None),
StartOfLineDownward => (next_line_start(map, point, times - 1), SelectionGoal::None), StartOfLineDownward => (next_line_start(map, point, times - 1), SelectionGoal::None),
EndOfLineDownward => (next_line_end(map, point, times), 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)) (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) 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( pub(crate) fn next_line_end(
map: &DisplaySnapshot, map: &DisplaySnapshot,
mut point: DisplayPoint, mut point: DisplayPoint,

View file

@ -193,10 +193,10 @@ mod test {
} }
#[gpui::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"]); let mut cx = NeovimBackedTestContext::new(cx).await.binding(["d", "e"]);
cx.assert("Teˇst Test").await; // cx.assert("Teˇst Test").await;
cx.assert("Tˇest test").await; // cx.assert("Tˇest test").await;
cx.assert(indoc! {" cx.assert(indoc! {"
Test teˇst Test teˇst
test"}) test"})

View file

@ -2,7 +2,7 @@ use std::ops::Range;
use editor::{ use editor::{
char_kind, char_kind,
display_map::DisplaySnapshot, display_map::{DisplaySnapshot, ToDisplayPoint},
movement::{self, FindRange}, movement::{self, FindRange},
Bias, CharKind, DisplayPoint, Bias, CharKind, DisplayPoint,
}; };
@ -20,6 +20,7 @@ pub enum Object {
Quotes, Quotes,
BackQuotes, BackQuotes,
DoubleQuotes, DoubleQuotes,
VerticalBars,
Parentheses, Parentheses,
SquareBrackets, SquareBrackets,
CurlyBrackets, CurlyBrackets,
@ -40,6 +41,7 @@ actions!(
Quotes, Quotes,
BackQuotes, BackQuotes,
DoubleQuotes, DoubleQuotes,
VerticalBars,
Parentheses, Parentheses,
SquareBrackets, SquareBrackets,
CurlyBrackets, 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, _: &CurlyBrackets, cx: _| object(Object::CurlyBrackets, cx));
cx.add_action(|_: &mut Workspace, _: &AngleBrackets, cx: _| object(Object::AngleBrackets, 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) { fn object(object: Object, cx: &mut WindowContext) {
@ -79,9 +82,11 @@ fn object(object: Object, cx: &mut WindowContext) {
impl Object { impl Object {
pub fn is_multiline(self) -> bool { pub fn is_multiline(self) -> bool {
match self { match self {
Object::Word { .. } | Object::Quotes | Object::BackQuotes | Object::DoubleQuotes => { Object::Word { .. }
false | Object::Quotes
} | Object::BackQuotes
| Object::VerticalBars
| Object::DoubleQuotes => false,
Object::Sentence Object::Sentence
| Object::Parentheses | Object::Parentheses
| Object::AngleBrackets | Object::AngleBrackets
@ -96,6 +101,7 @@ impl Object {
Object::Quotes Object::Quotes
| Object::BackQuotes | Object::BackQuotes
| Object::DoubleQuotes | Object::DoubleQuotes
| Object::VerticalBars
| Object::Parentheses | Object::Parentheses
| Object::SquareBrackets | Object::SquareBrackets
| Object::CurlyBrackets | Object::CurlyBrackets
@ -111,6 +117,7 @@ impl Object {
| Object::Quotes | Object::Quotes
| Object::BackQuotes | Object::BackQuotes
| Object::DoubleQuotes | Object::DoubleQuotes
| Object::VerticalBars
| Object::Parentheses | Object::Parentheses
| Object::SquareBrackets | Object::SquareBrackets
| Object::CurlyBrackets | Object::CurlyBrackets
@ -142,6 +149,9 @@ impl Object {
Object::DoubleQuotes => { Object::DoubleQuotes => {
surrounding_markers(map, relative_to, around, self.is_multiline(), '"', '"') surrounding_markers(map, relative_to, around, self.is_multiline(), '"', '"')
} }
Object::VerticalBars => {
surrounding_markers(map, relative_to, around, self.is_multiline(), '|', '|')
}
Object::Parentheses => { Object::Parentheses => {
surrounding_markers(map, relative_to, around, self.is_multiline(), '(', ')') surrounding_markers(map, relative_to, around, self.is_multiline(), '(', ')')
} }
@ -427,110 +437,151 @@ fn surrounding_markers(
relative_to: DisplayPoint, relative_to: DisplayPoint,
around: bool, around: bool,
search_across_lines: bool, search_across_lines: bool,
start_marker: char, open_marker: char,
end_marker: char, close_marker: char,
) -> Option<Range<DisplayPoint>> { ) -> Option<Range<DisplayPoint>> {
let mut matched_ends = 0; let point = relative_to.to_offset(map, Bias::Left);
let mut start = None;
for (char, mut point) in map.reverse_chars_at(relative_to) { let mut matched_closes = 0;
if char == start_marker { let mut opening = None;
if matched_ends > 0 {
matched_ends -= 1; if let Some((ch, range)) = movement::chars_after(map, point).next() {
} else { if ch == open_marker {
if around { if open_marker == close_marker {
start = Some(point) let mut total = 0;
} else { for (ch, _) in movement::chars_before(map, point) {
*point.column_mut() += char.len_utf8() as u32; if ch == '\n' {
start = Some(point) 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; if opening.is_none() {
let mut end = None; for (ch, range) in movement::chars_before(map, point) {
for (char, mut point) in map.chars_at(relative_to) { if ch == '\n' && !search_across_lines {
if char == end_marker {
if start.is_none() {
break; break;
} }
if matched_starts > 0 { if ch == open_marker {
matched_starts -= 1; if matched_closes == 0 {
} else { opening = Some(range);
if around { break;
*point.column_mut() += char.len_utf8() as u32;
end = Some(point);
} else {
end = Some(point);
} }
matched_closes -= 1;
break; } else if ch == close_marker {
matched_closes += 1
} }
} }
if char == start_marker {
if start.is_none() {
if around {
start = Some(point);
} else {
*point.column_mut() += char.len_utf8() as u32;
start = Some(point);
}
} else {
matched_starts += 1;
}
}
if char == '\n' && !search_across_lines {
break;
}
} }
let (Some(mut start), Some(mut end)) = (start, end) else { if opening.is_none() {
for (ch, range) in movement::chars_after(map, point) {
if ch == open_marker {
opening = Some(range);
break;
} else if ch == close_marker {
break;
}
}
}
let Some(mut opening) = opening else {
return None; return None;
}; };
if !around { let mut matched_opens = 0;
// if a block starts with a newline, move the start to after the newline. let mut closing = None;
let mut was_newline = false;
for (char, point) in map.chars_at(start) { for (ch, range) in movement::chars_after(map, opening.end) {
if was_newline { if ch == '\n' && !search_across_lines {
start = point;
} else if char == '\n' {
was_newline = true;
continue;
}
break; break;
} }
// if a block ends with a newline, then whitespace, then the delimeter,
// move the end to after the newline. if ch == close_marker {
let mut new_end = end; if matched_opens == 0 {
for (char, point) in map.reverse_chars_at(end) { closing = Some(range);
if char == '\n' {
end = new_end;
break; break;
} }
if !char.is_whitespace() { matched_opens -= 1;
break; } else if ch == open_marker {
} matched_opens += 1;
new_end = point
} }
} }
Some(start..end) let Some(mut closing) = closing else {
return None;
};
if around && !search_across_lines {
let mut found = false;
for (ch, range) in movement::chars_after(map, closing.end) {
if ch.is_whitespace() && ch != '\n' {
found = true;
closing.end = range.end;
} else {
break;
}
}
if !found {
for (ch, range) in movement::chars_before(map, opening.start) {
if ch.is_whitespace() && ch != '\n' {
opening.start = range.start
} else {
break;
}
}
}
}
if !around && search_across_lines {
if let Some((ch, range)) = movement::chars_after(map, opening.end).next() {
if ch == '\n' {
opening.end = range.end
}
}
for (ch, range) in movement::chars_before(map, closing.start) {
if !ch.is_whitespace() {
break;
}
if ch != '\n' {
closing.start = range.start
}
}
}
let result = if around {
opening.start..closing.end
} else {
opening.end..closing.start
};
Some(
map.clip_point(result.start.to_display_point(map), Bias::Left)
..map.clip_point(result.end.to_display_point(map), Bias::Right),
)
} }
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use indoc::indoc; use indoc::indoc;
use crate::test::{ExemptionFeatures, NeovimBackedTestContext}; use crate::{
state::Mode,
test::{ExemptionFeatures, NeovimBackedTestContext, VimTestContext},
};
const WORD_LOCATIONS: &'static str = indoc! {" const WORD_LOCATIONS: &'static str = indoc! {"
The quick ˇbrowˇnˇ The quick ˇbrowˇnˇ
@ -765,13 +816,6 @@ mod test {
let mut cx = NeovimBackedTestContext::new(cx).await; let mut cx = NeovimBackedTestContext::new(cx).await;
for (start, end) in SURROUNDING_OBJECTS { for (start, end) in SURROUNDING_OBJECTS {
if ((start == &'\'' || start == &'`' || start == &'"')
&& !ExemptionFeatures::QuotesSeekForward.supported())
|| (start == &'<' && !ExemptionFeatures::AngleBracketsFreezeNeovim.supported())
{
continue;
}
let marked_string = SURROUNDING_MARKER_STRING let marked_string = SURROUNDING_MARKER_STRING
.replace('`', &start.to_string()) .replace('`', &start.to_string())
.replace('\'', &end.to_string()); .replace('\'', &end.to_string());
@ -786,6 +830,63 @@ mod test {
.await; .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] #[gpui::test]
async fn test_multiline_surrounding_character_objects(cx: &mut gpui::TestAppContext) { async fn test_multiline_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
@ -827,6 +928,66 @@ mod test {
return false return false
}"}) }"})
.await; .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] #[gpui::test]
@ -834,12 +995,6 @@ mod test {
let mut cx = NeovimBackedTestContext::new(cx).await; let mut cx = NeovimBackedTestContext::new(cx).await;
for (start, end) in SURROUNDING_OBJECTS { for (start, end) in SURROUNDING_OBJECTS {
if ((start == &'\'' || start == &'`' || start == &'"')
&& !ExemptionFeatures::QuotesSeekForward.supported())
|| (start == &'<' && !ExemptionFeatures::AngleBracketsFreezeNeovim.supported())
{
continue;
}
let marked_string = SURROUNDING_MARKER_STRING let marked_string = SURROUNDING_MARKER_STRING
.replace('`', &start.to_string()) .replace('`', &start.to_string())
.replace('\'', &end.to_string()); .replace('\'', &end.to_string());

View file

@ -734,3 +734,26 @@ async fn test_paragraphs_dont_wrap(cx: &mut gpui::TestAppContext) {
two"}) two"})
.await; .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,
);
}

View file

@ -1,15 +1,12 @@
use editor::scroll::VERTICAL_SCROLL_MARGIN; use editor::scroll::VERTICAL_SCROLL_MARGIN;
use indoc::indoc; use indoc::indoc;
use settings::SettingsStore; use settings::SettingsStore;
use std::ops::{Deref, DerefMut, Range}; use std::ops::{Deref, DerefMut};
use collections::{HashMap, HashSet}; use collections::{HashMap, HashSet};
use gpui::{geometry::vector::vec2f, ContextHandle}; use gpui::{geometry::vector::vec2f, ContextHandle};
use language::{ use language::language_settings::{AllLanguageSettings, SoftWrap};
language_settings::{AllLanguageSettings, SoftWrap}, use util::test::marked_text_offsets;
OffsetRangeExt,
};
use util::test::{generate_marked_text, marked_text_offsets};
use super::{neovim_connection::NeovimConnection, NeovimBackedBindingTestContext, VimTestContext}; use super::{neovim_connection::NeovimConnection, NeovimBackedBindingTestContext, VimTestContext};
use crate::state::Mode; use crate::state::Mode;
@ -37,10 +34,6 @@ pub enum ExemptionFeatures {
AroundSentenceStartingBetweenIncludesWrongWhitespace, AroundSentenceStartingBetweenIncludesWrongWhitespace,
// Non empty selection with text objects in visual mode // Non empty selection with text objects in visual mode
NonEmptyVisualTextObjects, 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 // Sentence Doesn't backtrack when its at the end of the file
SentenceAfterPunctuationAtEndOfFile, SentenceAfterPunctuationAtEndOfFile,
} }
@ -250,25 +243,13 @@ impl<'a> NeovimBackedTestContext<'a> {
} }
pub async fn neovim_state(&mut self) -> String { pub async fn neovim_state(&mut self) -> String {
generate_marked_text( self.neovim.marked_text().await
self.neovim.text().await.as_str(),
&self.neovim_selections().await[..],
true,
)
} }
pub async fn neovim_mode(&mut self) -> Mode { pub async fn neovim_mode(&mut self) -> Mode {
self.neovim.mode().await.unwrap() 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) { pub async fn assert_state_matches(&mut self) {
self.is_dirty = false; self.is_dirty = false;
let neovim = self.neovim_state().await; let neovim = self.neovim_state().await;

View file

@ -1,9 +1,9 @@
use std::path::PathBuf;
#[cfg(feature = "neovim")] #[cfg(feature = "neovim")]
use std::{ use std::{
cmp, cmp,
ops::{Deref, DerefMut}, ops::{Deref, DerefMut, Range},
}; };
use std::{ops::Range, path::PathBuf};
#[cfg(feature = "neovim")] #[cfg(feature = "neovim")]
use async_compat::Compat; use async_compat::Compat;
@ -12,6 +12,7 @@ use async_trait::async_trait;
#[cfg(feature = "neovim")] #[cfg(feature = "neovim")]
use gpui::keymap_matcher::Keystroke; use gpui::keymap_matcher::Keystroke;
#[cfg(feature = "neovim")]
use language::Point; use language::Point;
#[cfg(feature = "neovim")] #[cfg(feature = "neovim")]
@ -109,7 +110,12 @@ impl NeovimConnection {
// Sends a keystroke to the neovim process. // Sends a keystroke to the neovim process.
#[cfg(feature = "neovim")] #[cfg(feature = "neovim")]
pub async fn send_keystroke(&mut self, keystroke_text: &str) { 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 let special = keystroke.shift
|| keystroke.ctrl || keystroke.ctrl
|| keystroke.alt || keystroke.alt
@ -296,7 +302,7 @@ impl NeovimConnection {
} }
#[cfg(feature = "neovim")] #[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 let nvim_buffer = self
.nvim .nvim
.get_current_buf() .get_current_buf()
@ -405,37 +411,33 @@ impl NeovimConnection {
.push(Point::new(selection_row, selection_col)..Point::new(cursor_row, cursor_col)), .push(Point::new(selection_row, selection_col)..Point::new(cursor_row, cursor_col)),
} }
let ranges = encode_ranges(&text, &selections);
let state = NeovimData::Get { let state = NeovimData::Get {
mode, mode,
state: encode_ranges(&text, &selections), state: ranges.clone(),
}; };
if self.data.back() != Some(&state) { if self.data.back() != Some(&state) {
self.data.push_back(state.clone()); self.data.push_back(state.clone());
} }
(mode, text, selections) (mode, ranges)
} }
#[cfg(not(feature = "neovim"))] #[cfg(not(feature = "neovim"))]
pub async fn state(&mut self) -> (Option<Mode>, String, Vec<Range<Point>>) { pub async fn state(&mut self) -> (Option<Mode>, String) {
if let Some(NeovimData::Get { state: text, mode }) = self.data.front() { if let Some(NeovimData::Get { state: raw, mode }) = self.data.front() {
let (text, ranges) = parse_state(text); (*mode, raw.to_string())
(*mode, text, ranges)
} else { } else {
panic!("operation does not match recorded script. re-record with --features=neovim"); 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> { pub async fn mode(&mut self) -> Option<Mode> {
self.state().await.0 self.state().await.0
} }
pub async fn text(&mut self) -> String { pub async fn marked_text(&mut self) -> String {
self.state().await.1 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>>) { fn parse_state(marked_text: &str) -> (String, Vec<Range<Point>>) {
let (text, ranges) = util::test::marked_text_ranges(marked_text, true); let (text, ranges) = util::test::marked_text_ranges(marked_text, true);
let point_ranges = ranges let point_ranges = ranges

View file

@ -1,5 +1,5 @@
use anyhow::Result; use anyhow::Result;
use std::{cmp, sync::Arc}; use std::sync::Arc;
use collections::HashMap; use collections::HashMap;
use editor::{ 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 let Some(range) = object.range(map, head, around) {
if !range.is_empty() { if !range.is_empty() {
let expand_both_ways = let expand_both_ways = object.always_expands_both_ways()
if object.always_expands_both_ways() || selection.is_empty() { || selection.is_empty()
true || movement::right(map, selection.start) == selection.end;
// contains only one character
} else if let Some((_, start)) =
map.reverse_chars_at(selection.end).next()
{
selection.start == start
} else {
false
};
if expand_both_ways { if expand_both_ways {
selection.start = cmp::min(selection.start, range.start); selection.start = range.start;
selection.end = cmp::max(selection.end, range.end); selection.end = range.end;
} else if selection.reversed { } else if selection.reversed {
selection.start = range.start; selection.start = range.start;
} else { } else {

View file

@ -1 +0,0 @@
[{"Text":"The quick\n\nbrown fox jumps\nover the lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[3,5],"end":[3,5]}}]

View file

@ -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"}} {"Put":{"state":"Test teˇst\ntest"}}
{"Key":"d"} {"Key":"d"}
{"Key":"e"} {"Key":"e"}

View file

@ -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"}}

View file

@ -8,3 +8,8 @@
{"Key":"i"} {"Key":"i"}
{"Key":"{"} {"Key":"{"}
{"Get":{"state":"func empty(a string) bool {\n if a == \"\" {\n« return true\nˇ» }\n return false\n}","mode":"Visual"}} {"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"}}

View file

@ -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"}}

View file

@ -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"}}

View file

@ -35,9 +35,9 @@ use gpui::{
CursorStyle, ModifiersChangedEvent, MouseButton, PathPromptOptions, Platform, PromptLevel, CursorStyle, ModifiersChangedEvent, MouseButton, PathPromptOptions, Platform, PromptLevel,
WindowBounds, WindowOptions, WindowBounds, WindowOptions,
}, },
AnyModelHandle, AnyViewHandle, AnyWeakViewHandle, AnyWindowHandle, AppContext, AsyncAppContext, AnyModelHandle, AnyViewHandle, AnyWeakViewHandle, AppContext, AsyncAppContext, Entity,
Entity, ModelContext, ModelHandle, SizeConstraint, Subscription, Task, View, ViewContext, ModelContext, ModelHandle, SizeConstraint, Subscription, Task, View, ViewContext, ViewHandle,
ViewHandle, WeakViewHandle, WindowContext, WindowHandle, WeakViewHandle, WindowContext, WindowHandle,
}; };
use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ProjectItem}; use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ProjectItem};
use itertools::Itertools; use itertools::Itertools;
@ -4238,6 +4238,10 @@ async fn join_channel_internal(
}) })
.await?; .await?;
let Some(room) = room else {
return anyhow::Ok(true);
};
room.update(cx, |room, _| room.room_update_completed()) room.update(cx, |room, _| room.room_update_completed())
.await; .await;
@ -4295,12 +4299,14 @@ pub fn join_channel(
} }
if let Err(err) = result { if let Err(err) = result {
let prompt = active_window.unwrap().prompt( let prompt = active_window.unwrap().update(&mut cx, |_, cx| {
PromptLevel::Critical, cx.prompt(
&format!("Failed to join channel: {}", err), PromptLevel::Critical,
&["Ok"], &format!("Failed to join channel: {}", err),
&mut cx, &["Ok"],
); )
});
if let Some(mut prompt) = prompt { if let Some(mut prompt) = prompt {
prompt.next().await; prompt.next().await;
} else { } 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() { for window in cx.windows() {
let found = window.update(cx, |cx| { if let Some(workspace) = window
let is_workspace = cx.root_view().clone().downcast::<Workspace>().is_some(); .update(cx, |cx| {
if is_workspace { cx.root_view()
cx.activate_window(); .clone()
} .downcast::<Workspace>()
is_workspace .map(|workspace| {
}); cx.activate_window();
if found == Some(true) { workspace
return Some(window); })
})
.flatten()
{
return Some(workspace);
} }
} }
None None

View file

@ -55,7 +55,7 @@ fn parse_eval() -> anyhow::Result<Vec<RepoEval>> {
.as_path() .as_path()
.parent() .parent()
.unwrap() .unwrap()
.join("crates/semantic_index/eval"); .join("zed/crates/semantic_index/eval");
let mut repo_evals: Vec<RepoEval> = Vec::new(); let mut repo_evals: Vec<RepoEval> = Vec::new();
for entry in fs::read_dir(eval_folder)? { for entry in fs::read_dir(eval_folder)? {

View file

@ -3,6 +3,7 @@
(raw_string) (raw_string)
(heredoc_body) (heredoc_body)
(heredoc_start) (heredoc_start)
(ansi_c_string)
] @string ] @string
(command_name) @function (command_name) @function

View file

@ -7,6 +7,7 @@ use cli::FORCE_CLI_MODE_ENV_VAR_NAME;
use client::{ use client::{
self, Client, TelemetrySettings, UserStore, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN, self, Client, TelemetrySettings, UserStore, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN,
}; };
use collab_ui::channel_view::ChannelView;
use db::kvp::KEY_VALUE_STORE; use db::kvp::KEY_VALUE_STORE;
use editor::Editor; use editor::Editor;
use futures::StreamExt; use futures::StreamExt;
@ -240,6 +241,20 @@ fn main() {
}) })
.detach_and_log_err(cx) .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 Ok(None) | Err(_) => cx
.spawn({ .spawn({
let app_state = app_state.clone(); let app_state = app_state.clone();
@ -254,8 +269,10 @@ fn main() {
while let Some(request) = open_rx.next().await { while let Some(request) = open_rx.next().await {
match request { match request {
OpenRequest::Paths { paths } => { OpenRequest::Paths { paths } => {
cx.update(|cx| workspace::open_paths(&paths, &app_state, None, cx)) cx.update(|cx| {
.detach(); workspace::open_paths(&paths, &app_state.clone(), None, cx)
})
.detach();
} }
OpenRequest::CliConnection { connection } => { OpenRequest::CliConnection { connection } => {
cx.spawn(|cx| handle_cli_connection(connection, app_state.clone(), cx)) 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) workspace::join_channel(channel_id, app_state.clone(), None, cx)
}) })
.detach(), .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();
})
}
}
} }
} }
} }

View file

@ -32,6 +32,9 @@ pub enum OpenRequest {
JoinChannel { JoinChannel {
channel_id: u64, channel_id: u64,
}, },
OpenChannelNotes {
channel_id: u64,
},
} }
pub struct OpenListener { pub struct OpenListener {
@ -85,7 +88,11 @@ impl OpenListener {
if let Some(slug) = parts.next() { if let Some(slug) = parts.next() {
if let Some(id_str) = slug.split("-").last() { if let Some(id_str) = slug.split("-").last() {
if let Ok(channel_id) = id_str.parse::<u64>() { 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 });
}
} }
} }
} }

View file

@ -1,3 +1,3 @@
#!/bin/bash #!/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

View file

@ -210,6 +210,14 @@ export default function contacts_panel(): any {
right: SPACING, right: SPACING,
}, },
}, },
dragged_over_header: {
margin: { top: SPACING },
padding: {
left: SPACING,
right: SPACING,
},
background: background(layer, "hovered"),
},
subheader_row, subheader_row,
leave_call: interactive({ leave_call: interactive({
base: { base: {
@ -279,7 +287,7 @@ export default function contacts_panel(): any {
margin: { margin: {
left: CHANNEL_SPACING, left: CHANNEL_SPACING,
}, },
} },
}, },
list_empty_label_container: { list_empty_label_container: {
margin: { margin: {

View file

@ -1,12 +1,22 @@
import { background, border, text } from "./components" import { background, border, text } from "./components"
import { icon_button } from "../component/icon_button" import { icon_button } from "../component/icon_button"
import { useTheme } from "../theme" import { useTheme, with_opacity } from "../theme"
import { interactive } from "../element" import { text_button } from "../component"
export default function (): any { export default function (): any {
const theme = useTheme() const theme = useTheme()
const layer = theme.middle 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 { return {
background: background(layer), background: background(layer),
avatar: { avatar: {
@ -31,34 +41,19 @@ export default function (): any {
}, },
}, },
read_text: { read_text: {
padding: { top: 4, bottom: 4 }, ...notification_text,
...text(layer, "sans", "disabled"), color: notification_read_text_color,
}, },
unread_text: { unread_text: notification_text,
padding: { top: 4, bottom: 4 }, button: text_button({
...text(layer, "sans", "base"), variant: "ghost",
},
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"),
},
},
}), }),
timestamp: text(layer, "sans", "base", "disabled"), timestamp: text(layer, "sans", "base", "disabled"),
avatar_container: { avatar_container: {
padding: { padding: {
right: 6, right: 8,
left: 2, left: 2,
top: 2, top: 4,
bottom: 2, bottom: 2,
}, },
}, },

View file

@ -2,7 +2,6 @@ import { with_opacity } from "../theme/color"
import { background, border, foreground, text } from "./components" import { background, border, foreground, text } from "./components"
import { interactive, toggleable } from "../element" import { interactive, toggleable } from "../element"
import { useTheme } from "../theme" import { useTheme } from "../theme"
import { text_button } from "../component/text_button"
const search_results = () => { const search_results = () => {
const theme = useTheme() const theme = useTheme()
@ -36,7 +35,7 @@ export default function search(): any {
left: 10, left: 10,
right: 4, right: 4,
}, },
margin: { right: SEARCH_ROW_SPACING } margin: { right: SEARCH_ROW_SPACING },
} }
const include_exclude_editor = { const include_exclude_editor = {
@ -378,7 +377,7 @@ export default function search(): any {
modes_container: { modes_container: {
padding: { padding: {
right: SEARCH_ROW_SPACING, right: SEARCH_ROW_SPACING,
} },
}, },
replace_icon: { replace_icon: {
icon: { icon: {