diff --git a/Cargo.lock b/Cargo.lock index d5d0493936..6d5fdc67d7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1347,6 +1347,43 @@ dependencies = [ "uuid 1.4.1", ] +[[package]] +name = "channel2" +version = "0.1.0" +dependencies = [ + "anyhow", + "client2", + "clock", + "collections", + "db2", + "feature_flags2", + "futures 0.3.28", + "gpui2", + "image", + "language2", + "lazy_static", + "log", + "parking_lot 0.11.2", + "postage", + "rand 0.8.5", + "rpc2", + "schemars", + "serde", + "serde_derive", + "settings2", + "smallvec", + "smol", + "sum_tree", + "tempfile", + "text", + "thiserror", + "time", + "tiny_http", + "url", + "util", + "uuid 1.4.1", +] + [[package]] name = "chrono" version = "0.4.31" @@ -2422,8 +2459,10 @@ dependencies = [ "client", "collections", "editor", + "futures 0.3.28", "gpui", "language", + "log", "lsp", "postage", "project", @@ -9690,7 +9729,7 @@ dependencies = [ [[package]] name = "tree-sitter-vue" version = "0.0.1" -source = "git+https://github.com/zed-industries/tree-sitter-vue?rev=95b2890#95b28908d90e928c308866f7631e73ef6e1d4b5f" +source = "git+https://github.com/zed-industries/tree-sitter-vue?rev=9b6cb221ccb8d0b956fcb17e9a1efac2feefeb58#9b6cb221ccb8d0b956fcb17e9a1efac2feefeb58" dependencies = [ "cc", "tree-sitter", diff --git a/Cargo.toml b/Cargo.toml index 998ea081a6..6245889530 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ members = [ "crates/call", "crates/call2", "crates/channel", + "crates/channel2", "crates/cli", "crates/client", "crates/client2", @@ -175,7 +176,7 @@ tree-sitter-yaml = { git = "https://github.com/zed-industries/tree-sitter-yaml", tree-sitter-lua = "0.0.14" tree-sitter-nix = { git = "https://github.com/nix-community/tree-sitter-nix", rev = "66e3e9ce9180ae08fc57372061006ef83f0abde7" } tree-sitter-nu = { git = "https://github.com/nushell/tree-sitter-nu", rev = "786689b0562b9799ce53e824cb45a1a2a04dc673"} -tree-sitter-vue = {git = "https://github.com/zed-industries/tree-sitter-vue", rev = "95b2890"} +tree-sitter-vue = {git = "https://github.com/zed-industries/tree-sitter-vue", rev = "9b6cb221ccb8d0b956fcb17e9a1efac2feefeb58"} [patch.crates-io] tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "35a6052fbcafc5e5fc0f9415b8652be7dcaf7222" } async-task = { git = "https://github.com/zed-industries/async-task", rev = "341b57d6de98cdfd7b418567b8de2022ca993a6e" } diff --git a/assets/icons/dash.svg b/assets/icons/dash.svg new file mode 100644 index 0000000000..efff9eab5e --- /dev/null +++ b/assets/icons/dash.svg @@ -0,0 +1 @@ + diff --git a/crates/channel2/Cargo.toml b/crates/channel2/Cargo.toml new file mode 100644 index 0000000000..c292d4e8dd --- /dev/null +++ b/crates/channel2/Cargo.toml @@ -0,0 +1,54 @@ +[package] +name = "channel2" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/channel2.rs" +doctest = false + +[features] +test-support = ["collections/test-support", "gpui/test-support", "rpc/test-support"] + +[dependencies] +client = { package = "client2", path = "../client2" } +collections = { path = "../collections" } +db = { package = "db2", path = "../db2" } +gpui = { package = "gpui2", path = "../gpui2" } +util = { path = "../util" } +rpc = { package = "rpc2", path = "../rpc2" } +text = { path = "../text" } +language = { package = "language2", path = "../language2" } +settings = { package = "settings2", path = "../settings2" } +feature_flags = { package = "feature_flags2", path = "../feature_flags2" } +sum_tree = { path = "../sum_tree" } +clock = { path = "../clock" } + +anyhow.workspace = true +futures.workspace = true +image = "0.23" +lazy_static.workspace = true +smallvec.workspace = true +log.workspace = true +parking_lot.workspace = true +postage.workspace = true +rand.workspace = true +schemars.workspace = true +smol.workspace = true +thiserror.workspace = true +time.workspace = true +tiny_http = "0.8" +uuid.workspace = true +url = "2.2" +serde.workspace = true +serde_derive.workspace = true +tempfile = "3" + +[dev-dependencies] +collections = { path = "../collections", features = ["test-support"] } +gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } +rpc = { package = "rpc2", path = "../rpc2", features = ["test-support"] } +client = { package = "client2", path = "../client2", features = ["test-support"] } +settings = { package = "settings2", path = "../settings2", features = ["test-support"] } +util = { path = "../util", features = ["test-support"] } diff --git a/crates/channel2/src/channel2.rs b/crates/channel2/src/channel2.rs new file mode 100644 index 0000000000..f38ae4078a --- /dev/null +++ b/crates/channel2/src/channel2.rs @@ -0,0 +1,23 @@ +mod channel_buffer; +mod channel_chat; +mod channel_store; + +use client::{Client, UserStore}; +use gpui::{AppContext, Model}; +use std::sync::Arc; + +pub use channel_buffer::{ChannelBuffer, ChannelBufferEvent, ACKNOWLEDGE_DEBOUNCE_INTERVAL}; +pub use channel_chat::{ + mentions_to_proto, ChannelChat, ChannelChatEvent, ChannelMessage, ChannelMessageId, + MessageParams, +}; +pub use channel_store::{Channel, ChannelEvent, ChannelId, ChannelMembership, ChannelStore}; + +#[cfg(test)] +mod channel_store_tests; + +pub fn init(client: &Arc, user_store: Model, cx: &mut AppContext) { + channel_store::init(client, user_store, cx); + channel_buffer::init(client); + channel_chat::init(client); +} diff --git a/crates/channel2/src/channel_buffer.rs b/crates/channel2/src/channel_buffer.rs new file mode 100644 index 0000000000..4c321a8fbc --- /dev/null +++ b/crates/channel2/src/channel_buffer.rs @@ -0,0 +1,259 @@ +use crate::{Channel, ChannelId, ChannelStore}; +use anyhow::Result; +use client::{Client, Collaborator, UserStore}; +use collections::HashMap; +use gpui::{AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Task}; +use language::proto::serialize_version; +use rpc::{ + proto::{self, PeerId}, + TypedEnvelope, +}; +use std::{sync::Arc, time::Duration}; +use util::ResultExt; + +pub const ACKNOWLEDGE_DEBOUNCE_INTERVAL: Duration = Duration::from_millis(250); + +pub(crate) fn init(client: &Arc) { + client.add_model_message_handler(ChannelBuffer::handle_update_channel_buffer); + client.add_model_message_handler(ChannelBuffer::handle_update_channel_buffer_collaborators); +} + +pub struct ChannelBuffer { + pub channel_id: ChannelId, + connected: bool, + collaborators: HashMap, + user_store: Model, + channel_store: Model, + buffer: Model, + buffer_epoch: u64, + client: Arc, + subscription: Option, + acknowledge_task: Option>>, +} + +pub enum ChannelBufferEvent { + CollaboratorsChanged, + Disconnected, + BufferEdited, + ChannelChanged, +} + +impl EventEmitter for ChannelBuffer { + type Event = ChannelBufferEvent; +} + +impl ChannelBuffer { + pub(crate) async fn new( + channel: Arc, + client: Arc, + user_store: Model, + channel_store: Model, + mut cx: AsyncAppContext, + ) -> Result> { + let response = client + .request(proto::JoinChannelBuffer { + channel_id: channel.id, + }) + .await?; + + let base_text = response.base_text; + let operations = response + .operations + .into_iter() + .map(language::proto::deserialize_operation) + .collect::, _>>()?; + + let buffer = cx.build_model(|_| { + language::Buffer::remote(response.buffer_id, response.replica_id as u16, base_text) + })?; + buffer.update(&mut cx, |buffer, cx| buffer.apply_ops(operations, cx))??; + + let subscription = client.subscribe_to_entity(channel.id)?; + + anyhow::Ok(cx.build_model(|cx| { + cx.subscribe(&buffer, Self::on_buffer_update).detach(); + cx.on_release(Self::release).detach(); + let mut this = Self { + buffer, + buffer_epoch: response.epoch, + client, + connected: true, + collaborators: Default::default(), + acknowledge_task: None, + channel_id: channel.id, + subscription: Some(subscription.set_model(&cx.handle(), &mut cx.to_async())), + user_store, + channel_store, + }; + this.replace_collaborators(response.collaborators, cx); + this + })?) + } + + fn release(&mut self, _: &mut AppContext) { + if self.connected { + if let Some(task) = self.acknowledge_task.take() { + task.detach(); + } + self.client + .send(proto::LeaveChannelBuffer { + channel_id: self.channel_id, + }) + .log_err(); + } + } + + pub fn remote_id(&self, cx: &AppContext) -> u64 { + self.buffer.read(cx).remote_id() + } + + pub fn user_store(&self) -> &Model { + &self.user_store + } + + pub(crate) fn replace_collaborators( + &mut self, + collaborators: Vec, + cx: &mut ModelContext, + ) { + let mut new_collaborators = HashMap::default(); + for collaborator in collaborators { + if let Ok(collaborator) = Collaborator::from_proto(collaborator) { + new_collaborators.insert(collaborator.peer_id, collaborator); + } + } + + for (_, old_collaborator) in &self.collaborators { + if !new_collaborators.contains_key(&old_collaborator.peer_id) { + self.buffer.update(cx, |buffer, cx| { + buffer.remove_peer(old_collaborator.replica_id as u16, cx) + }); + } + } + self.collaborators = new_collaborators; + cx.emit(ChannelBufferEvent::CollaboratorsChanged); + cx.notify(); + } + + async fn handle_update_channel_buffer( + this: Model, + update_channel_buffer: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result<()> { + let ops = update_channel_buffer + .payload + .operations + .into_iter() + .map(language::proto::deserialize_operation) + .collect::, _>>()?; + + this.update(&mut cx, |this, cx| { + cx.notify(); + this.buffer + .update(cx, |buffer, cx| buffer.apply_ops(ops, cx)) + })??; + + Ok(()) + } + + async fn handle_update_channel_buffer_collaborators( + this: Model, + message: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result<()> { + this.update(&mut cx, |this, cx| { + this.replace_collaborators(message.payload.collaborators, cx); + cx.emit(ChannelBufferEvent::CollaboratorsChanged); + cx.notify(); + }) + } + + fn on_buffer_update( + &mut self, + _: Model, + event: &language::Event, + cx: &mut ModelContext, + ) { + match event { + language::Event::Operation(operation) => { + let operation = language::proto::serialize_operation(operation); + self.client + .send(proto::UpdateChannelBuffer { + channel_id: self.channel_id, + operations: vec![operation], + }) + .log_err(); + } + language::Event::Edited => { + cx.emit(ChannelBufferEvent::BufferEdited); + } + _ => {} + } + } + + pub fn acknowledge_buffer_version(&mut self, cx: &mut ModelContext<'_, ChannelBuffer>) { + let buffer = self.buffer.read(cx); + let version = buffer.version(); + let buffer_id = buffer.remote_id(); + let client = self.client.clone(); + let epoch = self.epoch(); + + self.acknowledge_task = Some(cx.spawn(move |_, cx| async move { + cx.background_executor() + .timer(ACKNOWLEDGE_DEBOUNCE_INTERVAL) + .await; + client + .send(proto::AckBufferOperation { + buffer_id, + epoch, + version: serialize_version(&version), + }) + .ok(); + Ok(()) + })); + } + + pub fn epoch(&self) -> u64 { + self.buffer_epoch + } + + pub fn buffer(&self) -> Model { + self.buffer.clone() + } + + pub fn collaborators(&self) -> &HashMap { + &self.collaborators + } + + pub fn channel(&self, cx: &AppContext) -> Option> { + self.channel_store + .read(cx) + .channel_for_id(self.channel_id) + .cloned() + } + + pub(crate) fn disconnect(&mut self, cx: &mut ModelContext) { + log::info!("channel buffer {} disconnected", self.channel_id); + if self.connected { + self.connected = false; + self.subscription.take(); + cx.emit(ChannelBufferEvent::Disconnected); + cx.notify() + } + } + + pub(crate) fn channel_changed(&mut self, cx: &mut ModelContext) { + cx.emit(ChannelBufferEvent::ChannelChanged); + cx.notify() + } + + pub fn is_connected(&self) -> bool { + self.connected + } + + pub fn replica_id(&self, cx: &AppContext) -> u16 { + self.buffer.read(cx).replica_id() + } +} diff --git a/crates/channel2/src/channel_chat.rs b/crates/channel2/src/channel_chat.rs new file mode 100644 index 0000000000..a5b5249853 --- /dev/null +++ b/crates/channel2/src/channel_chat.rs @@ -0,0 +1,647 @@ +use crate::{Channel, ChannelId, ChannelStore}; +use anyhow::{anyhow, Result}; +use client::{ + proto, + user::{User, UserStore}, + Client, Subscription, TypedEnvelope, UserId, +}; +use futures::lock::Mutex; +use gpui::{AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Task}; +use rand::prelude::*; +use std::{ + collections::HashSet, + mem, + ops::{ControlFlow, Range}, + sync::Arc, +}; +use sum_tree::{Bias, SumTree}; +use time::OffsetDateTime; +use util::{post_inc, ResultExt as _, TryFutureExt}; + +pub struct ChannelChat { + pub channel_id: ChannelId, + messages: SumTree, + acknowledged_message_ids: HashSet, + channel_store: Model, + loaded_all_messages: bool, + last_acknowledged_id: Option, + next_pending_message_id: usize, + user_store: Model, + rpc: Arc, + outgoing_messages_lock: Arc>, + rng: StdRng, + _subscription: Subscription, +} + +#[derive(Debug, PartialEq, Eq)] +pub struct MessageParams { + pub text: String, + pub mentions: Vec<(Range, UserId)>, +} + +#[derive(Clone, Debug)] +pub struct ChannelMessage { + pub id: ChannelMessageId, + pub body: String, + pub timestamp: OffsetDateTime, + pub sender: Arc, + pub nonce: u128, + pub mentions: Vec<(Range, UserId)>, +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum ChannelMessageId { + Saved(u64), + Pending(usize), +} + +#[derive(Clone, Debug, Default)] +pub struct ChannelMessageSummary { + max_id: ChannelMessageId, + count: usize, +} + +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] +struct Count(usize); + +#[derive(Clone, Debug, PartialEq)] +pub enum ChannelChatEvent { + MessagesUpdated { + old_range: Range, + new_count: usize, + }, + NewMessage { + channel_id: ChannelId, + message_id: u64, + }, +} + +impl EventEmitter for ChannelChat { + type Event = ChannelChatEvent; +} +pub fn init(client: &Arc) { + client.add_model_message_handler(ChannelChat::handle_message_sent); + client.add_model_message_handler(ChannelChat::handle_message_removed); +} + +impl ChannelChat { + pub async fn new( + channel: Arc, + channel_store: Model, + user_store: Model, + client: Arc, + mut cx: AsyncAppContext, + ) -> Result> { + let channel_id = channel.id; + let subscription = client.subscribe_to_entity(channel_id).unwrap(); + + let response = client + .request(proto::JoinChannelChat { channel_id }) + .await?; + let messages = messages_from_proto(response.messages, &user_store, &mut cx).await?; + let loaded_all_messages = response.done; + + Ok(cx.build_model(|cx| { + cx.on_release(Self::release).detach(); + let mut this = Self { + channel_id: channel.id, + user_store, + channel_store, + rpc: client, + outgoing_messages_lock: Default::default(), + messages: Default::default(), + acknowledged_message_ids: Default::default(), + loaded_all_messages, + next_pending_message_id: 0, + last_acknowledged_id: None, + rng: StdRng::from_entropy(), + _subscription: subscription.set_model(&cx.handle(), &mut cx.to_async()), + }; + this.insert_messages(messages, cx); + this + })?) + } + + fn release(&mut self, _: &mut AppContext) { + self.rpc + .send(proto::LeaveChannelChat { + channel_id: self.channel_id, + }) + .log_err(); + } + + pub fn channel(&self, cx: &AppContext) -> Option> { + self.channel_store + .read(cx) + .channel_for_id(self.channel_id) + .cloned() + } + + pub fn client(&self) -> &Arc { + &self.rpc + } + + pub fn send_message( + &mut self, + message: MessageParams, + cx: &mut ModelContext, + ) -> Result>> { + if message.text.is_empty() { + Err(anyhow!("message body can't be empty"))?; + } + + let current_user = self + .user_store + .read(cx) + .current_user() + .ok_or_else(|| anyhow!("current_user is not present"))?; + + let channel_id = self.channel_id; + let pending_id = ChannelMessageId::Pending(post_inc(&mut self.next_pending_message_id)); + let nonce = self.rng.gen(); + self.insert_messages( + SumTree::from_item( + ChannelMessage { + id: pending_id, + body: message.text.clone(), + sender: current_user, + timestamp: OffsetDateTime::now_utc(), + mentions: message.mentions.clone(), + nonce, + }, + &(), + ), + cx, + ); + let user_store = self.user_store.clone(); + let rpc = self.rpc.clone(); + let outgoing_messages_lock = self.outgoing_messages_lock.clone(); + Ok(cx.spawn(move |this, mut cx| async move { + let outgoing_message_guard = outgoing_messages_lock.lock().await; + let request = rpc.request(proto::SendChannelMessage { + channel_id, + body: message.text, + nonce: Some(nonce.into()), + mentions: mentions_to_proto(&message.mentions), + }); + let response = request.await?; + drop(outgoing_message_guard); + let response = response.message.ok_or_else(|| anyhow!("invalid message"))?; + let id = response.id; + let message = ChannelMessage::from_proto(response, &user_store, &mut cx).await?; + this.update(&mut cx, |this, cx| { + this.insert_messages(SumTree::from_item(message, &()), cx); + })?; + Ok(id) + })) + } + + pub fn remove_message(&mut self, id: u64, cx: &mut ModelContext) -> Task> { + let response = self.rpc.request(proto::RemoveChannelMessage { + channel_id: self.channel_id, + message_id: id, + }); + cx.spawn(move |this, mut cx| async move { + response.await?; + this.update(&mut cx, |this, cx| { + this.message_removed(id, cx); + })?; + Ok(()) + }) + } + + pub fn load_more_messages(&mut self, cx: &mut ModelContext) -> Option>> { + if self.loaded_all_messages { + return None; + } + + let rpc = self.rpc.clone(); + let user_store = self.user_store.clone(); + let channel_id = self.channel_id; + let before_message_id = self.first_loaded_message_id()?; + Some(cx.spawn(move |this, mut cx| { + async move { + let response = rpc + .request(proto::GetChannelMessages { + channel_id, + before_message_id, + }) + .await?; + let loaded_all_messages = response.done; + let messages = messages_from_proto(response.messages, &user_store, &mut cx).await?; + this.update(&mut cx, |this, cx| { + this.loaded_all_messages = loaded_all_messages; + this.insert_messages(messages, cx); + })?; + anyhow::Ok(()) + } + .log_err() + })) + } + + pub fn first_loaded_message_id(&mut self) -> Option { + self.messages.first().and_then(|message| match message.id { + ChannelMessageId::Saved(id) => Some(id), + ChannelMessageId::Pending(_) => None, + }) + } + + /// Load all of the chat messages since a certain message id. + /// + /// For now, we always maintain a suffix of the channel's messages. + pub async fn load_history_since_message( + chat: Model, + message_id: u64, + mut cx: AsyncAppContext, + ) -> Option { + loop { + let step = chat + .update(&mut cx, |chat, cx| { + if let Some(first_id) = chat.first_loaded_message_id() { + if first_id <= message_id { + let mut cursor = chat.messages.cursor::<(ChannelMessageId, Count)>(); + let message_id = ChannelMessageId::Saved(message_id); + cursor.seek(&message_id, Bias::Left, &()); + return ControlFlow::Break( + if cursor + .item() + .map_or(false, |message| message.id == message_id) + { + Some(cursor.start().1 .0) + } else { + None + }, + ); + } + } + ControlFlow::Continue(chat.load_more_messages(cx)) + }) + .log_err()?; + match step { + ControlFlow::Break(ix) => return ix, + ControlFlow::Continue(task) => task?.await?, + } + } + } + + pub fn acknowledge_last_message(&mut self, cx: &mut ModelContext) { + if let ChannelMessageId::Saved(latest_message_id) = self.messages.summary().max_id { + if self + .last_acknowledged_id + .map_or(true, |acknowledged_id| acknowledged_id < latest_message_id) + { + self.rpc + .send(proto::AckChannelMessage { + channel_id: self.channel_id, + message_id: latest_message_id, + }) + .ok(); + self.last_acknowledged_id = Some(latest_message_id); + self.channel_store.update(cx, |store, cx| { + store.acknowledge_message_id(self.channel_id, latest_message_id, cx); + }); + } + } + } + + pub fn rejoin(&mut self, cx: &mut ModelContext) { + let user_store = self.user_store.clone(); + let rpc = self.rpc.clone(); + let channel_id = self.channel_id; + cx.spawn(move |this, mut cx| { + async move { + let response = rpc.request(proto::JoinChannelChat { channel_id }).await?; + let messages = messages_from_proto(response.messages, &user_store, &mut cx).await?; + let loaded_all_messages = response.done; + + let pending_messages = this.update(&mut cx, |this, cx| { + if let Some((first_new_message, last_old_message)) = + messages.first().zip(this.messages.last()) + { + if first_new_message.id > last_old_message.id { + let old_messages = mem::take(&mut this.messages); + cx.emit(ChannelChatEvent::MessagesUpdated { + old_range: 0..old_messages.summary().count, + new_count: 0, + }); + this.loaded_all_messages = loaded_all_messages; + } + } + + this.insert_messages(messages, cx); + if loaded_all_messages { + this.loaded_all_messages = loaded_all_messages; + } + + this.pending_messages().cloned().collect::>() + })?; + + for pending_message in pending_messages { + let request = rpc.request(proto::SendChannelMessage { + channel_id, + body: pending_message.body, + mentions: mentions_to_proto(&pending_message.mentions), + nonce: Some(pending_message.nonce.into()), + }); + let response = request.await?; + let message = ChannelMessage::from_proto( + response.message.ok_or_else(|| anyhow!("invalid message"))?, + &user_store, + &mut cx, + ) + .await?; + this.update(&mut cx, |this, cx| { + this.insert_messages(SumTree::from_item(message, &()), cx); + })?; + } + + anyhow::Ok(()) + } + .log_err() + }) + .detach(); + } + + pub fn message_count(&self) -> usize { + self.messages.summary().count + } + + pub fn messages(&self) -> &SumTree { + &self.messages + } + + pub fn message(&self, ix: usize) -> &ChannelMessage { + let mut cursor = self.messages.cursor::(); + cursor.seek(&Count(ix), Bias::Right, &()); + cursor.item().unwrap() + } + + pub fn acknowledge_message(&mut self, id: u64) { + if self.acknowledged_message_ids.insert(id) { + self.rpc + .send(proto::AckChannelMessage { + channel_id: self.channel_id, + message_id: id, + }) + .ok(); + } + } + + pub fn messages_in_range(&self, range: Range) -> impl Iterator { + let mut cursor = self.messages.cursor::(); + cursor.seek(&Count(range.start), Bias::Right, &()); + cursor.take(range.len()) + } + + pub fn pending_messages(&self) -> impl Iterator { + let mut cursor = self.messages.cursor::(); + cursor.seek(&ChannelMessageId::Pending(0), Bias::Left, &()); + cursor + } + + async fn handle_message_sent( + this: Model, + message: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result<()> { + let user_store = this.update(&mut cx, |this, _| this.user_store.clone())?; + let message = message + .payload + .message + .ok_or_else(|| anyhow!("empty message"))?; + let message_id = message.id; + + let message = ChannelMessage::from_proto(message, &user_store, &mut cx).await?; + this.update(&mut cx, |this, cx| { + this.insert_messages(SumTree::from_item(message, &()), cx); + cx.emit(ChannelChatEvent::NewMessage { + channel_id: this.channel_id, + message_id, + }) + })?; + + Ok(()) + } + + async fn handle_message_removed( + this: Model, + message: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result<()> { + this.update(&mut cx, |this, cx| { + this.message_removed(message.payload.message_id, cx) + })?; + Ok(()) + } + + fn insert_messages(&mut self, messages: SumTree, cx: &mut ModelContext) { + if let Some((first_message, last_message)) = messages.first().zip(messages.last()) { + let nonces = messages + .cursor::<()>() + .map(|m| m.nonce) + .collect::>(); + + let mut old_cursor = self.messages.cursor::<(ChannelMessageId, Count)>(); + let mut new_messages = old_cursor.slice(&first_message.id, Bias::Left, &()); + let start_ix = old_cursor.start().1 .0; + let removed_messages = old_cursor.slice(&last_message.id, Bias::Right, &()); + let removed_count = removed_messages.summary().count; + let new_count = messages.summary().count; + let end_ix = start_ix + removed_count; + + new_messages.append(messages, &()); + + let mut ranges = Vec::>::new(); + if new_messages.last().unwrap().is_pending() { + new_messages.append(old_cursor.suffix(&()), &()); + } else { + new_messages.append( + old_cursor.slice(&ChannelMessageId::Pending(0), Bias::Left, &()), + &(), + ); + + while let Some(message) = old_cursor.item() { + let message_ix = old_cursor.start().1 .0; + if nonces.contains(&message.nonce) { + if ranges.last().map_or(false, |r| r.end == message_ix) { + ranges.last_mut().unwrap().end += 1; + } else { + ranges.push(message_ix..message_ix + 1); + } + } else { + new_messages.push(message.clone(), &()); + } + old_cursor.next(&()); + } + } + + drop(old_cursor); + self.messages = new_messages; + + for range in ranges.into_iter().rev() { + cx.emit(ChannelChatEvent::MessagesUpdated { + old_range: range, + new_count: 0, + }); + } + cx.emit(ChannelChatEvent::MessagesUpdated { + old_range: start_ix..end_ix, + new_count, + }); + + cx.notify(); + } + } + + fn message_removed(&mut self, id: u64, cx: &mut ModelContext) { + let mut cursor = self.messages.cursor::(); + let mut messages = cursor.slice(&ChannelMessageId::Saved(id), Bias::Left, &()); + if let Some(item) = cursor.item() { + if item.id == ChannelMessageId::Saved(id) { + let ix = messages.summary().count; + cursor.next(&()); + messages.append(cursor.suffix(&()), &()); + drop(cursor); + self.messages = messages; + cx.emit(ChannelChatEvent::MessagesUpdated { + old_range: ix..ix + 1, + new_count: 0, + }); + } + } + } +} + +async fn messages_from_proto( + proto_messages: Vec, + user_store: &Model, + cx: &mut AsyncAppContext, +) -> Result> { + let messages = ChannelMessage::from_proto_vec(proto_messages, user_store, cx).await?; + let mut result = SumTree::new(); + result.extend(messages, &()); + Ok(result) +} + +impl ChannelMessage { + pub async fn from_proto( + message: proto::ChannelMessage, + user_store: &Model, + cx: &mut AsyncAppContext, + ) -> Result { + let sender = user_store + .update(cx, |user_store, cx| { + user_store.get_user(message.sender_id, cx) + })? + .await?; + Ok(ChannelMessage { + id: ChannelMessageId::Saved(message.id), + body: message.body, + mentions: message + .mentions + .into_iter() + .filter_map(|mention| { + let range = mention.range?; + Some((range.start as usize..range.end as usize, mention.user_id)) + }) + .collect(), + timestamp: OffsetDateTime::from_unix_timestamp(message.timestamp as i64)?, + sender, + nonce: message + .nonce + .ok_or_else(|| anyhow!("nonce is required"))? + .into(), + }) + } + + pub fn is_pending(&self) -> bool { + matches!(self.id, ChannelMessageId::Pending(_)) + } + + pub async fn from_proto_vec( + proto_messages: Vec, + user_store: &Model, + cx: &mut AsyncAppContext, + ) -> Result> { + let unique_user_ids = proto_messages + .iter() + .map(|m| m.sender_id) + .collect::>() + .into_iter() + .collect(); + user_store + .update(cx, |user_store, cx| { + user_store.get_users(unique_user_ids, cx) + })? + .await?; + + let mut messages = Vec::with_capacity(proto_messages.len()); + for message in proto_messages { + messages.push(ChannelMessage::from_proto(message, user_store, cx).await?); + } + Ok(messages) + } +} + +pub fn mentions_to_proto(mentions: &[(Range, UserId)]) -> Vec { + mentions + .iter() + .map(|(range, user_id)| proto::ChatMention { + range: Some(proto::Range { + start: range.start as u64, + end: range.end as u64, + }), + user_id: *user_id as u64, + }) + .collect() +} + +impl sum_tree::Item for ChannelMessage { + type Summary = ChannelMessageSummary; + + fn summary(&self) -> Self::Summary { + ChannelMessageSummary { + max_id: self.id, + count: 1, + } + } +} + +impl Default for ChannelMessageId { + fn default() -> Self { + Self::Saved(0) + } +} + +impl sum_tree::Summary for ChannelMessageSummary { + type Context = (); + + fn add_summary(&mut self, summary: &Self, _: &()) { + self.max_id = summary.max_id; + self.count += summary.count; + } +} + +impl<'a> sum_tree::Dimension<'a, ChannelMessageSummary> for ChannelMessageId { + fn add_summary(&mut self, summary: &'a ChannelMessageSummary, _: &()) { + debug_assert!(summary.max_id > *self); + *self = summary.max_id; + } +} + +impl<'a> sum_tree::Dimension<'a, ChannelMessageSummary> for Count { + fn add_summary(&mut self, summary: &'a ChannelMessageSummary, _: &()) { + self.0 += summary.count; + } +} + +impl<'a> From<&'a str> for MessageParams { + fn from(value: &'a str) -> Self { + Self { + text: value.into(), + mentions: Vec::new(), + } + } +} diff --git a/crates/channel2/src/channel_store.rs b/crates/channel2/src/channel_store.rs new file mode 100644 index 0000000000..3c9abd59e2 --- /dev/null +++ b/crates/channel2/src/channel_store.rs @@ -0,0 +1,1021 @@ +mod channel_index; + +use crate::{channel_buffer::ChannelBuffer, channel_chat::ChannelChat, ChannelMessage}; +use anyhow::{anyhow, Result}; +use channel_index::ChannelIndex; +use client::{Client, Subscription, User, UserId, UserStore}; +use collections::{hash_map, HashMap, HashSet}; +use db::RELEASE_CHANNEL; +use futures::{channel::mpsc, future::Shared, Future, FutureExt, StreamExt}; +use gpui::{ + AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Task, WeakModel, +}; +use rpc::{ + proto::{self, ChannelVisibility}, + TypedEnvelope, +}; +use std::{mem, sync::Arc, time::Duration}; +use util::{async_maybe, ResultExt}; + +pub fn init(client: &Arc, user_store: Model, cx: &mut AppContext) { + let channel_store = + cx.build_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx)); + cx.set_global(channel_store); +} + +pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30); + +pub type ChannelId = u64; + +pub struct ChannelStore { + pub channel_index: ChannelIndex, + channel_invitations: Vec>, + channel_participants: HashMap>>, + outgoing_invites: HashSet<(ChannelId, UserId)>, + update_channels_tx: mpsc::UnboundedSender, + opened_buffers: HashMap>, + opened_chats: HashMap>, + client: Arc, + user_store: Model, + _rpc_subscription: Subscription, + _watch_connection_status: Task>, + disconnect_channel_buffers_task: Option>, + _update_channels: Task<()>, +} + +#[derive(Clone, Debug, PartialEq)] +pub struct Channel { + pub id: ChannelId, + pub name: String, + pub visibility: proto::ChannelVisibility, + pub role: proto::ChannelRole, + pub unseen_note_version: Option<(u64, clock::Global)>, + pub unseen_message_id: Option, + pub parent_path: Vec, +} + +impl Channel { + pub fn link(&self) -> String { + RELEASE_CHANNEL.link_prefix().to_owned() + + "channel/" + + &self.slug() + + "-" + + &self.id.to_string() + } + + pub fn slug(&self) -> String { + let slug: String = self + .name + .chars() + .map(|c| if c.is_alphanumeric() { c } else { '-' }) + .collect(); + + slug.trim_matches(|c| c == '-').to_string() + } + + pub fn can_edit_notes(&self) -> bool { + self.role == proto::ChannelRole::Member || self.role == proto::ChannelRole::Admin + } +} + +pub struct ChannelMembership { + pub user: Arc, + pub kind: proto::channel_member::Kind, + pub role: proto::ChannelRole, +} +impl ChannelMembership { + pub fn sort_key(&self) -> MembershipSortKey { + MembershipSortKey { + role_order: match self.role { + proto::ChannelRole::Admin => 0, + proto::ChannelRole::Member => 1, + proto::ChannelRole::Banned => 2, + proto::ChannelRole::Guest => 3, + }, + kind_order: match self.kind { + proto::channel_member::Kind::Member => 0, + proto::channel_member::Kind::AncestorMember => 1, + proto::channel_member::Kind::Invitee => 2, + }, + username_order: self.user.github_login.as_str(), + } + } +} + +#[derive(PartialOrd, Ord, PartialEq, Eq)] +pub struct MembershipSortKey<'a> { + role_order: u8, + kind_order: u8, + username_order: &'a str, +} + +pub enum ChannelEvent { + ChannelCreated(ChannelId), + ChannelRenamed(ChannelId), +} + +impl EventEmitter for ChannelStore { + type Event = ChannelEvent; +} + +enum OpenedModelHandle { + Open(WeakModel), + Loading(Shared, Arc>>>), +} + +impl ChannelStore { + pub fn global(cx: &AppContext) -> Model { + cx.global::>().clone() + } + + pub fn new( + client: Arc, + user_store: Model, + cx: &mut ModelContext, + ) -> Self { + let rpc_subscription = + client.add_message_handler(cx.weak_model(), Self::handle_update_channels); + + let mut connection_status = client.status(); + let (update_channels_tx, mut update_channels_rx) = mpsc::unbounded(); + let watch_connection_status = cx.spawn(|this, mut cx| async move { + while let Some(status) = connection_status.next().await { + let this = this.upgrade()?; + match status { + client::Status::Connected { .. } => { + this.update(&mut cx, |this, cx| this.handle_connect(cx)) + .ok()? + .await + .log_err()?; + } + client::Status::SignedOut | client::Status::UpgradeRequired => { + this.update(&mut cx, |this, cx| this.handle_disconnect(false, cx)) + .ok(); + } + _ => { + this.update(&mut cx, |this, cx| this.handle_disconnect(true, cx)) + .ok(); + } + } + } + Some(()) + }); + + Self { + channel_invitations: Vec::default(), + channel_index: ChannelIndex::default(), + channel_participants: Default::default(), + outgoing_invites: Default::default(), + opened_buffers: Default::default(), + opened_chats: Default::default(), + update_channels_tx, + client, + user_store, + _rpc_subscription: rpc_subscription, + _watch_connection_status: watch_connection_status, + disconnect_channel_buffers_task: None, + _update_channels: cx.spawn(|this, mut cx| async move { + async_maybe!({ + while let Some(update_channels) = update_channels_rx.next().await { + if let Some(this) = this.upgrade() { + let update_task = this.update(&mut cx, |this, cx| { + this.update_channels(update_channels, cx) + })?; + if let Some(update_task) = update_task { + update_task.await.log_err(); + } + } + } + anyhow::Ok(()) + }) + .await + .log_err(); + }), + } + } + + pub fn client(&self) -> Arc { + self.client.clone() + } + + /// Returns the number of unique channels in the store + pub fn channel_count(&self) -> usize { + self.channel_index.by_id().len() + } + + /// Returns the index of a channel ID in the list of unique channels + pub fn index_of_channel(&self, channel_id: ChannelId) -> Option { + self.channel_index + .by_id() + .keys() + .position(|id| *id == channel_id) + } + + /// Returns an iterator over all unique channels + pub fn channels(&self) -> impl '_ + Iterator> { + self.channel_index.by_id().values() + } + + /// Iterate over all entries in the channel DAG + pub fn ordered_channels(&self) -> impl '_ + Iterator)> { + self.channel_index + .ordered_channels() + .iter() + .filter_map(move |id| { + let channel = self.channel_index.by_id().get(id)?; + Some((channel.parent_path.len(), channel)) + }) + } + + pub fn channel_at_index(&self, ix: usize) -> Option<&Arc> { + let channel_id = self.channel_index.ordered_channels().get(ix)?; + self.channel_index.by_id().get(channel_id) + } + + pub fn channel_at(&self, ix: usize) -> Option<&Arc> { + self.channel_index.by_id().values().nth(ix) + } + + pub fn has_channel_invitation(&self, channel_id: ChannelId) -> bool { + self.channel_invitations + .iter() + .any(|channel| channel.id == channel_id) + } + + pub fn channel_invitations(&self) -> &[Arc] { + &self.channel_invitations + } + + pub fn channel_for_id(&self, channel_id: ChannelId) -> Option<&Arc> { + self.channel_index.by_id().get(&channel_id) + } + + pub fn has_open_channel_buffer(&self, channel_id: ChannelId, _cx: &AppContext) -> bool { + if let Some(buffer) = self.opened_buffers.get(&channel_id) { + if let OpenedModelHandle::Open(buffer) = buffer { + return buffer.upgrade().is_some(); + } + } + false + } + + pub fn open_channel_buffer( + &mut self, + channel_id: ChannelId, + cx: &mut ModelContext, + ) -> Task>> { + let client = self.client.clone(); + let user_store = self.user_store.clone(); + let channel_store = cx.handle(); + self.open_channel_resource( + channel_id, + |this| &mut this.opened_buffers, + |channel, cx| ChannelBuffer::new(channel, client, user_store, channel_store, cx), + cx, + ) + } + + pub fn fetch_channel_messages( + &self, + message_ids: Vec, + cx: &mut ModelContext, + ) -> Task>> { + let request = if message_ids.is_empty() { + None + } else { + Some( + self.client + .request(proto::GetChannelMessagesById { message_ids }), + ) + }; + cx.spawn(|this, mut cx| async move { + if let Some(request) = request { + let response = request.await?; + let this = this + .upgrade() + .ok_or_else(|| anyhow!("channel store dropped"))?; + let user_store = this.update(&mut cx, |this, _| this.user_store.clone())?; + ChannelMessage::from_proto_vec(response.messages, &user_store, &mut cx).await + } else { + Ok(Vec::new()) + } + }) + } + + pub fn has_channel_buffer_changed(&self, channel_id: ChannelId) -> Option { + self.channel_index + .by_id() + .get(&channel_id) + .map(|channel| channel.unseen_note_version.is_some()) + } + + pub fn has_new_messages(&self, channel_id: ChannelId) -> Option { + self.channel_index + .by_id() + .get(&channel_id) + .map(|channel| channel.unseen_message_id.is_some()) + } + + pub fn notes_changed( + &mut self, + channel_id: ChannelId, + epoch: u64, + version: &clock::Global, + cx: &mut ModelContext, + ) { + self.channel_index.note_changed(channel_id, epoch, version); + cx.notify(); + } + + pub fn new_message( + &mut self, + channel_id: ChannelId, + message_id: u64, + cx: &mut ModelContext, + ) { + self.channel_index.new_message(channel_id, message_id); + cx.notify(); + } + + pub fn acknowledge_message_id( + &mut self, + channel_id: ChannelId, + message_id: u64, + cx: &mut ModelContext, + ) { + self.channel_index + .acknowledge_message_id(channel_id, message_id); + cx.notify(); + } + + pub fn acknowledge_notes_version( + &mut self, + channel_id: ChannelId, + epoch: u64, + version: &clock::Global, + cx: &mut ModelContext, + ) { + self.channel_index + .acknowledge_note_version(channel_id, epoch, version); + cx.notify(); + } + + pub fn open_channel_chat( + &mut self, + channel_id: ChannelId, + cx: &mut ModelContext, + ) -> Task>> { + let client = self.client.clone(); + let user_store = self.user_store.clone(); + let this = cx.handle(); + self.open_channel_resource( + channel_id, + |this| &mut this.opened_chats, + |channel, cx| ChannelChat::new(channel, this, user_store, client, cx), + cx, + ) + } + + /// Asynchronously open a given resource associated with a channel. + /// + /// Make sure that the resource is only opened once, even if this method + /// is called multiple times with the same channel id while the first task + /// is still running. + fn open_channel_resource( + &mut self, + channel_id: ChannelId, + get_map: fn(&mut Self) -> &mut HashMap>, + load: F, + cx: &mut ModelContext, + ) -> Task>> + where + F: 'static + FnOnce(Arc, AsyncAppContext) -> Fut, + Fut: Future>>, + T: 'static, + { + let task = loop { + match get_map(self).entry(channel_id) { + hash_map::Entry::Occupied(e) => match e.get() { + OpenedModelHandle::Open(model) => { + if let Some(model) = model.upgrade() { + break Task::ready(Ok(model)).shared(); + } else { + get_map(self).remove(&channel_id); + continue; + } + } + OpenedModelHandle::Loading(task) => { + break task.clone(); + } + }, + hash_map::Entry::Vacant(e) => { + let task = cx + .spawn(move |this, mut cx| async move { + let channel = this.update(&mut cx, |this, _| { + this.channel_for_id(channel_id).cloned().ok_or_else(|| { + Arc::new(anyhow!("no channel for id: {}", channel_id)) + }) + })??; + + load(channel, cx).await.map_err(Arc::new) + }) + .shared(); + + e.insert(OpenedModelHandle::Loading(task.clone())); + cx.spawn({ + let task = task.clone(); + move |this, mut cx| async move { + let result = task.await; + this.update(&mut cx, |this, _| match result { + Ok(model) => { + get_map(this).insert( + channel_id, + OpenedModelHandle::Open(model.downgrade()), + ); + } + Err(_) => { + get_map(this).remove(&channel_id); + } + }) + .ok(); + } + }) + .detach(); + break task; + } + } + }; + cx.background_executor() + .spawn(async move { task.await.map_err(|error| anyhow!("{}", error)) }) + } + + pub fn is_channel_admin(&self, channel_id: ChannelId) -> bool { + let Some(channel) = self.channel_for_id(channel_id) else { + return false; + }; + channel.role == proto::ChannelRole::Admin + } + + pub fn channel_participants(&self, channel_id: ChannelId) -> &[Arc] { + self.channel_participants + .get(&channel_id) + .map_or(&[], |v| v.as_slice()) + } + + pub fn create_channel( + &self, + name: &str, + parent_id: Option, + cx: &mut ModelContext, + ) -> Task> { + let client = self.client.clone(); + let name = name.trim_start_matches("#").to_owned(); + cx.spawn(move |this, mut cx| async move { + let response = client + .request(proto::CreateChannel { name, parent_id }) + .await?; + + let channel = response + .channel + .ok_or_else(|| anyhow!("missing channel in response"))?; + let channel_id = channel.id; + + this.update(&mut cx, |this, cx| { + let task = this.update_channels( + proto::UpdateChannels { + channels: vec![channel], + ..Default::default() + }, + cx, + ); + assert!(task.is_none()); + + // This event is emitted because the collab panel wants to clear the pending edit state + // before this frame is rendered. But we can't guarantee that the collab panel's future + // will resolve before this flush_effects finishes. Synchronously emitting this event + // ensures that the collab panel will observe this creation before the frame completes + cx.emit(ChannelEvent::ChannelCreated(channel_id)); + })?; + + Ok(channel_id) + }) + } + + pub fn move_channel( + &mut self, + channel_id: ChannelId, + to: Option, + cx: &mut ModelContext, + ) -> Task> { + let client = self.client.clone(); + cx.spawn(move |_, _| async move { + let _ = client + .request(proto::MoveChannel { channel_id, to }) + .await?; + + Ok(()) + }) + } + + pub fn set_channel_visibility( + &mut self, + channel_id: ChannelId, + visibility: ChannelVisibility, + cx: &mut ModelContext, + ) -> Task> { + let client = self.client.clone(); + cx.spawn(move |_, _| async move { + let _ = client + .request(proto::SetChannelVisibility { + channel_id, + visibility: visibility.into(), + }) + .await?; + + Ok(()) + }) + } + + pub fn invite_member( + &mut self, + channel_id: ChannelId, + user_id: UserId, + role: proto::ChannelRole, + cx: &mut ModelContext, + ) -> Task> { + if !self.outgoing_invites.insert((channel_id, user_id)) { + return Task::ready(Err(anyhow!("invite request already in progress"))); + } + + cx.notify(); + let client = self.client.clone(); + cx.spawn(move |this, mut cx| async move { + let result = client + .request(proto::InviteChannelMember { + channel_id, + user_id, + role: role.into(), + }) + .await; + + this.update(&mut cx, |this, cx| { + this.outgoing_invites.remove(&(channel_id, user_id)); + cx.notify(); + })?; + + result?; + + Ok(()) + }) + } + + pub fn remove_member( + &mut self, + channel_id: ChannelId, + user_id: u64, + cx: &mut ModelContext, + ) -> Task> { + if !self.outgoing_invites.insert((channel_id, user_id)) { + return Task::ready(Err(anyhow!("invite request already in progress"))); + } + + cx.notify(); + let client = self.client.clone(); + cx.spawn(move |this, mut cx| async move { + let result = client + .request(proto::RemoveChannelMember { + channel_id, + user_id, + }) + .await; + + this.update(&mut cx, |this, cx| { + this.outgoing_invites.remove(&(channel_id, user_id)); + cx.notify(); + })?; + result?; + Ok(()) + }) + } + + pub fn set_member_role( + &mut self, + channel_id: ChannelId, + user_id: UserId, + role: proto::ChannelRole, + cx: &mut ModelContext, + ) -> Task> { + if !self.outgoing_invites.insert((channel_id, user_id)) { + return Task::ready(Err(anyhow!("member request already in progress"))); + } + + cx.notify(); + let client = self.client.clone(); + cx.spawn(move |this, mut cx| async move { + let result = client + .request(proto::SetChannelMemberRole { + channel_id, + user_id, + role: role.into(), + }) + .await; + + this.update(&mut cx, |this, cx| { + this.outgoing_invites.remove(&(channel_id, user_id)); + cx.notify(); + })?; + + result?; + Ok(()) + }) + } + + pub fn rename( + &mut self, + channel_id: ChannelId, + new_name: &str, + cx: &mut ModelContext, + ) -> Task> { + let client = self.client.clone(); + let name = new_name.to_string(); + cx.spawn(move |this, mut cx| async move { + let channel = client + .request(proto::RenameChannel { channel_id, name }) + .await? + .channel + .ok_or_else(|| anyhow!("missing channel in response"))?; + this.update(&mut cx, |this, cx| { + let task = this.update_channels( + proto::UpdateChannels { + channels: vec![channel], + ..Default::default() + }, + cx, + ); + assert!(task.is_none()); + + // This event is emitted because the collab panel wants to clear the pending edit state + // before this frame is rendered. But we can't guarantee that the collab panel's future + // will resolve before this flush_effects finishes. Synchronously emitting this event + // ensures that the collab panel will observe this creation before the frame complete + cx.emit(ChannelEvent::ChannelRenamed(channel_id)) + })?; + Ok(()) + }) + } + + pub fn respond_to_channel_invite( + &mut self, + channel_id: ChannelId, + accept: bool, + cx: &mut ModelContext, + ) -> Task> { + let client = self.client.clone(); + cx.background_executor().spawn(async move { + client + .request(proto::RespondToChannelInvite { channel_id, accept }) + .await?; + Ok(()) + }) + } + + pub fn get_channel_member_details( + &self, + channel_id: ChannelId, + cx: &mut ModelContext, + ) -> Task>> { + let client = self.client.clone(); + let user_store = self.user_store.downgrade(); + cx.spawn(move |_, mut cx| async move { + let response = client + .request(proto::GetChannelMembers { channel_id }) + .await?; + + let user_ids = response.members.iter().map(|m| m.user_id).collect(); + let user_store = user_store + .upgrade() + .ok_or_else(|| anyhow!("user store dropped"))?; + let users = user_store + .update(&mut cx, |user_store, cx| user_store.get_users(user_ids, cx))? + .await?; + + Ok(users + .into_iter() + .zip(response.members) + .filter_map(|(user, member)| { + Some(ChannelMembership { + user, + role: member.role(), + kind: member.kind(), + }) + }) + .collect()) + }) + } + + pub fn remove_channel(&self, channel_id: ChannelId) -> impl Future> { + let client = self.client.clone(); + async move { + client.request(proto::DeleteChannel { channel_id }).await?; + Ok(()) + } + } + + pub fn has_pending_channel_invite_response(&self, _: &Arc) -> bool { + false + } + + pub fn has_pending_channel_invite(&self, channel_id: ChannelId, user_id: UserId) -> bool { + self.outgoing_invites.contains(&(channel_id, user_id)) + } + + async fn handle_update_channels( + this: Model, + message: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result<()> { + this.update(&mut cx, |this, _| { + this.update_channels_tx + .unbounded_send(message.payload) + .unwrap(); + })?; + Ok(()) + } + + fn handle_connect(&mut self, cx: &mut ModelContext) -> Task> { + self.channel_index.clear(); + self.channel_invitations.clear(); + self.channel_participants.clear(); + self.channel_index.clear(); + self.outgoing_invites.clear(); + self.disconnect_channel_buffers_task.take(); + + for chat in self.opened_chats.values() { + if let OpenedModelHandle::Open(chat) = chat { + if let Some(chat) = chat.upgrade() { + chat.update(cx, |chat, cx| { + chat.rejoin(cx); + }); + } + } + } + + let mut buffer_versions = Vec::new(); + for buffer in self.opened_buffers.values() { + if let OpenedModelHandle::Open(buffer) = buffer { + if let Some(buffer) = buffer.upgrade() { + let channel_buffer = buffer.read(cx); + let buffer = channel_buffer.buffer().read(cx); + buffer_versions.push(proto::ChannelBufferVersion { + channel_id: channel_buffer.channel_id, + epoch: channel_buffer.epoch(), + version: language::proto::serialize_version(&buffer.version()), + }); + } + } + } + + if buffer_versions.is_empty() { + return Task::ready(Ok(())); + } + + let response = self.client.request(proto::RejoinChannelBuffers { + buffers: buffer_versions, + }); + + cx.spawn(|this, mut cx| async move { + let mut response = response.await?; + + this.update(&mut cx, |this, cx| { + this.opened_buffers.retain(|_, buffer| match buffer { + OpenedModelHandle::Open(channel_buffer) => { + let Some(channel_buffer) = channel_buffer.upgrade() else { + return false; + }; + + channel_buffer.update(cx, |channel_buffer, cx| { + let channel_id = channel_buffer.channel_id; + if let Some(remote_buffer) = response + .buffers + .iter_mut() + .find(|buffer| buffer.channel_id == channel_id) + { + let channel_id = channel_buffer.channel_id; + let remote_version = + language::proto::deserialize_version(&remote_buffer.version); + + channel_buffer.replace_collaborators( + mem::take(&mut remote_buffer.collaborators), + cx, + ); + + let operations = channel_buffer + .buffer() + .update(cx, |buffer, cx| { + let outgoing_operations = + buffer.serialize_ops(Some(remote_version), cx); + let incoming_operations = + mem::take(&mut remote_buffer.operations) + .into_iter() + .map(language::proto::deserialize_operation) + .collect::>>()?; + buffer.apply_ops(incoming_operations, cx)?; + anyhow::Ok(outgoing_operations) + }) + .log_err(); + + if let Some(operations) = operations { + let client = this.client.clone(); + cx.background_executor() + .spawn(async move { + let operations = operations.await; + for chunk in + language::proto::split_operations(operations) + { + client + .send(proto::UpdateChannelBuffer { + channel_id, + operations: chunk, + }) + .ok(); + } + }) + .detach(); + return true; + } + } + + channel_buffer.disconnect(cx); + false + }) + } + OpenedModelHandle::Loading(_) => true, + }); + }) + .ok(); + anyhow::Ok(()) + }) + } + + fn handle_disconnect(&mut self, wait_for_reconnect: bool, cx: &mut ModelContext) { + cx.notify(); + + self.disconnect_channel_buffers_task.get_or_insert_with(|| { + cx.spawn(move |this, mut cx| async move { + if wait_for_reconnect { + cx.background_executor().timer(RECONNECT_TIMEOUT).await; + } + + if let Some(this) = this.upgrade() { + this.update(&mut cx, |this, cx| { + for (_, buffer) in this.opened_buffers.drain() { + if let OpenedModelHandle::Open(buffer) = buffer { + if let Some(buffer) = buffer.upgrade() { + buffer.update(cx, |buffer, cx| buffer.disconnect(cx)); + } + } + } + }) + .ok(); + } + }) + }); + } + + pub(crate) fn update_channels( + &mut self, + payload: proto::UpdateChannels, + cx: &mut ModelContext, + ) -> Option>> { + if !payload.remove_channel_invitations.is_empty() { + self.channel_invitations + .retain(|channel| !payload.remove_channel_invitations.contains(&channel.id)); + } + for channel in payload.channel_invitations { + match self + .channel_invitations + .binary_search_by_key(&channel.id, |c| c.id) + { + Ok(ix) => Arc::make_mut(&mut self.channel_invitations[ix]).name = channel.name, + Err(ix) => self.channel_invitations.insert( + ix, + Arc::new(Channel { + id: channel.id, + visibility: channel.visibility(), + role: channel.role(), + name: channel.name, + unseen_note_version: None, + unseen_message_id: None, + parent_path: channel.parent_path, + }), + ), + } + } + + let channels_changed = !payload.channels.is_empty() + || !payload.delete_channels.is_empty() + || !payload.unseen_channel_messages.is_empty() + || !payload.unseen_channel_buffer_changes.is_empty(); + + if channels_changed { + if !payload.delete_channels.is_empty() { + self.channel_index.delete_channels(&payload.delete_channels); + self.channel_participants + .retain(|channel_id, _| !&payload.delete_channels.contains(channel_id)); + + for channel_id in &payload.delete_channels { + let channel_id = *channel_id; + if payload + .channels + .iter() + .any(|channel| channel.id == channel_id) + { + continue; + } + if let Some(OpenedModelHandle::Open(buffer)) = + self.opened_buffers.remove(&channel_id) + { + if let Some(buffer) = buffer.upgrade() { + buffer.update(cx, ChannelBuffer::disconnect); + } + } + } + } + + let mut index = self.channel_index.bulk_insert(); + for channel in payload.channels { + 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() { + buffer.update(cx, ChannelBuffer::channel_changed); + } + } + } + } + + for unseen_buffer_change in payload.unseen_channel_buffer_changes { + let version = language::proto::deserialize_version(&unseen_buffer_change.version); + index.note_changed( + unseen_buffer_change.channel_id, + unseen_buffer_change.epoch, + &version, + ); + } + + for unseen_channel_message in payload.unseen_channel_messages { + index.new_messages( + unseen_channel_message.channel_id, + unseen_channel_message.message_id, + ); + } + } + + cx.notify(); + if payload.channel_participants.is_empty() { + return None; + } + + let mut all_user_ids = Vec::new(); + let channel_participants = payload.channel_participants; + for entry in &channel_participants { + for user_id in entry.participant_user_ids.iter() { + if let Err(ix) = all_user_ids.binary_search(user_id) { + all_user_ids.insert(ix, *user_id); + } + } + } + + let users = self + .user_store + .update(cx, |user_store, cx| user_store.get_users(all_user_ids, cx)); + Some(cx.spawn(|this, mut cx| async move { + let users = users.await?; + + this.update(&mut cx, |this, cx| { + for entry in &channel_participants { + let mut participants: Vec<_> = entry + .participant_user_ids + .iter() + .filter_map(|user_id| { + users + .binary_search_by_key(&user_id, |user| &user.id) + .ok() + .map(|ix| users[ix].clone()) + }) + .collect(); + + participants.sort_by_key(|u| u.id); + + this.channel_participants + .insert(entry.channel_id, participants); + } + + cx.notify(); + }) + })) + } +} diff --git a/crates/channel2/src/channel_store/channel_index.rs b/crates/channel2/src/channel_store/channel_index.rs new file mode 100644 index 0000000000..97b2ab6318 --- /dev/null +++ b/crates/channel2/src/channel_store/channel_index.rs @@ -0,0 +1,184 @@ +use crate::{Channel, ChannelId}; +use collections::BTreeMap; +use rpc::proto; +use std::sync::Arc; + +#[derive(Default, Debug)] +pub struct ChannelIndex { + channels_ordered: Vec, + channels_by_id: BTreeMap>, +} + +impl ChannelIndex { + pub fn by_id(&self) -> &BTreeMap> { + &self.channels_by_id + } + + pub fn ordered_channels(&self) -> &[ChannelId] { + &self.channels_ordered + } + + pub fn clear(&mut self) { + self.channels_ordered.clear(); + self.channels_by_id.clear(); + } + + /// Delete the given channels from this index. + pub fn delete_channels(&mut self, channels: &[ChannelId]) { + self.channels_by_id + .retain(|channel_id, _| !channels.contains(channel_id)); + self.channels_ordered + .retain(|channel_id| !channels.contains(channel_id)); + } + + pub fn bulk_insert(&mut self) -> ChannelPathsInsertGuard { + ChannelPathsInsertGuard { + channels_ordered: &mut self.channels_ordered, + channels_by_id: &mut self.channels_by_id, + } + } + + pub fn acknowledge_note_version( + &mut self, + channel_id: ChannelId, + epoch: u64, + version: &clock::Global, + ) { + if let Some(channel) = self.channels_by_id.get_mut(&channel_id) { + let channel = Arc::make_mut(channel); + if let Some((unseen_epoch, unseen_version)) = &channel.unseen_note_version { + if epoch > *unseen_epoch + || epoch == *unseen_epoch && version.observed_all(unseen_version) + { + channel.unseen_note_version = None; + } + } + } + } + + pub fn acknowledge_message_id(&mut self, channel_id: ChannelId, message_id: u64) { + if let Some(channel) = self.channels_by_id.get_mut(&channel_id) { + let channel = Arc::make_mut(channel); + if let Some(unseen_message_id) = channel.unseen_message_id { + if message_id >= unseen_message_id { + channel.unseen_message_id = None; + } + } + } + } + + 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); + } + + pub fn new_message(&mut self, channel_id: ChannelId, message_id: u64) { + insert_new_message(&mut self.channels_by_id, channel_id, message_id) + } +} + +/// A guard for ensuring that the paths index maintains its sort and uniqueness +/// invariants after a series of insertions +#[derive(Debug)] +pub struct ChannelPathsInsertGuard<'a> { + channels_ordered: &'a mut Vec, + channels_by_id: &'a mut BTreeMap>, +} + +impl<'a> ChannelPathsInsertGuard<'a> { + 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); + } + + pub fn new_messages(&mut self, channel_id: ChannelId, message_id: u64) { + insert_new_message(&mut self.channels_by_id, channel_id, message_id) + } + + pub fn insert(&mut self, channel_proto: proto::Channel) -> bool { + let mut ret = false; + if let Some(existing_channel) = self.channels_by_id.get_mut(&channel_proto.id) { + let existing_channel = Arc::make_mut(existing_channel); + + ret = existing_channel.visibility != channel_proto.visibility() + || existing_channel.role != channel_proto.role() + || existing_channel.name != channel_proto.name; + + existing_channel.visibility = channel_proto.visibility(); + existing_channel.role = channel_proto.role(); + existing_channel.name = channel_proto.name; + } else { + self.channels_by_id.insert( + channel_proto.id, + Arc::new(Channel { + id: channel_proto.id, + visibility: channel_proto.visibility(), + role: channel_proto.role(), + name: channel_proto.name, + unseen_note_version: None, + unseen_message_id: None, + parent_path: channel_proto.parent_path, + }), + ); + self.insert_root(channel_proto.id); + } + ret + } + + fn insert_root(&mut self, channel_id: ChannelId) { + self.channels_ordered.push(channel_id); + } +} + +impl<'a> Drop for ChannelPathsInsertGuard<'a> { + fn drop(&mut self) { + self.channels_ordered.sort_by(|a, b| { + let a = channel_path_sorting_key(*a, &self.channels_by_id); + let b = channel_path_sorting_key(*b, &self.channels_by_id); + a.cmp(b) + }); + self.channels_ordered.dedup(); + } +} + +fn channel_path_sorting_key<'a>( + id: ChannelId, + channels_by_id: &'a BTreeMap>, +) -> impl Iterator { + let (parent_path, name) = channels_by_id + .get(&id) + .map_or((&[] as &[_], None), |channel| { + (channel.parent_path.as_slice(), Some(channel.name.as_str())) + }); + parent_path + .iter() + .filter_map(|id| Some(channels_by_id.get(id)?.name.as_str())) + .chain(name) +} + +fn insert_note_changed( + channels_by_id: &mut BTreeMap>, + channel_id: u64, + epoch: u64, + version: &clock::Global, +) { + if let Some(channel) = channels_by_id.get_mut(&channel_id) { + let unseen_version = Arc::make_mut(channel) + .unseen_note_version + .get_or_insert((0, clock::Global::new())); + if epoch > unseen_version.0 { + *unseen_version = (epoch, version.clone()); + } else { + unseen_version.1.join(&version); + } + } +} + +fn insert_new_message( + channels_by_id: &mut BTreeMap>, + channel_id: u64, + message_id: u64, +) { + if let Some(channel) = channels_by_id.get_mut(&channel_id) { + let unseen_message_id = Arc::make_mut(channel).unseen_message_id.get_or_insert(0); + *unseen_message_id = message_id.max(*unseen_message_id); + } +} diff --git a/crates/channel2/src/channel_store_tests.rs b/crates/channel2/src/channel_store_tests.rs new file mode 100644 index 0000000000..e193917b76 --- /dev/null +++ b/crates/channel2/src/channel_store_tests.rs @@ -0,0 +1,380 @@ +use crate::channel_chat::ChannelChatEvent; + +use super::*; +use client::{test::FakeServer, Client, UserStore}; +use gpui::{AppContext, Context, Model, TestAppContext}; +use rpc::proto::{self}; +use settings::SettingsStore; +use util::http::FakeHttpClient; + +#[gpui::test] +fn test_update_channels(cx: &mut AppContext) { + let channel_store = init_test(cx); + + update_channels( + &channel_store, + proto::UpdateChannels { + channels: vec![ + proto::Channel { + id: 1, + name: "b".to_string(), + visibility: proto::ChannelVisibility::Members as i32, + role: proto::ChannelRole::Admin.into(), + parent_path: Vec::new(), + }, + proto::Channel { + id: 2, + name: "a".to_string(), + visibility: proto::ChannelVisibility::Members as i32, + role: proto::ChannelRole::Member.into(), + parent_path: Vec::new(), + }, + ], + ..Default::default() + }, + cx, + ); + assert_channels( + &channel_store, + &[ + // + (0, "a".to_string(), proto::ChannelRole::Member), + (0, "b".to_string(), proto::ChannelRole::Admin), + ], + cx, + ); + + update_channels( + &channel_store, + proto::UpdateChannels { + channels: vec![ + proto::Channel { + id: 3, + name: "x".to_string(), + visibility: proto::ChannelVisibility::Members as i32, + role: proto::ChannelRole::Admin.into(), + parent_path: vec![1], + }, + proto::Channel { + id: 4, + name: "y".to_string(), + visibility: proto::ChannelVisibility::Members as i32, + role: proto::ChannelRole::Member.into(), + parent_path: vec![2], + }, + ], + ..Default::default() + }, + cx, + ); + assert_channels( + &channel_store, + &[ + (0, "a".to_string(), proto::ChannelRole::Member), + (1, "y".to_string(), proto::ChannelRole::Member), + (0, "b".to_string(), proto::ChannelRole::Admin), + (1, "x".to_string(), proto::ChannelRole::Admin), + ], + cx, + ); +} + +#[gpui::test] +fn test_dangling_channel_paths(cx: &mut AppContext) { + let channel_store = init_test(cx); + + update_channels( + &channel_store, + proto::UpdateChannels { + channels: vec![ + proto::Channel { + id: 0, + name: "a".to_string(), + visibility: proto::ChannelVisibility::Members as i32, + role: proto::ChannelRole::Admin.into(), + parent_path: vec![], + }, + proto::Channel { + id: 1, + name: "b".to_string(), + visibility: proto::ChannelVisibility::Members as i32, + role: proto::ChannelRole::Admin.into(), + parent_path: vec![0], + }, + proto::Channel { + id: 2, + name: "c".to_string(), + visibility: proto::ChannelVisibility::Members as i32, + role: proto::ChannelRole::Admin.into(), + parent_path: vec![0, 1], + }, + ], + ..Default::default() + }, + cx, + ); + // Sanity check + assert_channels( + &channel_store, + &[ + // + (0, "a".to_string(), proto::ChannelRole::Admin), + (1, "b".to_string(), proto::ChannelRole::Admin), + (2, "c".to_string(), proto::ChannelRole::Admin), + ], + cx, + ); + + update_channels( + &channel_store, + proto::UpdateChannels { + delete_channels: vec![1, 2], + ..Default::default() + }, + cx, + ); + + // Make sure that the 1/2/3 path is gone + assert_channels( + &channel_store, + &[(0, "a".to_string(), proto::ChannelRole::Admin)], + cx, + ); +} + +#[gpui::test] +async fn test_channel_messages(cx: &mut TestAppContext) { + let user_id = 5; + let channel_id = 5; + let channel_store = cx.update(init_test); + let client = channel_store.update(cx, |s, _| s.client()); + let server = FakeServer::for_client(user_id, &client, cx).await; + + // Get the available channels. + server.send(proto::UpdateChannels { + channels: vec![proto::Channel { + id: channel_id, + name: "the-channel".to_string(), + visibility: proto::ChannelVisibility::Members as i32, + role: proto::ChannelRole::Member.into(), + parent_path: vec![], + }], + ..Default::default() + }); + cx.executor().run_until_parked(); + cx.update(|cx| { + assert_channels( + &channel_store, + &[(0, "the-channel".to_string(), proto::ChannelRole::Member)], + cx, + ); + }); + + let get_users = server.receive::().await.unwrap(); + assert_eq!(get_users.payload.user_ids, vec![5]); + server.respond( + get_users.receipt(), + proto::UsersResponse { + users: vec![proto::User { + id: 5, + github_login: "nathansobo".into(), + avatar_url: "http://avatar.com/nathansobo".into(), + }], + }, + ); + + // Join a channel and populate its existing messages. + let channel = channel_store.update(cx, |store, cx| { + let channel_id = store.ordered_channels().next().unwrap().1.id; + store.open_channel_chat(channel_id, cx) + }); + let join_channel = server.receive::().await.unwrap(); + server.respond( + join_channel.receipt(), + proto::JoinChannelChatResponse { + messages: vec![ + proto::ChannelMessage { + id: 10, + body: "a".into(), + timestamp: 1000, + sender_id: 5, + mentions: vec![], + nonce: Some(1.into()), + }, + proto::ChannelMessage { + id: 11, + body: "b".into(), + timestamp: 1001, + sender_id: 6, + mentions: vec![], + nonce: Some(2.into()), + }, + ], + done: false, + }, + ); + + cx.executor().start_waiting(); + + // Client requests all users for the received messages + let mut get_users = server.receive::().await.unwrap(); + get_users.payload.user_ids.sort(); + assert_eq!(get_users.payload.user_ids, vec![6]); + server.respond( + get_users.receipt(), + proto::UsersResponse { + users: vec![proto::User { + id: 6, + github_login: "maxbrunsfeld".into(), + avatar_url: "http://avatar.com/maxbrunsfeld".into(), + }], + }, + ); + + let channel = channel.await.unwrap(); + channel.update(cx, |channel, _| { + assert_eq!( + channel + .messages_in_range(0..2) + .map(|message| (message.sender.github_login.clone(), message.body.clone())) + .collect::>(), + &[ + ("nathansobo".into(), "a".into()), + ("maxbrunsfeld".into(), "b".into()) + ] + ); + }); + + // Receive a new message. + server.send(proto::ChannelMessageSent { + channel_id, + message: Some(proto::ChannelMessage { + id: 12, + body: "c".into(), + timestamp: 1002, + sender_id: 7, + mentions: vec![], + nonce: Some(3.into()), + }), + }); + + // Client requests user for message since they haven't seen them yet + let get_users = server.receive::().await.unwrap(); + assert_eq!(get_users.payload.user_ids, vec![7]); + server.respond( + get_users.receipt(), + proto::UsersResponse { + users: vec![proto::User { + id: 7, + github_login: "as-cii".into(), + avatar_url: "http://avatar.com/as-cii".into(), + }], + }, + ); + + assert_eq!( + channel.next_event(cx), + ChannelChatEvent::MessagesUpdated { + old_range: 2..2, + new_count: 1, + } + ); + channel.update(cx, |channel, _| { + assert_eq!( + channel + .messages_in_range(2..3) + .map(|message| (message.sender.github_login.clone(), message.body.clone())) + .collect::>(), + &[("as-cii".into(), "c".into())] + ) + }); + + // Scroll up to view older messages. + channel.update(cx, |channel, cx| { + channel.load_more_messages(cx).unwrap().detach(); + }); + let get_messages = server.receive::().await.unwrap(); + assert_eq!(get_messages.payload.channel_id, 5); + assert_eq!(get_messages.payload.before_message_id, 10); + server.respond( + get_messages.receipt(), + proto::GetChannelMessagesResponse { + done: true, + messages: vec![ + proto::ChannelMessage { + id: 8, + body: "y".into(), + timestamp: 998, + sender_id: 5, + nonce: Some(4.into()), + mentions: vec![], + }, + proto::ChannelMessage { + id: 9, + body: "z".into(), + timestamp: 999, + sender_id: 6, + nonce: Some(5.into()), + mentions: vec![], + }, + ], + }, + ); + + assert_eq!( + channel.next_event(cx), + ChannelChatEvent::MessagesUpdated { + old_range: 0..0, + new_count: 2, + } + ); + channel.update(cx, |channel, _| { + assert_eq!( + channel + .messages_in_range(0..2) + .map(|message| (message.sender.github_login.clone(), message.body.clone())) + .collect::>(), + &[ + ("nathansobo".into(), "y".into()), + ("maxbrunsfeld".into(), "z".into()) + ] + ); + }); +} + +fn init_test(cx: &mut AppContext) -> Model { + let http = FakeHttpClient::with_404_response(); + let client = Client::new(http.clone(), cx); + let user_store = cx.build_model(|cx| UserStore::new(client.clone(), http, cx)); + + let settings_store = SettingsStore::test(cx); + cx.set_global(settings_store); + client::init(&client, cx); + crate::init(&client, user_store, cx); + + ChannelStore::global(cx) +} + +fn update_channels( + channel_store: &Model, + message: proto::UpdateChannels, + cx: &mut AppContext, +) { + let task = channel_store.update(cx, |store, cx| store.update_channels(message, cx)); + assert!(task.is_none()); +} + +#[track_caller] +fn assert_channels( + channel_store: &Model, + expected_channels: &[(usize, String, proto::ChannelRole)], + cx: &mut AppContext, +) { + let actual = channel_store.update(cx, |store, _| { + store + .ordered_channels() + .map(|(depth, channel)| (depth, channel.name.to_string(), channel.role)) + .collect::>() + }); + assert_eq!(actual, expected_channels); +} diff --git a/crates/client2/src/user.rs b/crates/client2/src/user.rs index baf3a19dad..8ff134e6b7 100644 --- a/crates/client2/src/user.rs +++ b/crates/client2/src/user.rs @@ -292,22 +292,18 @@ impl UserStore { .upgrade() .ok_or_else(|| anyhow!("can't upgrade user store handle"))?; for contact in message.contacts { - let should_notify = contact.should_notify; - updated_contacts.push(( - Arc::new(Contact::from_proto(contact, &this, &mut cx).await?), - should_notify, + updated_contacts.push(Arc::new( + Contact::from_proto(contact, &this, &mut cx).await?, )); } let mut incoming_requests = Vec::new(); for request in message.incoming_requests { incoming_requests.push({ - let user = this - .update(&mut cx, |this, cx| { - this.get_user(request.requester_id, cx) - })? - .await?; - (user, request.should_notify) + this.update(&mut cx, |this, cx| { + this.get_user(request.requester_id, cx) + })? + .await? }); } @@ -331,13 +327,7 @@ impl UserStore { this.contacts .retain(|contact| !removed_contacts.contains(&contact.user.id)); // Update existing contacts and insert new ones - for (updated_contact, should_notify) in updated_contacts { - if should_notify { - cx.emit(Event::Contact { - user: updated_contact.user.clone(), - kind: ContactEventKind::Accepted, - }); - } + for updated_contact in updated_contacts { match this.contacts.binary_search_by_key( &&updated_contact.user.github_login, |contact| &contact.user.github_login, @@ -360,14 +350,7 @@ impl UserStore { } }); // Update existing incoming requests and insert new ones - for (user, should_notify) in incoming_requests { - if should_notify { - cx.emit(Event::Contact { - user: user.clone(), - kind: ContactEventKind::Requested, - }); - } - + for user in incoming_requests { match this .incoming_contact_requests .binary_search_by_key(&&user.github_login, |contact| { diff --git a/crates/collab_ui/src/channel_view.rs b/crates/collab_ui/src/channel_view.rs index 1bdcebd018..fe46f3bb3e 100644 --- a/crates/collab_ui/src/channel_view.rs +++ b/crates/collab_ui/src/channel_view.rs @@ -75,23 +75,23 @@ impl ChannelView { let workspace = workspace.read(cx); let project = workspace.project().to_owned(); let channel_store = ChannelStore::global(cx); - let markdown = workspace - .app_state() - .languages - .language_for_name("Markdown"); + let language_registry = workspace.app_state().languages.clone(); + let markdown = language_registry.language_for_name("Markdown"); let channel_buffer = channel_store.update(cx, |store, cx| store.open_channel_buffer(channel_id, cx)); cx.spawn(|mut cx| async move { let channel_buffer = channel_buffer.await?; + let markdown = markdown.await.log_err(); - if let Some(markdown) = markdown.await.log_err() { - channel_buffer.update(&mut cx, |buffer, cx| { - buffer.buffer().update(cx, |buffer, cx| { + channel_buffer.update(&mut cx, |buffer, cx| { + buffer.buffer().update(cx, |buffer, cx| { + buffer.set_language_registry(language_registry); + if let Some(markdown) = markdown { buffer.set_language(Some(markdown), cx); - }) - }); - } + } + }) + }); pane.update(&mut cx, |pane, cx| { let buffer_id = channel_buffer.read(cx).remote_id(cx); diff --git a/crates/diagnostics/Cargo.toml b/crates/diagnostics/Cargo.toml index b0b2450f05..0f9d108831 100644 --- a/crates/diagnostics/Cargo.toml +++ b/crates/diagnostics/Cargo.toml @@ -20,7 +20,9 @@ theme = { path = "../theme" } util = { path = "../util" } workspace = { path = "../workspace" } +log.workspace = true anyhow.workspace = true +futures.workspace = true schemars.workspace = true serde.workspace = true serde_derive.workspace = true diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 0b1c6f8470..e794771434 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -2,8 +2,8 @@ pub mod items; mod project_diagnostics_settings; mod toolbar_controls; -use anyhow::Result; -use collections::{BTreeSet, HashSet}; +use anyhow::{Context, Result}; +use collections::{HashMap, HashSet}; use editor::{ diagnostic_block_renderer, display_map::{BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock}, @@ -11,9 +11,10 @@ use editor::{ scroll::autoscroll::Autoscroll, Editor, ExcerptId, ExcerptRange, MultiBuffer, ToOffset, }; +use futures::future::try_join_all; use gpui::{ actions, elements::*, fonts::TextStyle, serde_json, AnyViewHandle, AppContext, Entity, - ModelHandle, Task, View, ViewContext, ViewHandle, WeakViewHandle, + ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, }; use language::{ Anchor, Bias, Buffer, Diagnostic, DiagnosticEntry, DiagnosticSeverity, Point, Selection, @@ -28,6 +29,7 @@ use std::{ any::{Any, TypeId}, borrow::Cow, cmp::Ordering, + mem, ops::Range, path::PathBuf, sync::Arc, @@ -60,8 +62,10 @@ struct ProjectDiagnosticsEditor { summary: DiagnosticSummary, excerpts: ModelHandle, path_states: Vec, - paths_to_update: BTreeSet<(ProjectPath, LanguageServerId)>, + paths_to_update: HashMap>, + current_diagnostics: HashMap>, include_warnings: bool, + _subscriptions: Vec, } struct PathState { @@ -125,9 +129,12 @@ impl View for ProjectDiagnosticsEditor { "summary": project.diagnostic_summary(cx), }), "summary": self.summary, - "paths_to_update": self.paths_to_update.iter().map(|(path, server_id)| - (path.path.to_string_lossy(), server_id.0) - ).collect::>(), + "paths_to_update": self.paths_to_update.iter().map(|(server_id, paths)| + (server_id.0, paths.into_iter().map(|path| path.path.to_string_lossy()).collect::>()) + ).collect::>(), + "current_diagnostics": self.current_diagnostics.iter().map(|(server_id, paths)| + (server_id.0, paths.into_iter().map(|path| path.path.to_string_lossy()).collect::>()) + ).collect::>(), "paths_states": self.path_states.iter().map(|state| json!({ "path": state.path.path.to_string_lossy(), @@ -149,21 +156,30 @@ impl ProjectDiagnosticsEditor { workspace: WeakViewHandle, cx: &mut ViewContext, ) -> Self { - cx.subscribe(&project_handle, |this, _, event, cx| match event { - project::Event::DiskBasedDiagnosticsFinished { language_server_id } => { - this.update_excerpts(Some(*language_server_id), cx); - this.update_title(cx); - } - project::Event::DiagnosticsUpdated { - language_server_id, - path, - } => { - this.paths_to_update - .insert((path.clone(), *language_server_id)); - } - _ => {} - }) - .detach(); + let project_event_subscription = + cx.subscribe(&project_handle, |this, _, event, cx| match event { + project::Event::DiskBasedDiagnosticsFinished { language_server_id } => { + log::debug!("Disk based diagnostics finished for server {language_server_id}"); + this.update_excerpts(Some(*language_server_id), cx); + } + project::Event::DiagnosticsUpdated { + language_server_id, + path, + } => { + log::debug!("Adding path {path:?} to update for server {language_server_id}"); + this.paths_to_update + .entry(*language_server_id) + .or_default() + .insert(path.clone()); + let no_multiselections = this.editor.update(cx, |editor, cx| { + editor.selections.all::(cx).len() <= 1 + }); + if no_multiselections && !this.is_dirty(cx) { + this.update_excerpts(Some(*language_server_id), cx); + } + } + _ => {} + }); let excerpts = cx.add_model(|cx| MultiBuffer::new(project_handle.read(cx).replica_id())); let editor = cx.add_view(|cx| { @@ -172,19 +188,14 @@ impl ProjectDiagnosticsEditor { editor.set_vertical_scroll_margin(5, cx); editor }); - cx.subscribe(&editor, |this, _, event, cx| { + let editor_event_subscription = cx.subscribe(&editor, |this, _, event, cx| { cx.emit(event.clone()); if event == &editor::Event::Focused && this.path_states.is_empty() { cx.focus_self() } - }) - .detach(); + }); let project = project_handle.read(cx); - let paths_to_update = project - .diagnostic_summaries(cx) - .map(|(path, server_id, _)| (path, server_id)) - .collect(); let summary = project.diagnostic_summary(cx); let mut this = Self { project: project_handle, @@ -193,8 +204,10 @@ impl ProjectDiagnosticsEditor { excerpts, editor, path_states: Default::default(), - paths_to_update, + paths_to_update: HashMap::default(), include_warnings: settings::get::(cx).include_warnings, + current_diagnostics: HashMap::default(), + _subscriptions: vec![project_event_subscription, editor_event_subscription], }; this.update_excerpts(None, cx); this @@ -214,12 +227,7 @@ impl ProjectDiagnosticsEditor { fn toggle_warnings(&mut self, _: &ToggleWarnings, cx: &mut ViewContext) { self.include_warnings = !self.include_warnings; - self.paths_to_update = self - .project - .read(cx) - .diagnostic_summaries(cx) - .map(|(path, server_id, _)| (path, server_id)) - .collect(); + self.paths_to_update = self.current_diagnostics.clone(); self.update_excerpts(None, cx); cx.notify(); } @@ -229,29 +237,94 @@ impl ProjectDiagnosticsEditor { language_server_id: Option, cx: &mut ViewContext, ) { - let mut paths = Vec::new(); - self.paths_to_update.retain(|(path, server_id)| { - if language_server_id - .map_or(true, |language_server_id| language_server_id == *server_id) - { - paths.push(path.clone()); - false + log::debug!("Updating excerpts for server {language_server_id:?}"); + let mut paths_to_recheck = HashSet::default(); + let mut new_summaries: HashMap> = self + .project + .read(cx) + .diagnostic_summaries(cx) + .fold(HashMap::default(), |mut summaries, (path, server_id, _)| { + summaries.entry(server_id).or_default().insert(path); + summaries + }); + let mut old_diagnostics = if let Some(language_server_id) = language_server_id { + new_summaries.retain(|server_id, _| server_id == &language_server_id); + self.paths_to_update.retain(|server_id, paths| { + if server_id == &language_server_id { + paths_to_recheck.extend(paths.drain()); + false + } else { + true + } + }); + let mut old_diagnostics = HashMap::default(); + if let Some(new_paths) = new_summaries.get(&language_server_id) { + if let Some(old_paths) = self + .current_diagnostics + .insert(language_server_id, new_paths.clone()) + { + old_diagnostics.insert(language_server_id, old_paths); + } } else { - true + if let Some(old_paths) = self.current_diagnostics.remove(&language_server_id) { + old_diagnostics.insert(language_server_id, old_paths); + } } - }); + old_diagnostics + } else { + paths_to_recheck.extend(self.paths_to_update.drain().flat_map(|(_, paths)| paths)); + mem::replace(&mut self.current_diagnostics, new_summaries.clone()) + }; + for (server_id, new_paths) in new_summaries { + match old_diagnostics.remove(&server_id) { + Some(mut old_paths) => { + paths_to_recheck.extend( + new_paths + .into_iter() + .filter(|new_path| !old_paths.remove(new_path)), + ); + paths_to_recheck.extend(old_paths); + } + None => paths_to_recheck.extend(new_paths), + } + } + paths_to_recheck.extend(old_diagnostics.into_iter().flat_map(|(_, paths)| paths)); + + if paths_to_recheck.is_empty() { + log::debug!("No paths to recheck for language server {language_server_id:?}"); + return; + } + log::debug!( + "Rechecking {} paths for language server {:?}", + paths_to_recheck.len(), + language_server_id + ); let project = self.project.clone(); cx.spawn(|this, mut cx| { async move { - for path in paths { - let buffer = project - .update(&mut cx, |project, cx| project.open_buffer(path.clone(), cx)) - .await?; - this.update(&mut cx, |this, cx| { - this.populate_excerpts(path, language_server_id, buffer, cx) - })?; - } - Result::<_, anyhow::Error>::Ok(()) + let _: Vec<()> = try_join_all(paths_to_recheck.into_iter().map(|path| { + let mut cx = cx.clone(); + let project = project.clone(); + async move { + let buffer = project + .update(&mut cx, |project, cx| project.open_buffer(path.clone(), cx)) + .await + .with_context(|| format!("opening buffer for path {path:?}"))?; + this.update(&mut cx, |this, cx| { + this.populate_excerpts(path, language_server_id, buffer, cx); + }) + .context("missing project")?; + anyhow::Ok(()) + } + })) + .await + .context("rechecking diagnostics for paths")?; + + this.update(&mut cx, |this, cx| { + this.summary = this.project.read(cx).diagnostic_summary(cx); + cx.emit(Event::TitleChanged); + })?; + anyhow::Ok(()) } .log_err() }) @@ -554,11 +627,6 @@ impl ProjectDiagnosticsEditor { } cx.notify(); } - - fn update_title(&mut self, cx: &mut ViewContext) { - self.summary = self.project.read(cx).diagnostic_summary(cx); - cx.emit(Event::TitleChanged); - } } impl Item for ProjectDiagnosticsEditor { @@ -1301,25 +1369,6 @@ mod tests { cx, ) .unwrap(); - project - .update_diagnostic_entries( - server_id_2, - PathBuf::from("/test/main.js"), - None, - vec![DiagnosticEntry { - range: Unclipped(PointUtf16::new(1, 0))..Unclipped(PointUtf16::new(1, 1)), - diagnostic: Diagnostic { - message: "warning 1".to_string(), - severity: DiagnosticSeverity::ERROR, - is_primary: true, - is_disk_based: true, - group_id: 2, - ..Default::default() - }, - }], - cx, - ) - .unwrap(); }); // The first language server finishes @@ -1353,6 +1402,25 @@ mod tests { // The second language server finishes project.update(cx, |project, cx| { + project + .update_diagnostic_entries( + server_id_2, + PathBuf::from("/test/main.js"), + None, + vec![DiagnosticEntry { + range: Unclipped(PointUtf16::new(1, 0))..Unclipped(PointUtf16::new(1, 1)), + diagnostic: Diagnostic { + message: "warning 1".to_string(), + severity: DiagnosticSeverity::ERROR, + is_primary: true, + is_disk_based: true, + group_id: 2, + ..Default::default() + }, + }], + cx, + ) + .unwrap(); project.disk_based_diagnostics_finished(server_id_2, cx); }); diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index c87606070e..1b922848e0 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -33,9 +33,9 @@ use util::{ paths::{PathExt, FILE_ROW_COLUMN_DELIMITER}, ResultExt, TryFutureExt, }; -use workspace::item::{BreadcrumbText, FollowableItemHandle}; +use workspace::item::{BreadcrumbText, FollowableItemHandle, ItemHandle}; use workspace::{ - item::{FollowableItem, Item, ItemEvent, ItemHandle, ProjectItem}, + item::{FollowableItem, Item, ItemEvent, ProjectItem}, searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle}, ItemId, ItemNavHistory, Pane, StatusItemView, ToolbarItemLocation, ViewId, Workspace, WorkspaceId, diff --git a/crates/gpui2/src/app.rs b/crates/gpui2/src/app.rs index bc9101fa0c..9afffeb685 100644 --- a/crates/gpui2/src/app.rs +++ b/crates/gpui2/src/app.rs @@ -46,13 +46,17 @@ pub struct AppCell { } impl AppCell { + #[track_caller] pub fn borrow(&self) -> AppRef { + let thread_id = std::thread::current().id(); + eprintln!("borrowed {thread_id:?}"); AppRef(self.app.borrow()) } + #[track_caller] pub fn borrow_mut(&self) -> AppRefMut { - // let thread_id = std::thread::current().id(); - // dbg!("borrowed {thread_id:?}"); + let thread_id = std::thread::current().id(); + eprintln!("borrowed {thread_id:?}"); AppRefMut(self.app.borrow_mut()) } } diff --git a/crates/gpui2/src/app/test_context.rs b/crates/gpui2/src/app/test_context.rs index e731dccc6e..856fce75b4 100644 --- a/crates/gpui2/src/app/test_context.rs +++ b/crates/gpui2/src/app/test_context.rs @@ -189,3 +189,22 @@ impl TestAppContext { .unwrap(); } } + +impl Model { + pub fn next_event(&self, cx: &mut TestAppContext) -> T::Event + where + T::Event: Send + Clone, + { + let (tx, mut rx) = futures::channel::mpsc::unbounded(); + let _subscription = self.update(cx, |_, cx| { + cx.subscribe(self, move |_, _, event, _| { + tx.unbounded_send(event.clone()).ok(); + }) + }); + + cx.executor().run_until_parked(); + rx.try_next() + .expect("no event received") + .expect("model was dropped") + } +} diff --git a/crates/gpui2_macros/src/test.rs b/crates/gpui2_macros/src/test.rs index acaaee597b..05d2c1f63a 100644 --- a/crates/gpui2_macros/src/test.rs +++ b/crates/gpui2_macros/src/test.rs @@ -175,6 +175,7 @@ pub fn test(args: TokenStream, function: TokenStream) -> TokenStream { inner_fn_args.extend(quote!(&mut #cx_varname_lock,)); cx_teardowns.extend(quote!( #cx_varname_lock.quit(); + drop(#cx_varname_lock); dispatcher.run_until_parked(); )); continue; diff --git a/crates/gpui_macros/src/gpui_macros.rs b/crates/gpui_macros/src/gpui_macros.rs index aa55c27eaa..62fba3b612 100644 --- a/crates/gpui_macros/src/gpui_macros.rs +++ b/crates/gpui_macros/src/gpui_macros.rs @@ -170,6 +170,8 @@ pub fn test(args: TokenStream, function: TokenStream) -> TokenStream { #max_retries, #detect_nondeterminism, &mut |cx, foreground_platform, deterministic, seed| { + // some of the macro contents do not use all variables, silence the warnings + let _ = (&cx, &foreground_platform, &deterministic, &seed); #cx_vars cx.foreground().run(#inner_fn_name(#inner_fn_args)); #cx_teardowns @@ -247,6 +249,8 @@ pub fn test(args: TokenStream, function: TokenStream) -> TokenStream { #max_retries, #detect_nondeterminism, &mut |cx, foreground_platform, deterministic, seed| { + // some of the macro contents do not use all variables, silence the warnings + let _ = (&cx, &foreground_platform, &deterministic, &seed); #cx_vars #inner_fn_name(#inner_fn_args); #cx_teardowns diff --git a/crates/refineable/derive_refineable/src/derive_refineable.rs b/crates/refineable/derive_refineable/src/derive_refineable.rs index ed233e1b0d..c2d001b287 100644 --- a/crates/refineable/derive_refineable/src/derive_refineable.rs +++ b/crates/refineable/derive_refineable/src/derive_refineable.rs @@ -16,9 +16,33 @@ pub fn derive_refineable(input: TokenStream) -> TokenStream { .. } = parse_macro_input!(input); - let impl_debug_on_refinement = attrs - .iter() - .any(|attr| attr.path.is_ident("refineable") && attr.tokens.to_string().contains("debug")); + let refineable_attr = attrs.iter().find(|attr| attr.path.is_ident("refineable")); + + let mut impl_debug_on_refinement = false; + let mut derive_serialize_on_refinement = false; + let mut derive_deserialize_on_refinement = false; + + if let Some(refineable_attr) = refineable_attr { + if let Ok(syn::Meta::List(meta_list)) = refineable_attr.parse_meta() { + for nested in meta_list.nested { + let syn::NestedMeta::Meta(syn::Meta::Path(path)) = nested else { + continue; + }; + + if path.is_ident("debug") { + impl_debug_on_refinement = true; + } + + if path.is_ident("serialize") { + derive_serialize_on_refinement = true; + } + + if path.is_ident("deserialize") { + derive_deserialize_on_refinement = true; + } + } + } + } let refinement_ident = format_ident!("{}Refinement", ident); let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); @@ -235,8 +259,22 @@ pub fn derive_refineable(input: TokenStream) -> TokenStream { quote! {} }; + let derive_serialize = if derive_serialize_on_refinement { + quote! { #[derive(serde::Serialize)]} + } else { + quote! {} + }; + + let derive_deserialize = if derive_deserialize_on_refinement { + quote! { #[derive(serde::Deserialize)]} + } else { + quote! {} + }; + let gen = quote! { #[derive(Clone)] + #derive_serialize + #derive_deserialize pub struct #refinement_ident #impl_generics { #( #field_visibilities #field_names: #wrapped_types ),* } diff --git a/crates/rpc2/proto/zed.proto b/crates/rpc2/proto/zed.proto index 3501e70e6a..206777879b 100644 --- a/crates/rpc2/proto/zed.proto +++ b/crates/rpc2/proto/zed.proto @@ -89,88 +89,96 @@ message Envelope { FormatBuffersResponse format_buffers_response = 70; GetCompletions get_completions = 71; GetCompletionsResponse get_completions_response = 72; - ApplyCompletionAdditionalEdits apply_completion_additional_edits = 73; - ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 74; - GetCodeActions get_code_actions = 75; - GetCodeActionsResponse get_code_actions_response = 76; - GetHover get_hover = 77; - GetHoverResponse get_hover_response = 78; - ApplyCodeAction apply_code_action = 79; - ApplyCodeActionResponse apply_code_action_response = 80; - PrepareRename prepare_rename = 81; - PrepareRenameResponse prepare_rename_response = 82; - PerformRename perform_rename = 83; - PerformRenameResponse perform_rename_response = 84; - SearchProject search_project = 85; - SearchProjectResponse search_project_response = 86; + ResolveCompletionDocumentation resolve_completion_documentation = 73; + ResolveCompletionDocumentationResponse resolve_completion_documentation_response = 74; + ApplyCompletionAdditionalEdits apply_completion_additional_edits = 75; + ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 76; + GetCodeActions get_code_actions = 77; + GetCodeActionsResponse get_code_actions_response = 78; + GetHover get_hover = 79; + GetHoverResponse get_hover_response = 80; + ApplyCodeAction apply_code_action = 81; + ApplyCodeActionResponse apply_code_action_response = 82; + PrepareRename prepare_rename = 83; + PrepareRenameResponse prepare_rename_response = 84; + PerformRename perform_rename = 85; + PerformRenameResponse perform_rename_response = 86; + SearchProject search_project = 87; + SearchProjectResponse search_project_response = 88; - UpdateContacts update_contacts = 87; - UpdateInviteInfo update_invite_info = 88; - ShowContacts show_contacts = 89; + UpdateContacts update_contacts = 89; + UpdateInviteInfo update_invite_info = 90; + ShowContacts show_contacts = 91; - GetUsers get_users = 90; - FuzzySearchUsers fuzzy_search_users = 91; - UsersResponse users_response = 92; - RequestContact request_contact = 93; - RespondToContactRequest respond_to_contact_request = 94; - RemoveContact remove_contact = 95; + GetUsers get_users = 92; + FuzzySearchUsers fuzzy_search_users = 93; + UsersResponse users_response = 94; + RequestContact request_contact = 95; + RespondToContactRequest respond_to_contact_request = 96; + RemoveContact remove_contact = 97; - Follow follow = 96; - FollowResponse follow_response = 97; - UpdateFollowers update_followers = 98; - Unfollow unfollow = 99; - GetPrivateUserInfo get_private_user_info = 100; - GetPrivateUserInfoResponse get_private_user_info_response = 101; - UpdateDiffBase update_diff_base = 102; + Follow follow = 98; + FollowResponse follow_response = 99; + UpdateFollowers update_followers = 100; + Unfollow unfollow = 101; + GetPrivateUserInfo get_private_user_info = 102; + GetPrivateUserInfoResponse get_private_user_info_response = 103; + UpdateDiffBase update_diff_base = 104; - OnTypeFormatting on_type_formatting = 103; - OnTypeFormattingResponse on_type_formatting_response = 104; + OnTypeFormatting on_type_formatting = 105; + OnTypeFormattingResponse on_type_formatting_response = 106; - UpdateWorktreeSettings update_worktree_settings = 105; + UpdateWorktreeSettings update_worktree_settings = 107; - InlayHints inlay_hints = 106; - InlayHintsResponse inlay_hints_response = 107; - ResolveInlayHint resolve_inlay_hint = 108; - ResolveInlayHintResponse resolve_inlay_hint_response = 109; - RefreshInlayHints refresh_inlay_hints = 110; + InlayHints inlay_hints = 108; + InlayHintsResponse inlay_hints_response = 109; + ResolveInlayHint resolve_inlay_hint = 110; + ResolveInlayHintResponse resolve_inlay_hint_response = 111; + RefreshInlayHints refresh_inlay_hints = 112; - CreateChannel create_channel = 111; - CreateChannelResponse create_channel_response = 112; - InviteChannelMember invite_channel_member = 113; - RemoveChannelMember remove_channel_member = 114; - RespondToChannelInvite respond_to_channel_invite = 115; - UpdateChannels update_channels = 116; - JoinChannel join_channel = 117; - DeleteChannel delete_channel = 118; - GetChannelMembers get_channel_members = 119; - GetChannelMembersResponse get_channel_members_response = 120; - SetChannelMemberAdmin set_channel_member_admin = 121; - RenameChannel rename_channel = 122; - RenameChannelResponse rename_channel_response = 123; + CreateChannel create_channel = 113; + CreateChannelResponse create_channel_response = 114; + InviteChannelMember invite_channel_member = 115; + RemoveChannelMember remove_channel_member = 116; + RespondToChannelInvite respond_to_channel_invite = 117; + UpdateChannels update_channels = 118; + JoinChannel join_channel = 119; + DeleteChannel delete_channel = 120; + GetChannelMembers get_channel_members = 121; + GetChannelMembersResponse get_channel_members_response = 122; + SetChannelMemberRole set_channel_member_role = 123; + RenameChannel rename_channel = 124; + RenameChannelResponse rename_channel_response = 125; - JoinChannelBuffer join_channel_buffer = 124; - JoinChannelBufferResponse join_channel_buffer_response = 125; - UpdateChannelBuffer update_channel_buffer = 126; - LeaveChannelBuffer leave_channel_buffer = 127; - UpdateChannelBufferCollaborators update_channel_buffer_collaborators = 128; - RejoinChannelBuffers rejoin_channel_buffers = 129; - RejoinChannelBuffersResponse rejoin_channel_buffers_response = 130; - AckBufferOperation ack_buffer_operation = 143; + JoinChannelBuffer join_channel_buffer = 126; + JoinChannelBufferResponse join_channel_buffer_response = 127; + UpdateChannelBuffer update_channel_buffer = 128; + LeaveChannelBuffer leave_channel_buffer = 129; + UpdateChannelBufferCollaborators update_channel_buffer_collaborators = 130; + RejoinChannelBuffers rejoin_channel_buffers = 131; + RejoinChannelBuffersResponse rejoin_channel_buffers_response = 132; + AckBufferOperation ack_buffer_operation = 133; - JoinChannelChat join_channel_chat = 131; - JoinChannelChatResponse join_channel_chat_response = 132; - LeaveChannelChat leave_channel_chat = 133; - SendChannelMessage send_channel_message = 134; - SendChannelMessageResponse send_channel_message_response = 135; - ChannelMessageSent channel_message_sent = 136; - GetChannelMessages get_channel_messages = 137; - GetChannelMessagesResponse get_channel_messages_response = 138; - RemoveChannelMessage remove_channel_message = 139; - AckChannelMessage ack_channel_message = 144; + JoinChannelChat join_channel_chat = 134; + JoinChannelChatResponse join_channel_chat_response = 135; + LeaveChannelChat leave_channel_chat = 136; + SendChannelMessage send_channel_message = 137; + SendChannelMessageResponse send_channel_message_response = 138; + ChannelMessageSent channel_message_sent = 139; + GetChannelMessages get_channel_messages = 140; + GetChannelMessagesResponse get_channel_messages_response = 141; + RemoveChannelMessage remove_channel_message = 142; + AckChannelMessage ack_channel_message = 143; + GetChannelMessagesById get_channel_messages_by_id = 144; - LinkChannel link_channel = 140; - UnlinkChannel unlink_channel = 141; - MoveChannel move_channel = 142; // current max: 144 + MoveChannel move_channel = 147; + SetChannelVisibility set_channel_visibility = 148; + + AddNotification add_notification = 149; + GetNotifications get_notifications = 150; + GetNotificationsResponse get_notifications_response = 151; + DeleteNotification delete_notification = 152; + MarkNotificationRead mark_notification_read = 153; // Current max } } @@ -332,6 +340,7 @@ message RoomUpdated { message LiveKitConnectionInfo { string server_url = 1; string token = 2; + bool can_publish = 3; } message ShareProject { @@ -832,6 +841,17 @@ message ResolveState { } } +message ResolveCompletionDocumentation { + uint64 project_id = 1; + uint64 language_server_id = 2; + bytes lsp_completion = 3; +} + +message ResolveCompletionDocumentationResponse { + string text = 1; + bool is_markdown = 2; +} + message ResolveInlayHint { uint64 project_id = 1; uint64 buffer_id = 2; @@ -950,13 +970,10 @@ message LspDiskBasedDiagnosticsUpdated {} message UpdateChannels { repeated Channel channels = 1; - repeated ChannelEdge insert_edge = 2; - repeated ChannelEdge delete_edge = 3; repeated uint64 delete_channels = 4; repeated Channel channel_invitations = 5; repeated uint64 remove_channel_invitations = 6; repeated ChannelParticipants channel_participants = 7; - repeated ChannelPermission channel_permissions = 8; repeated UnseenChannelMessage unseen_channel_messages = 9; repeated UnseenChannelBufferChange unseen_channel_buffer_changes = 10; } @@ -972,14 +989,9 @@ message UnseenChannelBufferChange { repeated VectorClockEntry version = 3; } -message ChannelEdge { - uint64 channel_id = 1; - uint64 parent_id = 2; -} - message ChannelPermission { uint64 channel_id = 1; - bool is_admin = 2; + ChannelRole role = 3; } message ChannelParticipants { @@ -1005,8 +1017,8 @@ message GetChannelMembersResponse { message ChannelMember { uint64 user_id = 1; - bool admin = 2; Kind kind = 3; + ChannelRole role = 4; enum Kind { Member = 0; @@ -1028,7 +1040,7 @@ message CreateChannelResponse { message InviteChannelMember { uint64 channel_id = 1; uint64 user_id = 2; - bool admin = 3; + ChannelRole role = 4; } message RemoveChannelMember { @@ -1036,10 +1048,22 @@ message RemoveChannelMember { uint64 user_id = 2; } -message SetChannelMemberAdmin { +enum ChannelRole { + Admin = 0; + Member = 1; + Guest = 2; + Banned = 3; +} + +message SetChannelMemberRole { uint64 channel_id = 1; uint64 user_id = 2; - bool admin = 3; + ChannelRole role = 3; +} + +message SetChannelVisibility { + uint64 channel_id = 1; + ChannelVisibility visibility = 2; } message RenameChannel { @@ -1068,6 +1092,7 @@ message SendChannelMessage { uint64 channel_id = 1; string body = 2; Nonce nonce = 3; + repeated ChatMention mentions = 4; } message RemoveChannelMessage { @@ -1099,20 +1124,13 @@ message GetChannelMessagesResponse { bool done = 2; } -message LinkChannel { - uint64 channel_id = 1; - uint64 to = 2; -} - -message UnlinkChannel { - uint64 channel_id = 1; - uint64 from = 2; +message GetChannelMessagesById { + repeated uint64 message_ids = 1; } message MoveChannel { uint64 channel_id = 1; - uint64 from = 2; - uint64 to = 3; + optional uint64 to = 2; } message JoinChannelBuffer { @@ -1125,6 +1143,12 @@ message ChannelMessage { uint64 timestamp = 3; uint64 sender_id = 4; Nonce nonce = 5; + repeated ChatMention mentions = 6; +} + +message ChatMention { + Range range = 1; + uint64 user_id = 2; } message RejoinChannelBuffers { @@ -1216,7 +1240,6 @@ message ShowContacts {} message IncomingContactRequest { uint64 requester_id = 1; - bool should_notify = 2; } message UpdateDiagnostics { @@ -1533,16 +1556,23 @@ message Nonce { uint64 lower_half = 2; } +enum ChannelVisibility { + Public = 0; + Members = 1; +} + message Channel { uint64 id = 1; string name = 2; + ChannelVisibility visibility = 3; + ChannelRole role = 4; + repeated uint64 parent_path = 5; } message Contact { uint64 user_id = 1; bool online = 2; bool busy = 3; - bool should_notify = 4; } message WorktreeMetadata { @@ -1557,3 +1587,34 @@ message UpdateDiffBase { uint64 buffer_id = 2; optional string diff_base = 3; } + +message GetNotifications { + optional uint64 before_id = 1; +} + +message AddNotification { + Notification notification = 1; +} + +message GetNotificationsResponse { + repeated Notification notifications = 1; + bool done = 2; +} + +message DeleteNotification { + uint64 notification_id = 1; +} + +message MarkNotificationRead { + uint64 notification_id = 1; +} + +message Notification { + uint64 id = 1; + uint64 timestamp = 2; + string kind = 3; + optional uint64 entity_id = 4; + string content = 5; + bool is_read = 6; + optional bool response = 7; +} diff --git a/crates/rpc2/src/proto.rs b/crates/rpc2/src/proto.rs index f0d7937f6f..77a69122c2 100644 --- a/crates/rpc2/src/proto.rs +++ b/crates/rpc2/src/proto.rs @@ -133,6 +133,9 @@ impl fmt::Display for PeerId { messages!( (Ack, Foreground), + (AckBufferOperation, Background), + (AckChannelMessage, Background), + (AddNotification, Foreground), (AddProjectCollaborator, Foreground), (ApplyCodeAction, Background), (ApplyCodeActionResponse, Background), @@ -143,57 +146,74 @@ messages!( (Call, Foreground), (CallCanceled, Foreground), (CancelCall, Foreground), + (ChannelMessageSent, Foreground), (CopyProjectEntry, Foreground), (CreateBufferForPeer, Foreground), (CreateChannel, Foreground), (CreateChannelResponse, Foreground), - (ChannelMessageSent, Foreground), (CreateProjectEntry, Foreground), (CreateRoom, Foreground), (CreateRoomResponse, Foreground), (DeclineCall, Foreground), + (DeleteChannel, Foreground), + (DeleteNotification, Foreground), (DeleteProjectEntry, Foreground), (Error, Foreground), (ExpandProjectEntry, Foreground), + (ExpandProjectEntryResponse, Foreground), (Follow, Foreground), (FollowResponse, Foreground), (FormatBuffers, Foreground), (FormatBuffersResponse, Foreground), (FuzzySearchUsers, Foreground), + (GetChannelMembers, Foreground), + (GetChannelMembersResponse, Foreground), + (GetChannelMessages, Background), + (GetChannelMessagesById, Background), + (GetChannelMessagesResponse, Background), (GetCodeActions, Background), (GetCodeActionsResponse, Background), - (GetHover, Background), - (GetHoverResponse, Background), - (GetChannelMessages, Background), - (GetChannelMessagesResponse, Background), - (SendChannelMessage, Background), - (SendChannelMessageResponse, Background), (GetCompletions, Background), (GetCompletionsResponse, Background), (GetDefinition, Background), (GetDefinitionResponse, Background), - (GetTypeDefinition, Background), - (GetTypeDefinitionResponse, Background), (GetDocumentHighlights, Background), (GetDocumentHighlightsResponse, Background), - (GetReferences, Background), - (GetReferencesResponse, Background), + (GetHover, Background), + (GetHoverResponse, Background), + (GetNotifications, Foreground), + (GetNotificationsResponse, Foreground), + (GetPrivateUserInfo, Foreground), + (GetPrivateUserInfoResponse, Foreground), (GetProjectSymbols, Background), (GetProjectSymbolsResponse, Background), + (GetReferences, Background), + (GetReferencesResponse, Background), + (GetTypeDefinition, Background), + (GetTypeDefinitionResponse, Background), (GetUsers, Foreground), (Hello, Foreground), (IncomingCall, Foreground), + (InlayHints, Background), + (InlayHintsResponse, Background), (InviteChannelMember, Foreground), - (UsersResponse, Foreground), + (JoinChannel, Foreground), + (JoinChannelBuffer, Foreground), + (JoinChannelBufferResponse, Foreground), + (JoinChannelChat, Foreground), + (JoinChannelChatResponse, Foreground), (JoinProject, Foreground), (JoinProjectResponse, Foreground), (JoinRoom, Foreground), (JoinRoomResponse, Foreground), - (JoinChannelChat, Foreground), - (JoinChannelChatResponse, Foreground), + (LeaveChannelBuffer, Background), (LeaveChannelChat, Foreground), (LeaveProject, Foreground), (LeaveRoom, Foreground), + (MarkNotificationRead, Foreground), + (MoveChannel, Foreground), + (OnTypeFormatting, Background), + (OnTypeFormattingResponse, Background), (OpenBufferById, Background), (OpenBufferByPath, Background), (OpenBufferForSymbol, Background), @@ -201,58 +221,56 @@ messages!( (OpenBufferResponse, Background), (PerformRename, Background), (PerformRenameResponse, Background), - (OnTypeFormatting, Background), - (OnTypeFormattingResponse, Background), - (InlayHints, Background), - (InlayHintsResponse, Background), - (ResolveInlayHint, Background), - (ResolveInlayHintResponse, Background), - (RefreshInlayHints, Foreground), (Ping, Foreground), (PrepareRename, Background), (PrepareRenameResponse, Background), - (ExpandProjectEntryResponse, Foreground), (ProjectEntryResponse, Foreground), + (RefreshInlayHints, Foreground), + (RejoinChannelBuffers, Foreground), + (RejoinChannelBuffersResponse, Foreground), (RejoinRoom, Foreground), (RejoinRoomResponse, Foreground), - (RemoveContact, Foreground), - (RemoveChannelMember, Foreground), - (RemoveChannelMessage, Foreground), (ReloadBuffers, Foreground), (ReloadBuffersResponse, Foreground), + (RemoveChannelMember, Foreground), + (RemoveChannelMessage, Foreground), + (RemoveContact, Foreground), (RemoveProjectCollaborator, Foreground), - (RenameProjectEntry, Foreground), - (RequestContact, Foreground), - (RespondToContactRequest, Foreground), - (RespondToChannelInvite, Foreground), - (JoinChannel, Foreground), - (RoomUpdated, Foreground), - (SaveBuffer, Foreground), (RenameChannel, Foreground), (RenameChannelResponse, Foreground), - (SetChannelMemberAdmin, Foreground), + (RenameProjectEntry, Foreground), + (RequestContact, Foreground), + (ResolveCompletionDocumentation, Background), + (ResolveCompletionDocumentationResponse, Background), + (ResolveInlayHint, Background), + (ResolveInlayHintResponse, Background), + (RespondToChannelInvite, Foreground), + (RespondToContactRequest, Foreground), + (RoomUpdated, Foreground), + (SaveBuffer, Foreground), + (SetChannelMemberRole, Foreground), + (SetChannelVisibility, Foreground), (SearchProject, Background), (SearchProjectResponse, Background), + (SendChannelMessage, Background), + (SendChannelMessageResponse, Background), (ShareProject, Foreground), (ShareProjectResponse, Foreground), (ShowContacts, Foreground), (StartLanguageServer, Foreground), (SynchronizeBuffers, Foreground), (SynchronizeBuffersResponse, Foreground), - (RejoinChannelBuffers, Foreground), - (RejoinChannelBuffersResponse, Foreground), (Test, Foreground), (Unfollow, Foreground), (UnshareProject, Foreground), (UpdateBuffer, Foreground), (UpdateBufferFile, Foreground), - (UpdateContacts, Foreground), - (DeleteChannel, Foreground), - (MoveChannel, Foreground), - (LinkChannel, Foreground), - (UnlinkChannel, Foreground), + (UpdateChannelBuffer, Foreground), + (UpdateChannelBufferCollaborators, Foreground), (UpdateChannels, Foreground), + (UpdateContacts, Foreground), (UpdateDiagnosticSummary, Foreground), + (UpdateDiffBase, Foreground), (UpdateFollowers, Foreground), (UpdateInviteInfo, Foreground), (UpdateLanguageServer, Foreground), @@ -261,18 +279,7 @@ messages!( (UpdateProjectCollaborator, Foreground), (UpdateWorktree, Foreground), (UpdateWorktreeSettings, Foreground), - (UpdateDiffBase, Foreground), - (GetPrivateUserInfo, Foreground), - (GetPrivateUserInfoResponse, Foreground), - (GetChannelMembers, Foreground), - (GetChannelMembersResponse, Foreground), - (JoinChannelBuffer, Foreground), - (JoinChannelBufferResponse, Foreground), - (LeaveChannelBuffer, Background), - (UpdateChannelBuffer, Foreground), - (UpdateChannelBufferCollaborators, Foreground), - (AckBufferOperation, Background), - (AckChannelMessage, Background), + (UsersResponse, Foreground), ); request_messages!( @@ -284,72 +291,78 @@ request_messages!( (Call, Ack), (CancelCall, Ack), (CopyProjectEntry, ProjectEntryResponse), + (CreateChannel, CreateChannelResponse), (CreateProjectEntry, ProjectEntryResponse), (CreateRoom, CreateRoomResponse), - (CreateChannel, CreateChannelResponse), (DeclineCall, Ack), + (DeleteChannel, Ack), (DeleteProjectEntry, ProjectEntryResponse), (ExpandProjectEntry, ExpandProjectEntryResponse), (Follow, FollowResponse), (FormatBuffers, FormatBuffersResponse), + (FuzzySearchUsers, UsersResponse), + (GetChannelMembers, GetChannelMembersResponse), + (GetChannelMessages, GetChannelMessagesResponse), + (GetChannelMessagesById, GetChannelMessagesResponse), (GetCodeActions, GetCodeActionsResponse), - (GetHover, GetHoverResponse), (GetCompletions, GetCompletionsResponse), (GetDefinition, GetDefinitionResponse), - (GetTypeDefinition, GetTypeDefinitionResponse), (GetDocumentHighlights, GetDocumentHighlightsResponse), - (GetReferences, GetReferencesResponse), + (GetHover, GetHoverResponse), + (GetNotifications, GetNotificationsResponse), (GetPrivateUserInfo, GetPrivateUserInfoResponse), (GetProjectSymbols, GetProjectSymbolsResponse), - (FuzzySearchUsers, UsersResponse), + (GetReferences, GetReferencesResponse), + (GetTypeDefinition, GetTypeDefinitionResponse), (GetUsers, UsersResponse), + (IncomingCall, Ack), + (InlayHints, InlayHintsResponse), (InviteChannelMember, Ack), + (JoinChannel, JoinRoomResponse), + (JoinChannelBuffer, JoinChannelBufferResponse), + (JoinChannelChat, JoinChannelChatResponse), (JoinProject, JoinProjectResponse), (JoinRoom, JoinRoomResponse), - (JoinChannelChat, JoinChannelChatResponse), + (LeaveChannelBuffer, Ack), (LeaveRoom, Ack), - (RejoinRoom, RejoinRoomResponse), - (IncomingCall, Ack), + (MarkNotificationRead, Ack), + (MoveChannel, Ack), + (OnTypeFormatting, OnTypeFormattingResponse), (OpenBufferById, OpenBufferResponse), (OpenBufferByPath, OpenBufferResponse), (OpenBufferForSymbol, OpenBufferForSymbolResponse), - (Ping, Ack), (PerformRename, PerformRenameResponse), + (Ping, Ack), (PrepareRename, PrepareRenameResponse), - (OnTypeFormatting, OnTypeFormattingResponse), - (InlayHints, InlayHintsResponse), - (ResolveInlayHint, ResolveInlayHintResponse), (RefreshInlayHints, Ack), + (RejoinChannelBuffers, RejoinChannelBuffersResponse), + (RejoinRoom, RejoinRoomResponse), (ReloadBuffers, ReloadBuffersResponse), - (RequestContact, Ack), (RemoveChannelMember, Ack), - (RemoveContact, Ack), - (RespondToContactRequest, Ack), - (RespondToChannelInvite, Ack), - (SetChannelMemberAdmin, Ack), - (SendChannelMessage, SendChannelMessageResponse), - (GetChannelMessages, GetChannelMessagesResponse), - (GetChannelMembers, GetChannelMembersResponse), - (JoinChannel, JoinRoomResponse), (RemoveChannelMessage, Ack), - (DeleteChannel, Ack), - (RenameProjectEntry, ProjectEntryResponse), + (RemoveContact, Ack), (RenameChannel, RenameChannelResponse), - (LinkChannel, Ack), - (UnlinkChannel, Ack), - (MoveChannel, Ack), + (RenameProjectEntry, ProjectEntryResponse), + (RequestContact, Ack), + ( + ResolveCompletionDocumentation, + ResolveCompletionDocumentationResponse + ), + (ResolveInlayHint, ResolveInlayHintResponse), + (RespondToChannelInvite, Ack), + (RespondToContactRequest, Ack), (SaveBuffer, BufferSaved), (SearchProject, SearchProjectResponse), + (SendChannelMessage, SendChannelMessageResponse), + (SetChannelMemberRole, Ack), + (SetChannelVisibility, Ack), (ShareProject, ShareProjectResponse), (SynchronizeBuffers, SynchronizeBuffersResponse), - (RejoinChannelBuffers, RejoinChannelBuffersResponse), (Test, Test), (UpdateBuffer, Ack), (UpdateParticipantLocation, Ack), (UpdateProject, Ack), (UpdateWorktree, Ack), - (JoinChannelBuffer, JoinChannelBufferResponse), - (LeaveChannelBuffer, Ack) ); entity_messages!( @@ -368,25 +381,26 @@ entity_messages!( GetCodeActions, GetCompletions, GetDefinition, - GetTypeDefinition, GetDocumentHighlights, GetHover, - GetReferences, GetProjectSymbols, + GetReferences, + GetTypeDefinition, + InlayHints, JoinProject, LeaveProject, + OnTypeFormatting, OpenBufferById, OpenBufferByPath, OpenBufferForSymbol, PerformRename, - OnTypeFormatting, - InlayHints, - ResolveInlayHint, - RefreshInlayHints, PrepareRename, + RefreshInlayHints, ReloadBuffers, RemoveProjectCollaborator, RenameProjectEntry, + ResolveCompletionDocumentation, + ResolveInlayHint, SaveBuffer, SearchProject, StartLanguageServer, @@ -395,19 +409,19 @@ entity_messages!( UpdateBuffer, UpdateBufferFile, UpdateDiagnosticSummary, + UpdateDiffBase, UpdateLanguageServer, UpdateProject, UpdateProjectCollaborator, UpdateWorktree, UpdateWorktreeSettings, - UpdateDiffBase ); entity_messages!( channel_id, ChannelMessageSent, - UpdateChannelBuffer, RemoveChannelMessage, + UpdateChannelBuffer, UpdateChannelBufferCollaborators, ); diff --git a/crates/storybook2/src/components.rs b/crates/storybook2/src/components.rs deleted file mode 100644 index a3dca51adc..0000000000 --- a/crates/storybook2/src/components.rs +++ /dev/null @@ -1,97 +0,0 @@ -use gpui2::{ - div, ArcCow, Element, EventContext, Interactive, IntoElement, MouseButton, ParentElement, - StyleHelpers, ViewContext, -}; -use std::{marker::PhantomData, rc::Rc}; - -struct ButtonHandlers { - click: Option)>>, -} - -impl Default for ButtonHandlers { - fn default() -> Self { - Self { click: None } - } -} - -#[derive(Component)] -pub struct Button { - handlers: ButtonHandlers, - label: Option>, - icon: Option>, - data: Rc, - view_type: PhantomData, -} - -// Impl block for buttons without data. -// See below for an impl block for any button. -impl Button { - fn new() -> Self { - Self { - handlers: ButtonHandlers::default(), - label: None, - icon: None, - data: Rc::new(()), - view_type: PhantomData, - } - } - - pub fn data(self, data: D) -> Button { - Button { - handlers: ButtonHandlers::default(), - label: self.label, - icon: self.icon, - data: Rc::new(data), - view_type: PhantomData, - } - } -} - -// Impl block for button regardless of its data type. -impl Button { - pub fn label(mut self, label: impl Into>) -> Self { - self.label = Some(label.into()); - self - } - - pub fn icon(mut self, icon: impl Into>) -> Self { - self.icon = Some(icon.into()); - self - } - - pub fn on_click( - mut self, - handler: impl Fn(&mut V, &D, &mut EventContext) + 'static, - ) -> Self { - self.handlers.click = Some(Rc::new(handler)); - self - } -} - -pub fn button() -> Button { - Button::new() -} - -impl Button { - fn render( - &mut self, - view: &mut V, - cx: &mut ViewContext, - ) -> impl IntoElement + Interactive { - // let colors = &cx.theme::().colors; - - let button = div() - // .fill(colors.error(0.5)) - .h_4() - .children(self.label.clone()); - - if let Some(handler) = self.handlers.click.clone() { - let data = self.data.clone(); - button.on_mouse_down(MouseButton::Left, move |view, event, cx| { - handler(view, data.as_ref(), cx) - }) - } else { - button - } - } -} diff --git a/crates/storybook2/src/stories/kitchen_sink.rs b/crates/storybook2/src/stories/kitchen_sink.rs index cfa91417b6..cf8277c4f4 100644 --- a/crates/storybook2/src/stories/kitchen_sink.rs +++ b/crates/storybook2/src/stories/kitchen_sink.rs @@ -1,7 +1,4 @@ -use crate::{ - story::Story, - story_selector::{ComponentStory, ElementStory}, -}; +use crate::{story::Story, story_selector::ComponentStory}; use gpui2::{Div, Render, StatefulInteraction, View, VisualContext}; use strum::IntoEnumIterator; use ui::prelude::*; @@ -18,9 +15,6 @@ impl Render for KitchenSinkStory { type Element = Div>; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { - let element_stories = ElementStory::iter() - .map(|selector| selector.story(cx)) - .collect::>(); let component_stories = ComponentStory::iter() .map(|selector| selector.story(cx)) .collect::>(); @@ -29,8 +23,6 @@ impl Render for KitchenSinkStory { .id("kitchen-sink") .overflow_y_scroll() .child(Story::title(cx, "Kitchen Sink")) - .child(Story::label(cx, "Elements")) - .child(div().flex().flex_col().children(element_stories)) .child(Story::label(cx, "Components")) .child(div().flex().flex_col().children(component_stories)) // Add a bit of space at the bottom of the kitchen sink so elements diff --git a/crates/storybook2/src/story_selector.rs b/crates/storybook2/src/story_selector.rs index 968309bd8a..2adf2956d3 100644 --- a/crates/storybook2/src/story_selector.rs +++ b/crates/storybook2/src/story_selector.rs @@ -7,55 +7,31 @@ use clap::builder::PossibleValue; use clap::ValueEnum; use gpui2::{AnyView, VisualContext}; use strum::{EnumIter, EnumString, IntoEnumIterator}; -use ui::{prelude::*, AvatarStory, ButtonStory, DetailsStory, IconStory, InputStory, LabelStory}; - -#[derive(Debug, PartialEq, Eq, Clone, Copy, strum::Display, EnumString, EnumIter)] -#[strum(serialize_all = "snake_case")] -pub enum ElementStory { - Avatar, - Button, - Colors, - Details, - Focus, - Icon, - Input, - Label, - Scroll, - Text, - ZIndex, -} - -impl ElementStory { - pub fn story(&self, cx: &mut WindowContext) -> AnyView { - match self { - Self::Colors => cx.build_view(|_| ColorsStory).into(), - Self::Avatar => cx.build_view(|_| AvatarStory).into(), - Self::Button => cx.build_view(|_| ButtonStory).into(), - Self::Details => cx.build_view(|_| DetailsStory).into(), - Self::Focus => FocusStory::view(cx).into(), - Self::Icon => cx.build_view(|_| IconStory).into(), - Self::Input => cx.build_view(|_| InputStory).into(), - Self::Label => cx.build_view(|_| LabelStory).into(), - Self::Scroll => ScrollStory::view(cx).into(), - Self::Text => TextStory::view(cx).into(), - Self::ZIndex => cx.build_view(|_| ZIndexStory).into(), - } - } -} +use ui::prelude::*; +use ui::{AvatarStory, ButtonStory, DetailsStory, IconStory, InputStory, LabelStory}; #[derive(Debug, PartialEq, Eq, Clone, Copy, strum::Display, EnumString, EnumIter)] #[strum(serialize_all = "snake_case")] pub enum ComponentStory { AssistantPanel, + Avatar, Breadcrumb, Buffer, + Button, ChatPanel, + Checkbox, CollabPanel, + Colors, CommandPalette, - Copilot, ContextMenu, + Copilot, + Details, Facepile, + Focus, + Icon, + Input, Keybinding, + Label, LanguageSelector, MultiBuffer, NotificationsPanel, @@ -63,29 +39,42 @@ pub enum ComponentStory { Panel, ProjectPanel, RecentProjects, + Scroll, Tab, TabBar, Terminal, + Text, ThemeSelector, TitleBar, Toast, Toolbar, TrafficLights, Workspace, + ZIndex, } impl ComponentStory { pub fn story(&self, cx: &mut WindowContext) -> AnyView { match self { Self::AssistantPanel => cx.build_view(|_| ui::AssistantPanelStory).into(), - Self::Buffer => cx.build_view(|_| ui::BufferStory).into(), + Self::Avatar => cx.build_view(|_| AvatarStory).into(), Self::Breadcrumb => cx.build_view(|_| ui::BreadcrumbStory).into(), + Self::Buffer => cx.build_view(|_| ui::BufferStory).into(), + Self::Button => cx.build_view(|_| ButtonStory).into(), Self::ChatPanel => cx.build_view(|_| ui::ChatPanelStory).into(), + Self::Checkbox => cx.build_view(|_| ui::CheckboxStory).into(), Self::CollabPanel => cx.build_view(|_| ui::CollabPanelStory).into(), + Self::Colors => cx.build_view(|_| ColorsStory).into(), Self::CommandPalette => cx.build_view(|_| ui::CommandPaletteStory).into(), Self::ContextMenu => cx.build_view(|_| ui::ContextMenuStory).into(), + Self::Copilot => cx.build_view(|_| ui::CopilotModalStory).into(), + Self::Details => cx.build_view(|_| DetailsStory).into(), Self::Facepile => cx.build_view(|_| ui::FacepileStory).into(), + Self::Focus => FocusStory::view(cx).into(), + Self::Icon => cx.build_view(|_| IconStory).into(), + Self::Input => cx.build_view(|_| InputStory).into(), Self::Keybinding => cx.build_view(|_| ui::KeybindingStory).into(), + Self::Label => cx.build_view(|_| LabelStory).into(), Self::LanguageSelector => cx.build_view(|_| ui::LanguageSelectorStory).into(), Self::MultiBuffer => cx.build_view(|_| ui::MultiBufferStory).into(), Self::NotificationsPanel => cx.build_view(|cx| ui::NotificationsPanelStory).into(), @@ -93,23 +82,24 @@ impl ComponentStory { Self::Panel => cx.build_view(|cx| ui::PanelStory).into(), Self::ProjectPanel => cx.build_view(|_| ui::ProjectPanelStory).into(), Self::RecentProjects => cx.build_view(|_| ui::RecentProjectsStory).into(), + Self::Scroll => ScrollStory::view(cx).into(), Self::Tab => cx.build_view(|_| ui::TabStory).into(), Self::TabBar => cx.build_view(|_| ui::TabBarStory).into(), Self::Terminal => cx.build_view(|_| ui::TerminalStory).into(), + Self::Text => TextStory::view(cx).into(), Self::ThemeSelector => cx.build_view(|_| ui::ThemeSelectorStory).into(), + Self::TitleBar => ui::TitleBarStory::view(cx).into(), Self::Toast => cx.build_view(|_| ui::ToastStory).into(), Self::Toolbar => cx.build_view(|_| ui::ToolbarStory).into(), Self::TrafficLights => cx.build_view(|_| ui::TrafficLightsStory).into(), - Self::Copilot => cx.build_view(|_| ui::CopilotModalStory).into(), - Self::TitleBar => ui::TitleBarStory::view(cx).into(), Self::Workspace => ui::WorkspaceStory::view(cx).into(), + Self::ZIndex => cx.build_view(|_| ZIndexStory).into(), } } } #[derive(Debug, PartialEq, Eq, Clone, Copy)] pub enum StorySelector { - Element(ElementStory), Component(ComponentStory), KitchenSink, } @@ -126,13 +116,6 @@ impl FromStr for StorySelector { return Ok(Self::KitchenSink); } - if let Some((_, story)) = story.split_once("elements/") { - let element_story = ElementStory::from_str(story) - .with_context(|| format!("story not found for element '{story}'"))?; - - return Ok(Self::Element(element_story)); - } - if let Some((_, story)) = story.split_once("components/") { let component_story = ComponentStory::from_str(story) .with_context(|| format!("story not found for component '{story}'"))?; @@ -147,7 +130,6 @@ impl FromStr for StorySelector { impl StorySelector { pub fn story(&self, cx: &mut WindowContext) -> AnyView { match self { - Self::Element(element_story) => element_story.story(cx), Self::Component(component_story) => component_story.story(cx), Self::KitchenSink => KitchenSinkStory::view(cx).into(), } @@ -160,11 +142,9 @@ static ALL_STORY_SELECTORS: OnceLock> = OnceLock::new(); impl ValueEnum for StorySelector { fn value_variants<'a>() -> &'a [Self] { let stories = ALL_STORY_SELECTORS.get_or_init(|| { - let element_stories = ElementStory::iter().map(StorySelector::Element); let component_stories = ComponentStory::iter().map(StorySelector::Component); - element_stories - .chain(component_stories) + component_stories .chain(std::iter::once(StorySelector::KitchenSink)) .collect::>() }); @@ -174,7 +154,6 @@ impl ValueEnum for StorySelector { fn to_possible_value(&self) -> Option { let value = match self { - Self::Element(story) => format!("elements/{story}"), Self::Component(story) => format!("components/{story}"), Self::KitchenSink => "kitchen_sink".to_string(), }; diff --git a/crates/theme2/src/colors.rs b/crates/theme2/src/colors.rs index 422e33e4f8..1a1fd2e99e 100644 --- a/crates/theme2/src/colors.rs +++ b/crates/theme2/src/colors.rs @@ -48,24 +48,26 @@ pub struct GitStatusColors { pub renamed: Hsla, } -#[derive(Refineable, Clone, Debug, Default)] -#[refineable(debug)] +#[derive(Refineable, Clone, Debug)] +#[refineable(debug, deserialize)] pub struct ThemeColors { pub border: Hsla, pub border_variant: Hsla, pub border_focused: Hsla, + pub border_selected: Hsla, pub border_transparent: Hsla, - pub elevated_surface: Hsla, - pub surface: Hsla, + pub border_disabled: Hsla, + pub elevated_surface_background: Hsla, + pub surface_background: Hsla, pub background: Hsla, - pub element: Hsla, + pub element_background: Hsla, pub element_hover: Hsla, pub element_active: Hsla, pub element_selected: Hsla, pub element_disabled: Hsla, pub element_placeholder: Hsla, pub element_drop_target: Hsla, - pub ghost_element: Hsla, + pub ghost_element_background: Hsla, pub ghost_element_hover: Hsla, pub ghost_element_active: Hsla, pub ghost_element_selected: Hsla, @@ -80,20 +82,39 @@ pub struct ThemeColors { pub icon_disabled: Hsla, pub icon_placeholder: Hsla, pub icon_accent: Hsla, - pub status_bar: Hsla, - pub title_bar: Hsla, - pub toolbar: Hsla, - pub tab_bar: Hsla, - pub tab_inactive: Hsla, - pub tab_active: Hsla, - pub editor: Hsla, - pub editor_subheader: Hsla, + pub status_bar_background: Hsla, + pub title_bar_background: Hsla, + pub toolbar_background: Hsla, + pub tab_bar_background: Hsla, + pub tab_inactive_background: Hsla, + pub tab_active_background: Hsla, + pub editor_background: Hsla, + pub editor_subheader_background: Hsla, pub editor_active_line: Hsla, + pub terminal_background: Hsla, + pub terminal_ansi_bright_black: Hsla, + pub terminal_ansi_bright_red: Hsla, + pub terminal_ansi_bright_green: Hsla, + pub terminal_ansi_bright_yellow: Hsla, + pub terminal_ansi_bright_blue: Hsla, + pub terminal_ansi_bright_magenta: Hsla, + pub terminal_ansi_bright_cyan: Hsla, + pub terminal_ansi_bright_white: Hsla, + pub terminal_ansi_black: Hsla, + pub terminal_ansi_red: Hsla, + pub terminal_ansi_green: Hsla, + pub terminal_ansi_yellow: Hsla, + pub terminal_ansi_blue: Hsla, + pub terminal_ansi_magenta: Hsla, + pub terminal_ansi_cyan: Hsla, + pub terminal_ansi_white: Hsla, } #[derive(Refineable, Clone)] pub struct ThemeStyles { pub system: SystemColors, + + #[refineable] pub colors: ThemeColors, pub status: StatusColors, pub git: GitStatusColors, @@ -103,6 +124,8 @@ pub struct ThemeStyles { #[cfg(test)] mod tests { + use serde_json::json; + use super::*; #[test] @@ -144,4 +167,16 @@ mod tests { assert_eq!(colors.text, magenta); assert_eq!(colors.background, green); } + + #[test] + fn deserialize_theme_colors_refinement_from_json() { + let colors: ThemeColorsRefinement = serde_json::from_value(json!({ + "background": "#ff00ff", + "text": "#ff0000" + })) + .unwrap(); + + assert_eq!(colors.background, Some(gpui::rgb(0xff00ff))); + assert_eq!(colors.text, Some(gpui::rgb(0xff0000))); + } } diff --git a/crates/theme2/src/default_colors.rs b/crates/theme2/src/default_colors.rs index 802392d296..53e34acf16 100644 --- a/crates/theme2/src/default_colors.rs +++ b/crates/theme2/src/default_colors.rs @@ -205,18 +205,20 @@ impl ThemeColors { border: neutral().light().step_6(), border_variant: neutral().light().step_5(), border_focused: blue().light().step_5(), + border_disabled: neutral().light().step_3(), + border_selected: blue().light().step_5(), border_transparent: system.transparent, - elevated_surface: neutral().light().step_2(), - surface: neutral().light().step_2(), + elevated_surface_background: neutral().light().step_2(), + surface_background: neutral().light().step_2(), background: neutral().light().step_1(), - element: neutral().light().step_3(), + element_background: neutral().light().step_3(), element_hover: neutral().light().step_4(), element_active: neutral().light().step_5(), element_selected: neutral().light().step_5(), element_disabled: neutral().light_alpha().step_3(), element_placeholder: neutral().light().step_11(), element_drop_target: blue().light_alpha().step_2(), - ghost_element: system.transparent, + ghost_element_background: system.transparent, ghost_element_hover: neutral().light().step_4(), ghost_element_active: neutral().light().step_5(), ghost_element_selected: neutral().light().step_5(), @@ -231,15 +233,32 @@ impl ThemeColors { icon_disabled: neutral().light().step_9(), icon_placeholder: neutral().light().step_10(), icon_accent: blue().light().step_11(), - status_bar: neutral().light().step_2(), - title_bar: neutral().light().step_2(), - toolbar: neutral().light().step_1(), - tab_bar: neutral().light().step_2(), - tab_active: neutral().light().step_1(), - tab_inactive: neutral().light().step_2(), - editor: neutral().light().step_1(), - editor_subheader: neutral().light().step_2(), + status_bar_background: neutral().light().step_2(), + title_bar_background: neutral().light().step_2(), + toolbar_background: neutral().light().step_1(), + tab_bar_background: neutral().light().step_2(), + tab_active_background: neutral().light().step_1(), + tab_inactive_background: neutral().light().step_2(), + editor_background: neutral().light().step_1(), + editor_subheader_background: neutral().light().step_2(), editor_active_line: neutral().light_alpha().step_3(), + terminal_background: neutral().light().step_1(), + terminal_ansi_black: black().light().step_12(), + terminal_ansi_red: red().light().step_11(), + terminal_ansi_green: green().light().step_11(), + terminal_ansi_yellow: yellow().light().step_11(), + terminal_ansi_blue: blue().light().step_11(), + terminal_ansi_magenta: violet().light().step_11(), + terminal_ansi_cyan: cyan().light().step_11(), + terminal_ansi_white: neutral().light().step_12(), + terminal_ansi_bright_black: black().light().step_11(), + terminal_ansi_bright_red: red().light().step_10(), + terminal_ansi_bright_green: green().light().step_10(), + terminal_ansi_bright_yellow: yellow().light().step_10(), + terminal_ansi_bright_blue: blue().light().step_10(), + terminal_ansi_bright_magenta: violet().light().step_10(), + terminal_ansi_bright_cyan: cyan().light().step_10(), + terminal_ansi_bright_white: neutral().light().step_11(), } } @@ -250,18 +269,20 @@ impl ThemeColors { border: neutral().dark().step_6(), border_variant: neutral().dark().step_5(), border_focused: blue().dark().step_5(), + border_disabled: neutral().dark().step_3(), + border_selected: blue().dark().step_5(), border_transparent: system.transparent, - elevated_surface: neutral().dark().step_2(), - surface: neutral().dark().step_2(), + elevated_surface_background: neutral().dark().step_2(), + surface_background: neutral().dark().step_2(), background: neutral().dark().step_1(), - element: neutral().dark().step_3(), + element_background: neutral().dark().step_3(), element_hover: neutral().dark().step_4(), element_active: neutral().dark().step_5(), element_selected: neutral().dark().step_5(), element_disabled: neutral().dark_alpha().step_3(), element_placeholder: neutral().dark().step_11(), element_drop_target: blue().dark_alpha().step_2(), - ghost_element: system.transparent, + ghost_element_background: system.transparent, ghost_element_hover: neutral().dark().step_4(), ghost_element_active: neutral().dark().step_5(), ghost_element_selected: neutral().dark().step_5(), @@ -276,15 +297,32 @@ impl ThemeColors { icon_disabled: neutral().dark().step_9(), icon_placeholder: neutral().dark().step_10(), icon_accent: blue().dark().step_11(), - status_bar: neutral().dark().step_2(), - title_bar: neutral().dark().step_2(), - toolbar: neutral().dark().step_1(), - tab_bar: neutral().dark().step_2(), - tab_active: neutral().dark().step_1(), - tab_inactive: neutral().dark().step_2(), - editor: neutral().dark().step_1(), - editor_subheader: neutral().dark().step_2(), + status_bar_background: neutral().dark().step_2(), + title_bar_background: neutral().dark().step_2(), + toolbar_background: neutral().dark().step_1(), + tab_bar_background: neutral().dark().step_2(), + tab_active_background: neutral().dark().step_1(), + tab_inactive_background: neutral().dark().step_2(), + editor_background: neutral().dark().step_1(), + editor_subheader_background: neutral().dark().step_2(), editor_active_line: neutral().dark_alpha().step_3(), + terminal_background: neutral().dark().step_1(), + terminal_ansi_black: black().dark().step_12(), + terminal_ansi_red: red().dark().step_11(), + terminal_ansi_green: green().dark().step_11(), + terminal_ansi_yellow: yellow().dark().step_11(), + terminal_ansi_blue: blue().dark().step_11(), + terminal_ansi_magenta: violet().dark().step_11(), + terminal_ansi_cyan: cyan().dark().step_11(), + terminal_ansi_white: neutral().dark().step_12(), + terminal_ansi_bright_black: black().dark().step_11(), + terminal_ansi_bright_red: red().dark().step_10(), + terminal_ansi_bright_green: green().dark().step_10(), + terminal_ansi_bright_yellow: yellow().dark().step_10(), + terminal_ansi_bright_blue: blue().dark().step_10(), + terminal_ansi_bright_magenta: violet().dark().step_10(), + terminal_ansi_bright_cyan: cyan().dark().step_10(), + terminal_ansi_bright_white: neutral().dark().step_11(), } } } diff --git a/crates/theme2/src/theme2.rs b/crates/theme2/src/theme2.rs index faf252e2e5..b8e22f8319 100644 --- a/crates/theme2/src/theme2.rs +++ b/crates/theme2/src/theme2.rs @@ -17,7 +17,7 @@ pub use syntax::*; use gpui::{AppContext, Hsla, SharedString}; -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, PartialEq, Clone, Copy)] pub enum Appearance { Light, Dark, diff --git a/crates/theme2/src/themes/.gitkeep b/crates/theme2/src/themes/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/crates/ui2/docs/building-ui.md b/crates/ui2/docs/building-ui.md new file mode 100644 index 0000000000..e0160e336e --- /dev/null +++ b/crates/ui2/docs/building-ui.md @@ -0,0 +1,49 @@ +# Building UI with GPUI + +## Common patterns + +### Method ordering + +- id +- Flex properties +- Position properties +- Size properties +- Style properties +- Handlers +- State properties + +### Using the Label Component to Create UI Text + +The `Label` component helps in displaying text on user interfaces. It creates an interface where specific parameters such as label color, line height style, and strikethrough can be set. + +Firstly, to create a `Label` instance, use the `Label::new()` function. This function takes a string that will be displayed as text in the interface. + +```rust +Label::new("Hello, world!"); +``` + +Now let's dive a bit deeper into how to customize `Label` instances: + +- **Setting Color:** To set the color of the label using various predefined color options such as `Default`, `Muted`, `Created`, `Modified`, `Deleted`, etc, the `color()` function is called on the `Label` instance: + + ```rust + Label::new("Hello, world!").color(LabelColor::Default); + ``` + +- **Setting Line Height Style:** To set the line height style, the `line_height_style()` function is utilized: + + ```rust + Label::new("Hello, world!").line_height_style(LineHeightStyle::TextLabel); + ``` + +- **Adding a Strikethrough:** To add a strikethrough in a `Label`, the `set_strikethrough()` function is used: + + ```rust + Label::new("Hello, world!").set_strikethrough(true); + ``` + +That's it! Now you can use the `Label` component to create and customize text on your application's interface. + +## Building a new component + +TODO diff --git a/crates/ui2/docs/elevation.md b/crates/ui2/docs/elevation.md deleted file mode 100644 index bd34de3396..0000000000 --- a/crates/ui2/docs/elevation.md +++ /dev/null @@ -1,57 +0,0 @@ -# Elevation - -Elevation in Zed applies to all surfaces and components. Elevation is categorized into levels. - -Elevation accomplishes the following: -- Allows surfaces to move in front of or behind others, such as content scrolling beneath app top bars. -- Reflects spatial relationships, for instance, how a floating action button’s shadow intimates its disconnection from a collection of cards. -- Directs attention to structures at the highest elevation, like a temporary dialog arising in front of other surfaces. - -Elevations are the initial elevation values assigned to components by default. - -Components may transition to a higher elevation in some cases, like user interations. - -On such occasions, components transition to predetermined dynamic elevation offsets. These are the typical elevations to which components move when they are not at rest. - -## Understanding Elevation - -Elevation can be thought of as the physical closeness of an element to the user. Elements with lower elevations are physically further away from the user on the z-axis and appear to be underneath elements with higher elevations. - -Material Design 3 has a some great visualizations of elevation that may be helpful to understanding the mental modal of elevation. [Material Design – Elevation](https://m3.material.io/styles/elevation/overview) - -## Elevation Levels - -Zed integrates six unique elevation levels in its design system. The elevation of a surface is expressed as a whole number ranging from 0 to 5, both numbers inclusive. A component’s elevation is ascertained by combining the component’s resting elevation with any dynamic elevation offsets. - -The levels are detailed as follows: - -0. App Background -1. UI Surface -2. Elevated Elements -3. Wash -4. Focused Element -5. Dragged Element - -### 0. App Background - -The app background constitutes the lowest elevation layer, appearing behind all other surfaces and components. It is predominantly used for the background color of the app. - -### 1. UI Surface - -The UI Surface is the standard elevation for components and is placed above the app background. It is generally used for the background color of the app bar, card, and sheet. - -### 2. Elevated Elements - -Elevated elements appear above the UI surface layer surfaces and components. Elevated elements are predominantly used for creating popovers, context menus, and tooltips. - -### 3. Wash - -Wash denotes a distinct elevation reserved to isolate app UI layers from high elevation components such as modals, notifications, and overlaid panels. The wash may not consistently be visible when these components are active. This layer is often referred to as a scrim or overlay and the background color of the wash is typically deployed in its design. - -### 4. Focused Element - -Focused elements obtain a higher elevation above surfaces and components at wash elevation. They are often used for modals, notifications, and overlaid panels and indicate that they are the sole element the user is interacting with at the moment. - -### 5. Dragged Element - -Dragged elements gain the highest elevation, thus appearing above surfaces and components at the elevation of focused elements. These are typically used for elements that are being dragged, following the cursor diff --git a/crates/ui2/docs/hello-world.md b/crates/ui2/docs/hello-world.md new file mode 100644 index 0000000000..c6ded9ce34 --- /dev/null +++ b/crates/ui2/docs/hello-world.md @@ -0,0 +1,160 @@ +# Hello World + +Let's work through the prototypical "Build a todo app" example to showcase how we might build a simple component from scratch. + +## Setup + +We'll create a headline, a list of todo items, and a form to add new items. + +~~~rust +struct TodoList { + headline: SharedString, + items: Vec, + submit_form: ClickHandler +} + +struct TodoItem { + text: SharedString, + completed: bool, + delete: ClickHandler +} + +impl TodoList { + pub fn new( + // Here we impl Into + headline: impl Into, + items: Vec, + submit_form: ClickHandler + ) -> Self { + Self { + // and here we call .into() so we can simply pass a string + // when creating the headline. This pattern is used throughout + // outr components + headline: headline.into(), + items: Vec::new(), + submit_form, + } + } +} +~~~ + +All of this is relatively straightforward. + +We use [gpui2::SharedString] in components instead of [std::string::String]. This allows us to [TODO: someone who actually knows please explain why we use SharedString]. + +When we want to pass an action we pass a `ClickHandler`. Whenever we want to add an action, the struct it belongs to needs to be generic over the view type `V`. + +~~~rust +use gpui2::hsla + +impl TodoList { + // ... + fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { + div().size_4().bg(hsla(50.0/360.0, 1.0, 0.5, 1.0)) + } +} +~~~ + +Every component needs a render method, and it should return `impl Component`. This basic component will render a 16x16px yellow square on the screen. + +A couple of questions might come to mind: + +**Why is `size_4()` 16px, not 4px?** + +gpui's style system is based on conventions created by [Tailwind CSS](https://tailwindcss.com/). Here is an example of the list of sizes for `width`: [Width - TailwindCSS Docs](https://tailwindcss.com/docs/width). + +I'll quote from the Tailwind [Core Concepts](https://tailwindcss.com/docs/utility-first) docs here: + +> Now I know what you’re thinking, “this is an atrocity, what a horrible mess!” +> and you’re right, it’s kind of ugly. In fact it’s just about impossible to +> think this is a good idea the first time you see it — +> you have to actually try it. + +As you start using the Tailwind-style conventions you will be surprised how quick it makes it to build out UIs. + +**Why `50.0/360.0` in `hsla()`?** + +gpui [gpui2::Hsla] use `0.0-1.0` for all it's values, but it is common for tools to use `0-360` for hue. + +This may change in the future, but this is a little trick that let's you use familiar looking values. + +## Building out the container + +Let's grab our [theme2::colors::ThemeColors] from the theme and start building out a basic container. + +We can access the current theme's colors like this: + +~~~rust +impl TodoList { + // ... + fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { + let color = cx.theme().colors() + + div().size_4().hsla(50.0/360.0, 1.0, 0.5, 1.0) + } +} +~~~ + +Now we have access to the complete set of colors defined in the theme. + +~~~rust +use gpui2::hsla + +impl TodoList { + // ... + fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { + let color = cx.theme().colors() + + div().size_4().bg(color.surface) + } +} +~~~ + +Let's finish up some basic styles for the container then move on to adding the other elements. + +~~~rust +use gpui2::hsla + +impl TodoList { + // ... + fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { + let color = cx.theme().colors() + + div() + // Flex properties + .flex() + .flex_col() // Stack elements vertically + .gap_2() // Add 8px of space between elements + // Size properties + .w_96() // Set width to 384px + .p_4() // Add 16px of padding on all sides + // Color properties + .bg(color.surface) // Set background color + .text_color(color.text) // Set text color + // Border properties + .rounded_md() // Add 4px of border radius + .border() // Add a 1px border + .border_color(color.border) + .child( + "Hello, world!" + ) + } +} +~~~ + +### Headline + +TODO + +### List of todo items + +TODO + +### Input + +TODO + + +### End result + +TODO diff --git a/crates/ui2/docs/todo.md b/crates/ui2/docs/todo.md new file mode 100644 index 0000000000..e7a053ecf4 --- /dev/null +++ b/crates/ui2/docs/todo.md @@ -0,0 +1,25 @@ +## Documentation priorities: + +These are the priorities to get documented, in a rough stack rank order: + +- [ ] label +- [ ] button +- [ ] icon_button +- [ ] icon +- [ ] list +- [ ] avatar +- [ ] panel +- [ ] modal +- [ ] palette +- [ ] input +- [ ] facepile +- [ ] player +- [ ] stacks +- [ ] context menu +- [ ] input +- [ ] textarea/multiline input (not built - not an editor) +- [ ] indicator +- [ ] public actor +- [ ] keybinding +- [ ] tab +- [ ] toast diff --git a/crates/ui2/src/components.rs b/crates/ui2/src/components.rs index f10f8cee8d..857d0f1042 100644 --- a/crates/ui2/src/components.rs +++ b/crates/ui2/src/components.rs @@ -1,73 +1,53 @@ -mod assistant_panel; -mod breadcrumb; -mod buffer; -mod buffer_search; -mod chat_panel; -mod collab_panel; -mod command_palette; +mod avatar; +mod button; +mod checkbox; mod context_menu; -mod copilot; -mod editor_pane; +mod details; mod facepile; +mod icon; mod icon_button; +mod indicator; +mod input; mod keybinding; -mod language_selector; +mod label; mod list; mod modal; -mod multi_buffer; mod notification_toast; -mod notifications_panel; mod palette; mod panel; -mod panes; +mod player; mod player_stack; -mod project_panel; -mod recent_projects; -mod status_bar; +mod slot; +mod stack; mod tab; -mod tab_bar; -mod terminal; -mod theme_selector; -mod title_bar; mod toast; -mod toolbar; +mod toggle; +mod tool_divider; mod tooltip; -mod traffic_lights; -mod workspace; -pub use assistant_panel::*; -pub use breadcrumb::*; -pub use buffer::*; -pub use buffer_search::*; -pub use chat_panel::*; -pub use collab_panel::*; -pub use command_palette::*; +pub use avatar::*; +pub use button::*; +pub use checkbox::*; pub use context_menu::*; -pub use copilot::*; -pub use editor_pane::*; +pub use details::*; pub use facepile::*; +pub use icon::*; pub use icon_button::*; +pub use indicator::*; +pub use input::*; pub use keybinding::*; -pub use language_selector::*; +pub use label::*; pub use list::*; pub use modal::*; -pub use multi_buffer::*; pub use notification_toast::*; -pub use notifications_panel::*; pub use palette::*; pub use panel::*; -pub use panes::*; +pub use player::*; pub use player_stack::*; -pub use project_panel::*; -pub use recent_projects::*; -pub use status_bar::*; +pub use slot::*; +pub use stack::*; pub use tab::*; -pub use tab_bar::*; -pub use terminal::*; -pub use theme_selector::*; -pub use title_bar::*; pub use toast::*; -pub use toolbar::*; +pub use toggle::*; +pub use tool_divider::*; pub use tooltip::*; -pub use traffic_lights::*; -pub use workspace::*; diff --git a/crates/ui2/src/elements/avatar.rs b/crates/ui2/src/components/avatar.rs similarity index 100% rename from crates/ui2/src/elements/avatar.rs rename to crates/ui2/src/components/avatar.rs diff --git a/crates/ui2/src/elements/button.rs b/crates/ui2/src/components/button.rs similarity index 96% rename from crates/ui2/src/elements/button.rs rename to crates/ui2/src/components/button.rs index 073bcdbb45..c13460aadd 100644 --- a/crates/ui2/src/elements/button.rs +++ b/crates/ui2/src/components/button.rs @@ -2,8 +2,27 @@ use std::sync::Arc; use gpui2::{div, rems, DefiniteLength, Hsla, MouseButton, WindowContext}; -use crate::prelude::*; use crate::{h_stack, Icon, IconColor, IconElement, Label, LabelColor, LineHeightStyle}; +use crate::{prelude::*, IconButton}; + +/// Provides the flexibility to use either a standard +/// button or an icon button in a given context. +pub enum ButtonOrIconButton { + Button(Button), + IconButton(IconButton), +} + +impl From> for ButtonOrIconButton { + fn from(value: Button) -> Self { + Self::Button(value) + } +} + +impl From> for ButtonOrIconButton { + fn from(value: IconButton) -> Self { + Self::IconButton(value) + } +} #[derive(Default, PartialEq, Clone, Copy)] pub enum IconPosition { @@ -22,8 +41,8 @@ pub enum ButtonVariant { impl ButtonVariant { pub fn bg_color(&self, cx: &mut WindowContext) -> Hsla { match self { - ButtonVariant::Ghost => cx.theme().colors().ghost_element, - ButtonVariant::Filled => cx.theme().colors().element, + ButtonVariant::Ghost => cx.theme().colors().ghost_element_background, + ButtonVariant::Filled => cx.theme().colors().element_background, } } diff --git a/crates/ui2/src/components/checkbox.rs b/crates/ui2/src/components/checkbox.rs new file mode 100644 index 0000000000..4b6a6240bb --- /dev/null +++ b/crates/ui2/src/components/checkbox.rs @@ -0,0 +1,220 @@ +///! # Checkbox +///! +///! Checkboxes are used for multiple choices, not for mutually exclusive choices. +///! Each checkbox works independently from other checkboxes in the list, +///! therefore checking an additional box does not affect any other selections. +use gpui2::{ + div, Component, ParentElement, SharedString, StatelessInteractive, Styled, ViewContext, +}; +use theme2::ActiveTheme; + +use crate::{Icon, IconColor, IconElement, Selected}; + +#[derive(Component)] +pub struct Checkbox { + id: SharedString, + checked: Selected, + disabled: bool, +} + +impl Checkbox { + pub fn new(id: impl Into) -> Self { + Self { + id: id.into(), + checked: Selected::Unselected, + disabled: false, + } + } + + pub fn toggle(mut self) -> Self { + self.checked = match self.checked { + Selected::Selected => Selected::Unselected, + Selected::Unselected => Selected::Selected, + Selected::Indeterminate => Selected::Selected, + }; + self + } + + pub fn set_indeterminate(mut self) -> Self { + self.checked = Selected::Indeterminate; + self + } + + pub fn set_disabled(mut self, disabled: bool) -> Self { + self.disabled = disabled; + self + } + + pub fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { + let group_id = format!("checkbox_group_{}", self.id); + + // The icon is different depending on the state of the checkbox. + // + // We need the match to return all the same type, + // so we wrap the eatch result in a div. + // + // We are still exploring the best way to handle this. + let icon = match self.checked { + // When selected, we show a checkmark. + Selected::Selected => { + div().child( + IconElement::new(Icon::Check) + .size(crate::IconSize::Small) + .color( + // If the checkbox is disabled we change the color of the icon. + if self.disabled { + IconColor::Disabled + } else { + IconColor::Selected + }, + ), + ) + } + // In an indeterminate state, we show a dash. + Selected::Indeterminate => { + div().child( + IconElement::new(Icon::Dash) + .size(crate::IconSize::Small) + .color( + // If the checkbox is disabled we change the color of the icon. + if self.disabled { + IconColor::Disabled + } else { + IconColor::Selected + }, + ), + ) + } + // When unselected, we show nothing. + Selected::Unselected => div(), + }; + + // A checkbox could be in an indeterminate state, + // for example the indeterminate state could represent: + // - a group of options of which only some are selected + // - an enabled option that is no longer available + // - a previously agreed to license that has been updated + // + // For the sake of styles we treat the indeterminate state as selected, + // but it's icon will be different. + let selected = + self.checked == Selected::Selected || self.checked == Selected::Indeterminate; + + // We could use something like this to make the checkbox background when selected: + // + // ~~~rust + // ... + // .when(selected, |this| { + // this.bg(cx.theme().colors().element_selected) + // }) + // ~~~ + // + // But we use a match instead here because the checkbox might be disabled, + // and it could be disabled _while_ it is selected, as well as while it is not selected. + let (bg_color, border_color) = match (self.disabled, selected) { + (true, _) => ( + cx.theme().colors().ghost_element_disabled, + cx.theme().colors().border_disabled, + ), + (false, true) => ( + cx.theme().colors().element_selected, + cx.theme().colors().border, + ), + (false, false) => ( + cx.theme().colors().element_background, + cx.theme().colors().border, + ), + }; + + div() + // Rather than adding `px_1()` to add some space around the checkbox, + // we use a larger parent element to create a slightly larger + // click area for the checkbox. + .size_5() + // Because we've enlarged the click area, we need to create a + // `group` to pass down interaction events to the checkbox. + .group(group_id.clone()) + .child( + div() + .flex() + // This prevent the flex element from growing + // or shrinking in response to any size changes + .flex_none() + // The combo of `justify_center()` and `items_center()` + // is used frequently to center elements in a flex container. + // + // We use this to center the icon in the checkbox. + .justify_center() + .items_center() + .m_1() + .size_4() + .rounded_sm() + .bg(bg_color) + .border() + .border_color(border_color) + // We only want the interaction states to fire when we + // are in a checkbox that isn't disabled. + .when(!self.disabled, |this| { + // Here instead of `hover()` we use `group_hover()` + // to pass it the group id. + this.group_hover(group_id.clone(), |el| { + el.bg(cx.theme().colors().element_hover) + }) + }) + .child(icon), + ) + } +} + +#[cfg(feature = "stories")] +pub use stories::*; + +#[cfg(feature = "stories")] +mod stories { + use super::*; + use crate::{h_stack, Story}; + use gpui2::{Div, Render}; + + pub struct CheckboxStory; + + impl Render for CheckboxStory { + type Element = Div; + + fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + Story::container(cx) + .child(Story::title_for::<_, Checkbox>(cx)) + .child(Story::label(cx, "Default")) + .child( + h_stack() + .p_2() + .gap_2() + .rounded_md() + .border() + .border_color(cx.theme().colors().border) + .child(Checkbox::new("checkbox-enabled")) + .child(Checkbox::new("checkbox-intermediate").set_indeterminate()) + .child(Checkbox::new("checkbox-selected").toggle()), + ) + .child(Story::label(cx, "Disabled")) + .child( + h_stack() + .p_2() + .gap_2() + .rounded_md() + .border() + .border_color(cx.theme().colors().border) + .child(Checkbox::new("checkbox-disabled").set_disabled(true)) + .child( + Checkbox::new("checkbox-disabled-intermediate") + .set_disabled(true) + .set_indeterminate(), + ) + .child( + Checkbox::new("checkbox-disabled-selected") + .set_disabled(true) + .toggle(), + ), + ) + } + } +} diff --git a/crates/ui2/src/components/context_menu.rs b/crates/ui2/src/components/context_menu.rs index 8345be1b35..87be445c19 100644 --- a/crates/ui2/src/components/context_menu.rs +++ b/crates/ui2/src/components/context_menu.rs @@ -8,7 +8,7 @@ pub enum ContextMenuItem { } impl ContextMenuItem { - fn to_list_item(self) -> ListItem { + fn to_list_item(self) -> ListItem { match self { ContextMenuItem::Header(label) => ListSubHeader::new(label).into(), ContextMenuItem::Entry(label) => { @@ -46,18 +46,15 @@ impl ContextMenu { fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { v_stack() .flex() - .bg(cx.theme().colors().elevated_surface) + .bg(cx.theme().colors().elevated_surface_background) .border() .border_color(cx.theme().colors().border) - .child( - List::new( - self.items - .into_iter() - .map(ContextMenuItem::to_list_item) - .collect(), - ) - .toggle(ToggleState::Toggled), - ) + .child(List::new( + self.items + .into_iter() + .map(ContextMenuItem::to_list_item::) + .collect(), + )) } } diff --git a/crates/ui2/src/elements/details.rs b/crates/ui2/src/components/details.rs similarity index 100% rename from crates/ui2/src/elements/details.rs rename to crates/ui2/src/components/details.rs diff --git a/crates/ui2/src/elements/icon.rs b/crates/ui2/src/components/icon.rs similarity index 97% rename from crates/ui2/src/elements/icon.rs rename to crates/ui2/src/components/icon.rs index 9c056d68d1..fa1e1c6315 100644 --- a/crates/ui2/src/elements/icon.rs +++ b/crates/ui2/src/components/icon.rs @@ -22,6 +22,7 @@ pub enum IconColor { Warning, Success, Info, + Selected, } impl IconColor { @@ -36,6 +37,7 @@ impl IconColor { IconColor::Warning => cx.theme().status().warning, IconColor::Success => cx.theme().status().success, IconColor::Info => cx.theme().status().info, + IconColor::Selected => cx.theme().colors().icon_accent, } } } @@ -55,6 +57,7 @@ pub enum Icon { ChevronRight, ChevronUp, Close, + Dash, Exit, ExclamationTriangle, File, @@ -112,6 +115,7 @@ impl Icon { Icon::ChevronRight => "icons/chevron_right.svg", Icon::ChevronUp => "icons/chevron_up.svg", Icon::Close => "icons/x.svg", + Icon::Dash => "icons/dash.svg", Icon::Exit => "icons/exit.svg", Icon::ExclamationTriangle => "icons/warning.svg", Icon::File => "icons/file.svg", diff --git a/crates/ui2/src/components/icon_button.rs b/crates/ui2/src/components/icon_button.rs index 101c845a76..8fe1a1fa9b 100644 --- a/crates/ui2/src/components/icon_button.rs +++ b/crates/ui2/src/components/icon_button.rs @@ -73,12 +73,12 @@ impl IconButton { let (bg_color, bg_hover_color, bg_active_color) = match self.variant { ButtonVariant::Filled => ( - cx.theme().colors().element, + cx.theme().colors().element_background, cx.theme().colors().element_hover, cx.theme().colors().element_active, ), ButtonVariant::Ghost => ( - cx.theme().colors().ghost_element, + cx.theme().colors().ghost_element_background, cx.theme().colors().ghost_element_hover, cx.theme().colors().ghost_element_active, ), diff --git a/crates/ui2/src/elements/indicator.rs b/crates/ui2/src/components/indicator.rs similarity index 87% rename from crates/ui2/src/elements/indicator.rs rename to crates/ui2/src/components/indicator.rs index 1f6e00e621..94398ab7f6 100644 --- a/crates/ui2/src/elements/indicator.rs +++ b/crates/ui2/src/components/indicator.rs @@ -14,7 +14,7 @@ impl UnreadIndicator { div() .rounded_full() .border_2() - .border_color(cx.theme().colors().surface) + .border_color(cx.theme().colors().surface_background) .w(px(9.0)) .h(px(9.0)) .z_index(2) diff --git a/crates/ui2/src/elements/input.rs b/crates/ui2/src/components/input.rs similarity index 96% rename from crates/ui2/src/elements/input.rs rename to crates/ui2/src/components/input.rs index 2884470ce2..f288f3ca56 100644 --- a/crates/ui2/src/elements/input.rs +++ b/crates/ui2/src/components/input.rs @@ -59,12 +59,12 @@ impl Input { fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { let (input_bg, input_hover_bg, input_active_bg) = match self.variant { InputVariant::Ghost => ( - cx.theme().colors().ghost_element, + cx.theme().colors().ghost_element_background, cx.theme().colors().ghost_element_hover, cx.theme().colors().ghost_element_active, ), InputVariant::Filled => ( - cx.theme().colors().element, + cx.theme().colors().element_background, cx.theme().colors().element_hover, cx.theme().colors().element_active, ), diff --git a/crates/ui2/src/components/keybinding.rs b/crates/ui2/src/components/keybinding.rs index 88cabbdc88..8b8fba8c08 100644 --- a/crates/ui2/src/components/keybinding.rs +++ b/crates/ui2/src/components/keybinding.rs @@ -66,7 +66,7 @@ impl Key { .rounded_md() .text_sm() .text_color(cx.theme().colors().text) - .bg(cx.theme().colors().element) + .bg(cx.theme().colors().element_background) .child(self.key.clone()) } } diff --git a/crates/ui2/src/elements/label.rs b/crates/ui2/src/components/label.rs similarity index 100% rename from crates/ui2/src/elements/label.rs rename to crates/ui2/src/components/label.rs diff --git a/crates/ui2/src/components/list.rs b/crates/ui2/src/components/list.rs index 50a86ff256..b30beacd98 100644 --- a/crates/ui2/src/components/list.rs +++ b/crates/ui2/src/components/list.rs @@ -1,11 +1,11 @@ -use gpui2::{div, px, relative, Div}; +use gpui2::div; use crate::settings::user_settings; use crate::{ - h_stack, v_stack, Avatar, ClickHandler, Icon, IconColor, IconElement, IconSize, Label, - LabelColor, + disclosure_control, h_stack, v_stack, Avatar, Icon, IconColor, IconElement, IconSize, Label, + LabelColor, Toggle, }; -use crate::{prelude::*, Button}; +use crate::{prelude::*, GraphicSlot}; #[derive(Clone, Copy, Default, Debug, PartialEq)] pub enum ListItemVariant { @@ -29,7 +29,7 @@ pub struct ListHeader { left_icon: Option, meta: Option, variant: ListItemVariant, - toggleable: Toggleable, + toggle: Toggle, } impl ListHeader { @@ -39,17 +39,12 @@ impl ListHeader { left_icon: None, meta: None, variant: ListItemVariant::default(), - toggleable: Toggleable::NotToggleable, + toggle: Toggle::NotToggleable, } } - pub fn toggle(mut self, toggle: ToggleState) -> Self { - self.toggleable = toggle.into(); - self - } - - pub fn toggleable(mut self, toggleable: Toggleable) -> Self { - self.toggleable = toggleable; + pub fn toggle(mut self, toggle: Toggle) -> Self { + self.toggle = toggle; self } @@ -63,30 +58,8 @@ impl ListHeader { self } - fn disclosure_control(&self) -> Div { - let is_toggleable = self.toggleable != Toggleable::NotToggleable; - let is_toggled = Toggleable::is_toggled(&self.toggleable); - - match (is_toggleable, is_toggled) { - (false, _) => div(), - (_, true) => div().child( - IconElement::new(Icon::ChevronDown) - .color(IconColor::Muted) - .size(IconSize::Small), - ), - (_, false) => div().child( - IconElement::new(Icon::ChevronRight) - .color(IconColor::Muted) - .size(IconSize::Small), - ), - } - } - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { - let is_toggleable = self.toggleable != Toggleable::NotToggleable; - let is_toggled = self.toggleable.is_toggled(); - - let disclosure_control = self.disclosure_control(); + let disclosure_control = disclosure_control(self.toggle); let meta = match self.meta { Some(ListHeaderMeta::Tools(icons)) => div().child( @@ -106,7 +79,7 @@ impl ListHeader { h_stack() .w_full() - .bg(cx.theme().colors().surface) + .bg(cx.theme().colors().surface_background) // TODO: Add focus state // .when(self.state == InteractionState::Focused, |this| { // this.border() @@ -193,12 +166,6 @@ impl ListSubHeader { } } -#[derive(Clone)] -pub enum LeftContent { - Icon(Icon), - Avatar(SharedString), -} - #[derive(Default, PartialEq, Copy, Clone)] pub enum ListEntrySize { #[default] @@ -207,44 +174,36 @@ pub enum ListEntrySize { } #[derive(Component)] -pub enum ListItem { +pub enum ListItem { Entry(ListEntry), - Details(ListDetailsEntry), Separator(ListSeparator), Header(ListSubHeader), } -impl From for ListItem { +impl From for ListItem { fn from(entry: ListEntry) -> Self { Self::Entry(entry) } } -impl From> for ListItem { - fn from(entry: ListDetailsEntry) -> Self { - Self::Details(entry) - } -} - -impl From for ListItem { +impl From for ListItem { fn from(entry: ListSeparator) -> Self { Self::Separator(entry) } } -impl From for ListItem { +impl From for ListItem { fn from(entry: ListSubHeader) -> Self { Self::Header(entry) } } -impl ListItem { - fn render(self, view: &mut V, cx: &mut ViewContext) -> impl Component { +impl ListItem { + fn render(self, view: &mut V, cx: &mut ViewContext) -> impl Component { match self { ListItem::Entry(entry) => div().child(entry.render(view, cx)), ListItem::Separator(separator) => div().child(separator.render(view, cx)), ListItem::Header(header) => div().child(header.render(view, cx)), - ListItem::Details(details) => div().child(details.render(view, cx)), } } @@ -263,31 +222,29 @@ impl ListItem { #[derive(Component)] pub struct ListEntry { - disclosure_control_style: DisclosureControlVisibility, + disabled: bool, + // TODO: Reintroduce this + // disclosure_control_style: DisclosureControlVisibility, indent_level: u32, label: Label, - left_content: Option, - variant: ListItemVariant, - size: ListEntrySize, - state: InteractionState, - toggle: Option, + left_slot: Option, overflow: OverflowStyle, + size: ListEntrySize, + toggle: Toggle, + variant: ListItemVariant, } impl ListEntry { pub fn new(label: Label) -> Self { Self { - disclosure_control_style: DisclosureControlVisibility::default(), + disabled: false, indent_level: 0, label, - variant: ListItemVariant::default(), - left_content: None, - size: ListEntrySize::default(), - state: InteractionState::default(), - // TODO: Should use Toggleable::NotToggleable - // or remove Toggleable::NotToggleable from the system - toggle: None, + left_slot: None, overflow: OverflowStyle::Hidden, + size: ListEntrySize::default(), + toggle: Toggle::NotToggleable, + variant: ListItemVariant::default(), } } @@ -301,28 +258,23 @@ impl ListEntry { self } - pub fn toggle(mut self, toggle: ToggleState) -> Self { - self.toggle = Some(toggle); + pub fn toggle(mut self, toggle: Toggle) -> Self { + self.toggle = toggle; self } - pub fn left_content(mut self, left_content: LeftContent) -> Self { - self.left_content = Some(left_content); + pub fn left_content(mut self, left_content: GraphicSlot) -> Self { + self.left_slot = Some(left_content); self } pub fn left_icon(mut self, left_icon: Icon) -> Self { - self.left_content = Some(LeftContent::Icon(left_icon)); + self.left_slot = Some(GraphicSlot::Icon(left_icon)); self } pub fn left_avatar(mut self, left_avatar: impl Into) -> Self { - self.left_content = Some(LeftContent::Avatar(left_avatar.into())); - self - } - - pub fn state(mut self, state: InteractionState) -> Self { - self.state = state; + self.left_slot = Some(GraphicSlot::Avatar(left_avatar.into())); self } @@ -331,63 +283,19 @@ impl ListEntry { self } - pub fn disclosure_control_style( - mut self, - disclosure_control_style: DisclosureControlVisibility, - ) -> Self { - self.disclosure_control_style = disclosure_control_style; - self - } - - fn label_color(&self) -> LabelColor { - match self.state { - InteractionState::Disabled => LabelColor::Disabled, - _ => Default::default(), - } - } - - fn icon_color(&self) -> IconColor { - match self.state { - InteractionState::Disabled => IconColor::Disabled, - _ => Default::default(), - } - } - - fn disclosure_control( - &mut self, - cx: &mut ViewContext, - ) -> Option> { - let disclosure_control_icon = if let Some(ToggleState::Toggled) = self.toggle { - IconElement::new(Icon::ChevronDown) - } else { - IconElement::new(Icon::ChevronRight) - } - .color(IconColor::Muted) - .size(IconSize::Small); - - match (self.toggle, self.disclosure_control_style) { - (Some(_), DisclosureControlVisibility::OnHover) => { - Some(div().absolute().neg_left_5().child(disclosure_control_icon)) - } - (Some(_), DisclosureControlVisibility::Always) => { - Some(div().child(disclosure_control_icon)) - } - (None, _) => None, - } - } - - fn render(mut self, _view: &mut V, cx: &mut ViewContext) -> impl Component { + fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { let settings = user_settings(cx); - let left_content = match self.left_content.clone() { - Some(LeftContent::Icon(i)) => Some( + let left_content = match self.left_slot.clone() { + Some(GraphicSlot::Icon(i)) => Some( h_stack().child( IconElement::new(i) .size(IconSize::Small) .color(IconColor::Muted), ), ), - Some(LeftContent::Avatar(src)) => Some(h_stack().child(Avatar::new(src))), + Some(GraphicSlot::Avatar(src)) => Some(h_stack().child(Avatar::new(src))), + Some(GraphicSlot::PublicActor(src)) => Some(h_stack().child(Avatar::new(src))), None => None, }; @@ -399,11 +307,8 @@ impl ListEntry { div() .relative() .group("") - .bg(cx.theme().colors().surface) - .when(self.state == InteractionState::Focused, |this| { - this.border() - .border_color(cx.theme().colors().border_focused) - }) + .bg(cx.theme().colors().surface_background) + // TODO: Add focus state .child( sized_item .when(self.variant == ListItemVariant::Inset, |this| this.px_2()) @@ -425,131 +330,13 @@ impl ListEntry { .gap_1() .items_center() .relative() - .children(self.disclosure_control(cx)) + .child(disclosure_control(self.toggle)) .children(left_content) .child(self.label), ) } } -struct ListDetailsEntryHandlers { - click: Option>, -} - -impl Default for ListDetailsEntryHandlers { - fn default() -> Self { - Self { click: None } - } -} - -#[derive(Component)] -pub struct ListDetailsEntry { - label: SharedString, - meta: Option, - left_content: Option, - handlers: ListDetailsEntryHandlers, - actions: Option>>, - // TODO: make this more generic instead of - // specifically for notifications - seen: bool, -} - -impl ListDetailsEntry { - pub fn new(label: impl Into) -> Self { - Self { - label: label.into(), - meta: None, - left_content: None, - handlers: ListDetailsEntryHandlers::default(), - actions: None, - seen: false, - } - } - - pub fn meta(mut self, meta: impl Into) -> Self { - self.meta = Some(meta.into()); - self - } - - pub fn seen(mut self, seen: bool) -> Self { - self.seen = seen; - self - } - - pub fn on_click(mut self, handler: ClickHandler) -> Self { - self.handlers.click = Some(handler); - self - } - - pub fn actions(mut self, actions: Vec>) -> Self { - self.actions = Some(actions); - self - } - - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { - let settings = user_settings(cx); - - let (item_bg, item_bg_hover, item_bg_active) = ( - cx.theme().colors().ghost_element, - cx.theme().colors().ghost_element_hover, - cx.theme().colors().ghost_element_active, - ); - - let label_color = match self.seen { - true => LabelColor::Muted, - false => LabelColor::Default, - }; - - div() - .relative() - .group("") - .bg(item_bg) - .px_2() - .py_1p5() - .w_full() - .z_index(1) - .when(!self.seen, |this| { - this.child( - div() - .absolute() - .left(px(3.0)) - .top_3() - .rounded_full() - .border_2() - .border_color(cx.theme().colors().surface) - .w(px(9.0)) - .h(px(9.0)) - .z_index(2) - .bg(cx.theme().status().info), - ) - }) - .child( - v_stack() - .w_full() - .line_height(relative(1.2)) - .gap_1() - .child( - div() - .w_5() - .h_5() - .rounded_full() - .bg(cx.theme().colors().icon_accent), - ) - .child(Label::new(self.label.clone()).color(label_color)) - .children( - self.meta - .map(|meta| Label::new(meta).color(LabelColor::Muted)), - ) - .child( - h_stack() - .gap_1() - .justify_end() - .children(self.actions.unwrap_or_default()), - ), - ) - } -} - #[derive(Clone, Component)] pub struct ListSeparator; @@ -564,20 +351,22 @@ impl ListSeparator { } #[derive(Component)] -pub struct List { - items: Vec>, +pub struct List { + items: Vec, + /// Message to display when the list is empty + /// Defaults to "No items" empty_message: SharedString, header: Option, - toggleable: Toggleable, + toggle: Toggle, } -impl List { - pub fn new(items: Vec>) -> Self { +impl List { + pub fn new(items: Vec) -> Self { Self { items, empty_message: "No items".into(), header: None, - toggleable: Toggleable::default(), + toggle: Toggle::NotToggleable, } } @@ -591,19 +380,16 @@ impl List { self } - pub fn toggle(mut self, toggle: ToggleState) -> Self { - self.toggleable = toggle.into(); + pub fn toggle(mut self, toggle: Toggle) -> Self { + self.toggle = toggle; self } - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { - let is_toggleable = self.toggleable != Toggleable::NotToggleable; - let is_toggled = Toggleable::is_toggled(&self.toggleable); - - let list_content = match (self.items.is_empty(), is_toggled) { + fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { + let list_content = match (self.items.is_empty(), self.toggle) { (false, _) => div().children(self.items), - (true, false) => div(), - (true, true) => { + (true, Toggle::Toggled(false)) => div(), + (true, _) => { div().child(Label::new(self.empty_message.clone()).color(LabelColor::Muted)) } }; @@ -611,7 +397,7 @@ impl List { v_stack() .w_full() .py_1() - .children(self.header.map(|header| header.toggleable(self.toggleable))) + .children(self.header.map(|header| header)) .child(list_content) } } diff --git a/crates/ui2/src/components/notification_toast.rs b/crates/ui2/src/components/notification_toast.rs index 59078c98f4..e8739b925c 100644 --- a/crates/ui2/src/components/notification_toast.rs +++ b/crates/ui2/src/components/notification_toast.rs @@ -34,7 +34,7 @@ impl NotificationToast { .px_1p5() .rounded_lg() .shadow_md() - .bg(cx.theme().colors().elevated_surface) + .bg(cx.theme().colors().elevated_surface_background) .child(div().size_full().child(self.label.clone())) } } diff --git a/crates/ui2/src/components/palette.rs b/crates/ui2/src/components/palette.rs index a1f3eb7e1c..269b39d86d 100644 --- a/crates/ui2/src/components/palette.rs +++ b/crates/ui2/src/components/palette.rs @@ -47,7 +47,7 @@ impl Palette { .id(self.id.clone()) .w_96() .rounded_lg() - .bg(cx.theme().colors().elevated_surface) + .bg(cx.theme().colors().elevated_surface_background) .border() .border_color(cx.theme().colors().border) .child( @@ -56,7 +56,12 @@ impl Palette { .child(v_stack().py_0p5().px_1().child(div().px_2().py_0p5().child( Label::new(self.input_placeholder.clone()).color(LabelColor::Placeholder), ))) - .child(div().h_px().w_full().bg(cx.theme().colors().element)) + .child( + div() + .h_px() + .w_full() + .bg(cx.theme().colors().element_background), + ) .child( v_stack() .id("items") diff --git a/crates/ui2/src/components/panel.rs b/crates/ui2/src/components/panel.rs index 5d941eb50e..ba88abb337 100644 --- a/crates/ui2/src/components/panel.rs +++ b/crates/ui2/src/components/panel.rs @@ -107,7 +107,7 @@ impl Panel { PanelSide::Right => this.border_l(), PanelSide::Bottom => this.border_b().w_full().h(current_size), }) - .bg(cx.theme().colors().surface) + .bg(cx.theme().colors().surface_background) .border_color(cx.theme().colors().border) .children(self.children) } diff --git a/crates/ui2/src/elements/player.rs b/crates/ui2/src/components/player.rs similarity index 87% rename from crates/ui2/src/elements/player.rs rename to crates/ui2/src/components/player.rs index c7b7ade1c1..b6c80400cf 100644 --- a/crates/ui2/src/elements/player.rs +++ b/crates/ui2/src/components/player.rs @@ -2,6 +2,24 @@ use gpui2::{Hsla, ViewContext}; use crate::prelude::*; +/// Represents a person with a Zed account's public profile. +/// All data in this struct should be considered public. +pub struct PublicPlayer { + pub username: SharedString, + pub avatar: SharedString, + pub is_contact: bool, +} + +impl PublicPlayer { + pub fn new(username: impl Into, avatar: impl Into) -> Self { + Self { + username: username.into(), + avatar: avatar.into(), + is_contact: false, + } + } +} + #[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] pub enum PlayerStatus { #[default] diff --git a/crates/ui2/src/components/slot.rs b/crates/ui2/src/components/slot.rs new file mode 100644 index 0000000000..f980e2fbda --- /dev/null +++ b/crates/ui2/src/components/slot.rs @@ -0,0 +1,14 @@ +use gpui2::SharedString; + +use crate::Icon; + +#[derive(Debug, Clone)] +/// A slot utility that provides a way to to pass either +/// an icon or an image to a component. +/// +/// Can be filled with a [] +pub enum GraphicSlot { + Icon(Icon), + Avatar(SharedString), + PublicActor(SharedString), +} diff --git a/crates/ui2/src/elements/stack.rs b/crates/ui2/src/components/stack.rs similarity index 100% rename from crates/ui2/src/elements/stack.rs rename to crates/ui2/src/components/stack.rs diff --git a/crates/ui2/src/components/tab.rs b/crates/ui2/src/components/tab.rs index e8b0ee3be5..47de0541f1 100644 --- a/crates/ui2/src/components/tab.rs +++ b/crates/ui2/src/components/tab.rs @@ -109,12 +109,12 @@ impl Tab { let (tab_bg, tab_hover_bg, tab_active_bg) = match self.current { false => ( - cx.theme().colors().tab_inactive, + cx.theme().colors().tab_inactive_background, cx.theme().colors().ghost_element_hover, cx.theme().colors().ghost_element_active, ), true => ( - cx.theme().colors().tab_active, + cx.theme().colors().tab_active_background, cx.theme().colors().element_hover, cx.theme().colors().element_active, ), diff --git a/crates/ui2/src/components/toast.rs b/crates/ui2/src/components/toast.rs index 3b81ac42b4..4ab6625dba 100644 --- a/crates/ui2/src/components/toast.rs +++ b/crates/ui2/src/components/toast.rs @@ -54,7 +54,7 @@ impl Toast { .rounded_lg() .shadow_md() .overflow_hidden() - .bg(cx.theme().colors().elevated_surface) + .bg(cx.theme().colors().elevated_surface_background) .children(self.children) } } diff --git a/crates/ui2/src/components/toggle.rs b/crates/ui2/src/components/toggle.rs new file mode 100644 index 0000000000..adde367581 --- /dev/null +++ b/crates/ui2/src/components/toggle.rs @@ -0,0 +1,61 @@ +use gpui2::{div, Component, ParentElement}; + +use crate::{Icon, IconColor, IconElement, IconSize}; + +/// Whether the entry is toggleable, and if so, whether it is currently toggled. +/// +/// To make an element toggleable, simply add a `Toggle::Toggled(_)` and handle it's cases. +/// +/// You can check if an element is toggleable with `.is_toggleable()` +/// +/// Possible values: +/// - `Toggle::NotToggleable` - The entry is not toggleable +/// - `Toggle::Toggled(true)` - The entry is toggleable and toggled +/// - `Toggle::Toggled(false)` - The entry is toggleable and not toggled +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum Toggle { + NotToggleable, + Toggled(bool), +} + +impl Toggle { + /// Returns true if the entry is toggled (or is not toggleable.) + /// + /// As element that isn't toggleable is always "expanded" or "enabled" + /// returning true in that case makes sense. + pub fn is_toggled(&self) -> bool { + match self { + Self::Toggled(false) => false, + _ => true, + } + } + + pub fn is_toggleable(&self) -> bool { + match self { + Self::Toggled(_) => true, + _ => false, + } + } +} + +impl From for Toggle { + fn from(toggled: bool) -> Self { + Toggle::Toggled(toggled) + } +} + +pub fn disclosure_control(toggle: Toggle) -> impl Component { + match (toggle.is_toggleable(), toggle.is_toggled()) { + (false, _) => div(), + (_, true) => div().child( + IconElement::new(Icon::ChevronDown) + .color(IconColor::Muted) + .size(IconSize::Small), + ), + (_, false) => div().child( + IconElement::new(Icon::ChevronRight) + .color(IconColor::Muted) + .size(IconSize::Small), + ), + } +} diff --git a/crates/ui2/src/elements/tool_divider.rs b/crates/ui2/src/components/tool_divider.rs similarity index 100% rename from crates/ui2/src/elements/tool_divider.rs rename to crates/ui2/src/components/tool_divider.rs diff --git a/crates/ui2/src/elements.rs b/crates/ui2/src/elements.rs deleted file mode 100644 index dfff2761a7..0000000000 --- a/crates/ui2/src/elements.rs +++ /dev/null @@ -1,21 +0,0 @@ -mod avatar; -mod button; -mod details; -mod icon; -mod indicator; -mod input; -mod label; -mod player; -mod stack; -mod tool_divider; - -pub use avatar::*; -pub use button::*; -pub use details::*; -pub use icon::*; -pub use indicator::*; -pub use input::*; -pub use label::*; -pub use player::*; -pub use stack::*; -pub use tool_divider::*; diff --git a/crates/ui2/src/lib.rs b/crates/ui2/src/lib.rs index 5d0a57c6d9..5fb3998438 100644 --- a/crates/ui2/src/lib.rs +++ b/crates/ui2/src/lib.rs @@ -7,28 +7,25 @@ //! This crate is still a work in progress. The initial primitives and components are built for getting all the UI on the screen, //! much of the state and functionality is mocked or hard codeded, and performance has not been a focus. //! -//! Expect some inconsistencies from component to component as we work out the best way to build these components. -//! -//! ## Design Philosophy -//! -//! Work in Progress! -//! +#![doc = include_str!("../docs/hello-world.md")] +#![doc = include_str!("../docs/building-ui.md")] +#![doc = include_str!("../docs/todo.md")] // TODO: Fix warnings instead of supressing. #![allow(dead_code, unused_variables)] mod components; -mod elements; mod elevation; pub mod prelude; pub mod settings; mod static_data; +mod to_extract; pub mod utils; pub use components::*; -pub use elements::*; pub use prelude::*; pub use static_data::*; +pub use to_extract::*; // This needs to be fully qualified with `crate::` otherwise we get a panic // at: diff --git a/crates/ui2/src/prelude.rs b/crates/ui2/src/prelude.rs index fbb7ccc528..072ed00060 100644 --- a/crates/ui2/src/prelude.rs +++ b/crates/ui2/src/prelude.rs @@ -10,24 +10,6 @@ pub use theme2::ActiveTheme; use gpui2::Hsla; use strum::EnumIter; -/// Represents a person with a Zed account's public profile. -/// All data in this struct should be considered public. -pub struct PublicActor { - pub username: SharedString, - pub avatar: SharedString, - pub is_contact: bool, -} - -impl PublicActor { - pub fn new(username: impl Into, avatar: impl Into) -> Self { - Self { - username: username.into(), - avatar: avatar.into(), - is_contact: false, - } - } -} - #[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)] pub enum FileSystemStatus { #[default] @@ -172,68 +154,10 @@ impl InteractionState { } } -#[derive(Default, PartialEq)] -pub enum SelectedState { +#[derive(Debug, Default, PartialEq, Eq, Hash, Clone, Copy)] +pub enum Selected { #[default] Unselected, - PartiallySelected, + Indeterminate, Selected, } - -#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] -pub enum Toggleable { - Toggleable(ToggleState), - #[default] - NotToggleable, -} - -impl Toggleable { - pub fn is_toggled(&self) -> bool { - match self { - Self::Toggleable(ToggleState::Toggled) => true, - _ => false, - } - } -} - -impl From for Toggleable { - fn from(state: ToggleState) -> Self { - Self::Toggleable(state) - } -} - -#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] -pub enum ToggleState { - /// The "on" state of a toggleable element. - /// - /// Example: - /// - A collasable list that is currently expanded - /// - A toggle button that is currently on. - Toggled, - /// The "off" state of a toggleable element. - /// - /// Example: - /// - A collasable list that is currently collapsed - /// - A toggle button that is currently off. - #[default] - NotToggled, -} - -impl From for ToggleState { - fn from(toggleable: Toggleable) -> Self { - match toggleable { - Toggleable::Toggleable(state) => state, - Toggleable::NotToggleable => ToggleState::NotToggled, - } - } -} - -impl From for ToggleState { - fn from(toggled: bool) -> Self { - if toggled { - ToggleState::Toggled - } else { - ToggleState::NotToggled - } - } -} diff --git a/crates/ui2/src/static_data.rs b/crates/ui2/src/static_data.rs index 68f625c75d..5342e1fb16 100644 --- a/crates/ui2/src/static_data.rs +++ b/crates/ui2/src/static_data.rs @@ -7,13 +7,13 @@ use gpui2::{AppContext, ViewContext}; use rand::Rng; use theme2::ActiveTheme; +use crate::HighlightedText; use crate::{ Buffer, BufferRow, BufferRows, Button, EditorPane, FileSystemStatus, GitStatus, - HighlightedLine, Icon, Keybinding, Label, LabelColor, ListEntry, ListEntrySize, ListSubHeader, - Livestream, MicStatus, ModifierKeys, Notification, PaletteItem, Player, PlayerCallStatus, - PlayerWithCallStatus, PublicActor, ScreenShareStatus, Symbol, Tab, ToggleState, VideoStatus, + HighlightedLine, Icon, Keybinding, Label, LabelColor, ListEntry, ListEntrySize, Livestream, + MicStatus, ModifierKeys, Notification, PaletteItem, Player, PlayerCallStatus, + PlayerWithCallStatus, PublicPlayer, ScreenShareStatus, Symbol, Tab, Toggle, VideoStatus, }; -use crate::{HighlightedText, ListDetailsEntry}; use crate::{ListItem, NotificationAction}; pub fn static_tabs_example() -> Vec { @@ -345,7 +345,7 @@ pub fn static_new_notification_items_2() -> Vec> { DateTime::parse_from_rfc3339("2023-11-02T12:09:07Z") .unwrap() .naive_local(), - PublicActor::new("as-cii", "http://github.com/as-cii.png?s=50"), + PublicPlayer::new("as-cii", "http://github.com/as-cii.png?s=50"), [ NotificationAction::new( Button::new("Decline"), @@ -374,7 +374,7 @@ pub fn static_new_notification_items_2() -> Vec> { DateTime::parse_from_rfc3339("2023-11-01T12:09:07Z") .unwrap() .naive_local(), - PublicActor::new("as-cii", "http://github.com/as-cii.png?s=50"), + PublicPlayer::new("as-cii", "http://github.com/as-cii.png?s=50"), [ NotificationAction::new( Button::new("Decline"), @@ -403,7 +403,7 @@ pub fn static_new_notification_items_2() -> Vec> { DateTime::parse_from_rfc3339("2022-10-25T12:09:07Z") .unwrap() .naive_local(), - PublicActor::new("as-cii", "http://github.com/as-cii.png?s=50"), + PublicPlayer::new("as-cii", "http://github.com/as-cii.png?s=50"), [ NotificationAction::new( Button::new("Decline"), @@ -432,7 +432,7 @@ pub fn static_new_notification_items_2() -> Vec> { DateTime::parse_from_rfc3339("2021-10-12T12:09:07Z") .unwrap() .naive_local(), - PublicActor::new("as-cii", "http://github.com/as-cii.png?s=50"), + PublicPlayer::new("as-cii", "http://github.com/as-cii.png?s=50"), [ NotificationAction::new( Button::new("Decline"), @@ -461,7 +461,7 @@ pub fn static_new_notification_items_2() -> Vec> { DateTime::parse_from_rfc3339("1969-07-20T00:00:00Z") .unwrap() .naive_local(), - PublicActor::new("as-cii", "http://github.com/as-cii.png?s=50"), + PublicPlayer::new("as-cii", "http://github.com/as-cii.png?s=50"), [ NotificationAction::new( Button::new("Decline"), @@ -478,89 +478,12 @@ pub fn static_new_notification_items_2() -> Vec> { ] } -pub fn static_new_notification_items() -> Vec> { - vec![ - ListItem::Header(ListSubHeader::new("New")), - ListItem::Details( - ListDetailsEntry::new("maxdeviant invited you to join a stream in #design.") - .meta("4 people in stream."), - ), - ListItem::Details(ListDetailsEntry::new( - "nathansobo accepted your contact request.", - )), - ListItem::Header(ListSubHeader::new("Earlier")), - ListItem::Details( - ListDetailsEntry::new("mikaylamaki added you as a contact.").actions(vec![ - Button::new("Decline"), - Button::new("Accept").variant(crate::ButtonVariant::Filled), - ]), - ), - ListItem::Details( - ListDetailsEntry::new("maxdeviant invited you to a stream in #design.") - .seen(true) - .meta("This stream has ended."), - ), - ListItem::Details(ListDetailsEntry::new( - "as-cii accepted your contact request.", - )), - ListItem::Details( - ListDetailsEntry::new("You were added as an admin on the #gpui2 channel.").seen(true), - ), - ListItem::Details(ListDetailsEntry::new( - "osiewicz accepted your contact request.", - )), - ListItem::Details(ListDetailsEntry::new( - "ConradIrwin accepted your contact request.", - )), - ListItem::Details( - ListDetailsEntry::new("nathansobo invited you to a stream in #gpui2.") - .seen(true) - .meta("This stream has ended."), - ), - ListItem::Details(ListDetailsEntry::new( - "nathansobo accepted your contact request.", - )), - ListItem::Header(ListSubHeader::new("Earlier")), - ListItem::Details( - ListDetailsEntry::new("mikaylamaki added you as a contact.").actions(vec![ - Button::new("Decline"), - Button::new("Accept").variant(crate::ButtonVariant::Filled), - ]), - ), - ListItem::Details( - ListDetailsEntry::new("maxdeviant invited you to a stream in #design.") - .seen(true) - .meta("This stream has ended."), - ), - ListItem::Details(ListDetailsEntry::new( - "as-cii accepted your contact request.", - )), - ListItem::Details( - ListDetailsEntry::new("You were added as an admin on the #gpui2 channel.").seen(true), - ), - ListItem::Details(ListDetailsEntry::new( - "osiewicz accepted your contact request.", - )), - ListItem::Details(ListDetailsEntry::new( - "ConradIrwin accepted your contact request.", - )), - ListItem::Details( - ListDetailsEntry::new("nathansobo invited you to a stream in #gpui2.") - .seen(true) - .meta("This stream has ended."), - ), - ] - .into_iter() - .map(From::from) - .collect() -} - -pub fn static_project_panel_project_items() -> Vec> { +pub fn static_project_panel_project_items() -> Vec { vec![ ListEntry::new(Label::new("zed")) .left_icon(Icon::FolderOpen.into()) .indent_level(0) - .toggle(ToggleState::Toggled), + .toggle(Toggle::Toggled(true)), ListEntry::new(Label::new(".cargo")) .left_icon(Icon::Folder.into()) .indent_level(1), @@ -579,14 +502,14 @@ pub fn static_project_panel_project_items() -> Vec> { ListEntry::new(Label::new("assets")) .left_icon(Icon::Folder.into()) .indent_level(1) - .toggle(ToggleState::Toggled), + .toggle(Toggle::Toggled(true)), ListEntry::new(Label::new("cargo-target").color(LabelColor::Hidden)) .left_icon(Icon::Folder.into()) .indent_level(1), ListEntry::new(Label::new("crates")) .left_icon(Icon::FolderOpen.into()) .indent_level(1) - .toggle(ToggleState::Toggled), + .toggle(Toggle::Toggled(true)), ListEntry::new(Label::new("activity_indicator")) .left_icon(Icon::Folder.into()) .indent_level(2), @@ -608,38 +531,38 @@ pub fn static_project_panel_project_items() -> Vec> { ListEntry::new(Label::new("sqlez").color(LabelColor::Modified)) .left_icon(Icon::Folder.into()) .indent_level(2) - .toggle(ToggleState::NotToggled), + .toggle(Toggle::Toggled(false)), ListEntry::new(Label::new("gpui2")) .left_icon(Icon::FolderOpen.into()) .indent_level(2) - .toggle(ToggleState::Toggled), + .toggle(Toggle::Toggled(true)), ListEntry::new(Label::new("src")) .left_icon(Icon::FolderOpen.into()) .indent_level(3) - .toggle(ToggleState::Toggled), + .toggle(Toggle::Toggled(true)), ListEntry::new(Label::new("derive_element.rs")) .left_icon(Icon::FileRust.into()) .indent_level(4), ListEntry::new(Label::new("storybook").color(LabelColor::Modified)) .left_icon(Icon::FolderOpen.into()) .indent_level(1) - .toggle(ToggleState::Toggled), + .toggle(Toggle::Toggled(true)), ListEntry::new(Label::new("docs").color(LabelColor::Default)) .left_icon(Icon::Folder.into()) .indent_level(2) - .toggle(ToggleState::Toggled), + .toggle(Toggle::Toggled(true)), ListEntry::new(Label::new("src").color(LabelColor::Modified)) .left_icon(Icon::FolderOpen.into()) .indent_level(3) - .toggle(ToggleState::Toggled), + .toggle(Toggle::Toggled(true)), ListEntry::new(Label::new("ui").color(LabelColor::Modified)) .left_icon(Icon::FolderOpen.into()) .indent_level(4) - .toggle(ToggleState::Toggled), + .toggle(Toggle::Toggled(true)), ListEntry::new(Label::new("component").color(LabelColor::Created)) .left_icon(Icon::FolderOpen.into()) .indent_level(5) - .toggle(ToggleState::Toggled), + .toggle(Toggle::Toggled(true)), ListEntry::new(Label::new("facepile.rs").color(LabelColor::Default)) .left_icon(Icon::FileRust.into()) .indent_level(6), @@ -682,7 +605,7 @@ pub fn static_project_panel_project_items() -> Vec> { .collect() } -pub fn static_project_panel_single_items() -> Vec> { +pub fn static_project_panel_single_items() -> Vec { vec![ ListEntry::new(Label::new("todo.md")) .left_icon(Icon::FileDoc.into()) @@ -699,7 +622,7 @@ pub fn static_project_panel_single_items() -> Vec> { .collect() } -pub fn static_collab_panel_current_call() -> Vec> { +pub fn static_collab_panel_current_call() -> Vec { vec![ ListEntry::new(Label::new("as-cii")).left_avatar("http://github.com/as-cii.png?s=50"), ListEntry::new(Label::new("nathansobo")) @@ -712,7 +635,7 @@ pub fn static_collab_panel_current_call() -> Vec> { .collect() } -pub fn static_collab_panel_channels() -> Vec> { +pub fn static_collab_panel_channels() -> Vec { vec![ ListEntry::new(Label::new("zed")) .left_icon(Icon::Hash.into()) diff --git a/crates/ui2/src/to_extract.rs b/crates/ui2/src/to_extract.rs new file mode 100644 index 0000000000..e786dd0f7e --- /dev/null +++ b/crates/ui2/src/to_extract.rs @@ -0,0 +1,47 @@ +mod assistant_panel; +mod breadcrumb; +mod buffer; +mod buffer_search; +mod chat_panel; +mod collab_panel; +mod command_palette; +mod copilot; +mod editor_pane; +mod language_selector; +mod multi_buffer; +mod notifications_panel; +mod panes; +mod project_panel; +mod recent_projects; +mod status_bar; +mod tab_bar; +mod terminal; +mod theme_selector; +mod title_bar; +mod toolbar; +mod traffic_lights; +mod workspace; + +pub use assistant_panel::*; +pub use breadcrumb::*; +pub use buffer::*; +pub use buffer_search::*; +pub use chat_panel::*; +pub use collab_panel::*; +pub use command_palette::*; +pub use copilot::*; +pub use editor_pane::*; +pub use language_selector::*; +pub use multi_buffer::*; +pub use notifications_panel::*; +pub use panes::*; +pub use project_panel::*; +pub use recent_projects::*; +pub use status_bar::*; +pub use tab_bar::*; +pub use terminal::*; +pub use theme_selector::*; +pub use title_bar::*; +pub use toolbar::*; +pub use traffic_lights::*; +pub use workspace::*; diff --git a/crates/ui2/src/components/assistant_panel.rs b/crates/ui2/src/to_extract/assistant_panel.rs similarity index 100% rename from crates/ui2/src/components/assistant_panel.rs rename to crates/ui2/src/to_extract/assistant_panel.rs diff --git a/crates/ui2/src/components/breadcrumb.rs b/crates/ui2/src/to_extract/breadcrumb.rs similarity index 100% rename from crates/ui2/src/components/breadcrumb.rs rename to crates/ui2/src/to_extract/breadcrumb.rs diff --git a/crates/ui2/src/components/buffer.rs b/crates/ui2/src/to_extract/buffer.rs similarity index 99% rename from crates/ui2/src/components/buffer.rs rename to crates/ui2/src/to_extract/buffer.rs index 2b3db676ce..e12beff2fc 100644 --- a/crates/ui2/src/components/buffer.rs +++ b/crates/ui2/src/to_extract/buffer.rs @@ -220,7 +220,7 @@ impl Buffer { .flex_1() .w_full() .h_full() - .bg(cx.theme().colors().editor) + .bg(cx.theme().colors().editor_background) .children(rows) } } diff --git a/crates/ui2/src/components/buffer_search.rs b/crates/ui2/src/to_extract/buffer_search.rs similarity index 57% rename from crates/ui2/src/components/buffer_search.rs rename to crates/ui2/src/to_extract/buffer_search.rs index 5d7de1b408..02f689ca3e 100644 --- a/crates/ui2/src/components/buffer_search.rs +++ b/crates/ui2/src/to_extract/buffer_search.rs @@ -30,14 +30,17 @@ impl Render for BufferSearch { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Div { - h_stack().bg(cx.theme().colors().toolbar).p_2().child( - h_stack().child(Input::new("Search")).child( - IconButton::::new("replace", Icon::Replace) - .when(self.is_replace_open, |this| this.color(IconColor::Accent)) - .on_click(|buffer_search, cx| { - buffer_search.toggle_replace(cx); - }), - ), - ) + h_stack() + .bg(cx.theme().colors().toolbar_background) + .p_2() + .child( + h_stack().child(Input::new("Search")).child( + IconButton::::new("replace", Icon::Replace) + .when(self.is_replace_open, |this| this.color(IconColor::Accent)) + .on_click(|buffer_search, cx| { + buffer_search.toggle_replace(cx); + }), + ), + ) } } diff --git a/crates/ui2/src/components/chat_panel.rs b/crates/ui2/src/to_extract/chat_panel.rs similarity index 100% rename from crates/ui2/src/components/chat_panel.rs rename to crates/ui2/src/to_extract/chat_panel.rs diff --git a/crates/ui2/src/components/collab_panel.rs b/crates/ui2/src/to_extract/collab_panel.rs similarity index 84% rename from crates/ui2/src/components/collab_panel.rs rename to crates/ui2/src/to_extract/collab_panel.rs index a0e3b55f63..9d9dc861e2 100644 --- a/crates/ui2/src/components/collab_panel.rs +++ b/crates/ui2/src/to_extract/collab_panel.rs @@ -1,7 +1,6 @@ -use crate::prelude::*; +use crate::{prelude::*, Toggle}; use crate::{ - static_collab_panel_channels, static_collab_panel_current_call, v_stack, Icon, List, - ListHeader, ToggleState, + static_collab_panel_channels, static_collab_panel_current_call, v_stack, Icon, List, ListHeader, }; #[derive(Component)] @@ -18,7 +17,7 @@ impl CollabPanel { v_stack() .id(self.id.clone()) .h_full() - .bg(cx.theme().colors().surface) + .bg(cx.theme().colors().surface_background) .child( v_stack() .id("crdb") @@ -34,17 +33,17 @@ impl CollabPanel { .header( ListHeader::new("CRDB") .left_icon(Icon::Hash.into()) - .toggle(ToggleState::Toggled), + .toggle(Toggle::Toggled(true)), ) - .toggle(ToggleState::Toggled), + .toggle(Toggle::Toggled(true)), ), ) .child( v_stack().id("channels").py_1().child( List::new(static_collab_panel_channels()) - .header(ListHeader::new("CHANNELS").toggle(ToggleState::Toggled)) + .header(ListHeader::new("CHANNELS").toggle(Toggle::Toggled(true))) .empty_message("No channels yet. Add a channel to get started.") - .toggle(ToggleState::Toggled), + .toggle(Toggle::Toggled(true)), ), ) .child( @@ -52,9 +51,9 @@ impl CollabPanel { List::new(static_collab_panel_current_call()) .header( ListHeader::new("CONTACTS – ONLINE") - .toggle(ToggleState::Toggled), + .toggle(Toggle::Toggled(true)), ) - .toggle(ToggleState::Toggled), + .toggle(Toggle::Toggled(true)), ), ) .child( @@ -62,9 +61,9 @@ impl CollabPanel { List::new(static_collab_panel_current_call()) .header( ListHeader::new("CONTACTS – OFFLINE") - .toggle(ToggleState::NotToggled), + .toggle(Toggle::Toggled(false)), ) - .toggle(ToggleState::NotToggled), + .toggle(Toggle::Toggled(false)), ), ), ) diff --git a/crates/ui2/src/components/command_palette.rs b/crates/ui2/src/to_extract/command_palette.rs similarity index 100% rename from crates/ui2/src/components/command_palette.rs rename to crates/ui2/src/to_extract/command_palette.rs diff --git a/crates/ui2/src/components/copilot.rs b/crates/ui2/src/to_extract/copilot.rs similarity index 100% rename from crates/ui2/src/components/copilot.rs rename to crates/ui2/src/to_extract/copilot.rs diff --git a/crates/ui2/src/components/editor_pane.rs b/crates/ui2/src/to_extract/editor_pane.rs similarity index 100% rename from crates/ui2/src/components/editor_pane.rs rename to crates/ui2/src/to_extract/editor_pane.rs diff --git a/crates/ui2/src/components/language_selector.rs b/crates/ui2/src/to_extract/language_selector.rs similarity index 100% rename from crates/ui2/src/components/language_selector.rs rename to crates/ui2/src/to_extract/language_selector.rs diff --git a/crates/ui2/src/components/multi_buffer.rs b/crates/ui2/src/to_extract/multi_buffer.rs similarity index 99% rename from crates/ui2/src/components/multi_buffer.rs rename to crates/ui2/src/to_extract/multi_buffer.rs index ea130f20bd..1334703015 100644 --- a/crates/ui2/src/components/multi_buffer.rs +++ b/crates/ui2/src/to_extract/multi_buffer.rs @@ -24,7 +24,7 @@ impl MultiBuffer { .items_center() .justify_between() .p_4() - .bg(cx.theme().colors().editor_subheader) + .bg(cx.theme().colors().editor_subheader_background) .child(Label::new("main.rs")) .child(IconButton::new("arrow_up_right", Icon::ArrowUpRight)), ) diff --git a/crates/ui2/src/components/notifications_panel.rs b/crates/ui2/src/to_extract/notifications_panel.rs similarity index 94% rename from crates/ui2/src/components/notifications_panel.rs rename to crates/ui2/src/to_extract/notifications_panel.rs index 74f015ac06..84794b71b2 100644 --- a/crates/ui2/src/components/notifications_panel.rs +++ b/crates/ui2/src/to_extract/notifications_panel.rs @@ -1,8 +1,8 @@ use crate::utils::naive_format_distance_from_now; use crate::{ - h_stack, prelude::*, static_new_notification_items_2, v_stack, Avatar, Button, Icon, - IconButton, IconElement, Label, LabelColor, LineHeightStyle, ListHeaderMeta, ListSeparator, - UnreadIndicator, + h_stack, prelude::*, static_new_notification_items_2, v_stack, Avatar, ButtonOrIconButton, + Icon, IconElement, Label, LabelColor, LineHeightStyle, ListHeaderMeta, ListSeparator, + PublicPlayer, UnreadIndicator, }; use crate::{ClickHandler, ListHeader}; @@ -22,7 +22,7 @@ impl NotificationsPanel { .flex() .flex_col() .size_full() - .bg(cx.theme().colors().surface) + .bg(cx.theme().colors().surface_background) .child( ListHeader::new("Notifications").meta(Some(ListHeaderMeta::Tools(vec![ Icon::AtSign, @@ -43,7 +43,7 @@ impl NotificationsPanel { .p_1() // TODO: Add cursor style // .cursor(Cursor::IBeam) - .bg(cx.theme().colors().element) + .bg(cx.theme().colors().element_background) .border() .border_color(cx.theme().colors().border_variant) .child( @@ -57,23 +57,6 @@ impl NotificationsPanel { } } -pub enum ButtonOrIconButton { - Button(Button), - IconButton(IconButton), -} - -impl From> for ButtonOrIconButton { - fn from(value: Button) -> Self { - Self::Button(value) - } -} - -impl From> for ButtonOrIconButton { - fn from(value: IconButton) -> Self { - Self::IconButton(value) - } -} - pub struct NotificationAction { button: ButtonOrIconButton, tooltip: SharedString, @@ -102,7 +85,7 @@ impl NotificationAction { } pub enum ActorOrIcon { - Actor(PublicActor), + Actor(PublicPlayer), Icon(Icon), } @@ -171,7 +154,7 @@ impl Notification { id: impl Into, message: impl Into, date_received: NaiveDateTime, - actor: PublicActor, + actor: PublicPlayer, click_action: ClickHandler, ) -> Self { Self::new( @@ -210,7 +193,7 @@ impl Notification { id: impl Into, message: impl Into, date_received: NaiveDateTime, - actor: PublicActor, + actor: PublicPlayer, actions: [NotificationAction; 2], ) -> Self { Self::new( diff --git a/crates/ui2/src/components/panes.rs b/crates/ui2/src/to_extract/panes.rs similarity index 98% rename from crates/ui2/src/components/panes.rs rename to crates/ui2/src/to_extract/panes.rs index bf0f27d43f..a1e040b0ca 100644 --- a/crates/ui2/src/components/panes.rs +++ b/crates/ui2/src/to_extract/panes.rs @@ -113,7 +113,7 @@ impl PaneGroup { .gap_px() .w_full() .h_full() - .bg(cx.theme().colors().editor) + .bg(cx.theme().colors().editor_background) .children(self.groups.into_iter().map(|group| group.render(view, cx))); if self.split_direction == SplitDirection::Horizontal { diff --git a/crates/ui2/src/components/project_panel.rs b/crates/ui2/src/to_extract/project_panel.rs similarity index 83% rename from crates/ui2/src/components/project_panel.rs rename to crates/ui2/src/to_extract/project_panel.rs index 76fa50d338..a34a30bcbc 100644 --- a/crates/ui2/src/components/project_panel.rs +++ b/crates/ui2/src/to_extract/project_panel.rs @@ -18,9 +18,8 @@ impl ProjectPanel { .id(self.id.clone()) .flex() .flex_col() - .w_full() - .h_full() - .bg(cx.theme().colors().surface) + .size_full() + .bg(cx.theme().colors().surface_background) .child( div() .id("project-panel-contents") @@ -30,15 +29,13 @@ impl ProjectPanel { .overflow_y_scroll() .child( List::new(static_project_panel_single_items()) - .header(ListHeader::new("FILES").toggle(ToggleState::Toggled)) - .empty_message("No files in directory") - .toggle(ToggleState::Toggled), + .header(ListHeader::new("FILES")) + .empty_message("No files in directory"), ) .child( List::new(static_project_panel_project_items()) - .header(ListHeader::new("PROJECT").toggle(ToggleState::Toggled)) - .empty_message("No folders in directory") - .toggle(ToggleState::Toggled), + .header(ListHeader::new("PROJECT")) + .empty_message("No folders in directory"), ), ) .child( diff --git a/crates/ui2/src/components/recent_projects.rs b/crates/ui2/src/to_extract/recent_projects.rs similarity index 100% rename from crates/ui2/src/components/recent_projects.rs rename to crates/ui2/src/to_extract/recent_projects.rs diff --git a/crates/ui2/src/components/status_bar.rs b/crates/ui2/src/to_extract/status_bar.rs similarity index 99% rename from crates/ui2/src/components/status_bar.rs rename to crates/ui2/src/to_extract/status_bar.rs index 136472f605..34a5993e69 100644 --- a/crates/ui2/src/components/status_bar.rs +++ b/crates/ui2/src/to_extract/status_bar.rs @@ -93,7 +93,7 @@ impl StatusBar { .items_center() .justify_between() .w_full() - .bg(cx.theme().colors().status_bar) + .bg(cx.theme().colors().status_bar_background) .child(self.left_tools(view, cx)) .child(self.right_tools(view, cx)) } diff --git a/crates/ui2/src/components/tab_bar.rs b/crates/ui2/src/to_extract/tab_bar.rs similarity index 98% rename from crates/ui2/src/components/tab_bar.rs rename to crates/ui2/src/to_extract/tab_bar.rs index bb7fca1153..a128044183 100644 --- a/crates/ui2/src/components/tab_bar.rs +++ b/crates/ui2/src/to_extract/tab_bar.rs @@ -31,7 +31,7 @@ impl TabBar { .id(self.id.clone()) .w_full() .flex() - .bg(cx.theme().colors().tab_bar) + .bg(cx.theme().colors().tab_bar_background) // Left Side .child( div() diff --git a/crates/ui2/src/components/terminal.rs b/crates/ui2/src/to_extract/terminal.rs similarity index 98% rename from crates/ui2/src/components/terminal.rs rename to crates/ui2/src/to_extract/terminal.rs index 051ebf7315..b912a59607 100644 --- a/crates/ui2/src/components/terminal.rs +++ b/crates/ui2/src/to_extract/terminal.rs @@ -24,7 +24,7 @@ impl Terminal { div() .w_full() .flex() - .bg(cx.theme().colors().surface) + .bg(cx.theme().colors().surface_background) .child( div().px_1().flex().flex_none().gap_2().child( div() diff --git a/crates/ui2/src/components/theme_selector.rs b/crates/ui2/src/to_extract/theme_selector.rs similarity index 100% rename from crates/ui2/src/components/theme_selector.rs rename to crates/ui2/src/to_extract/theme_selector.rs diff --git a/crates/ui2/src/components/title_bar.rs b/crates/ui2/src/to_extract/title_bar.rs similarity index 100% rename from crates/ui2/src/components/title_bar.rs rename to crates/ui2/src/to_extract/title_bar.rs diff --git a/crates/ui2/src/components/toolbar.rs b/crates/ui2/src/to_extract/toolbar.rs similarity index 98% rename from crates/ui2/src/components/toolbar.rs rename to crates/ui2/src/to_extract/toolbar.rs index 05a5c991d6..0e3e7c259f 100644 --- a/crates/ui2/src/components/toolbar.rs +++ b/crates/ui2/src/to_extract/toolbar.rs @@ -56,7 +56,7 @@ impl Toolbar { fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { div() - .bg(cx.theme().colors().toolbar) + .bg(cx.theme().colors().toolbar_background) .p_2() .flex() .justify_between() diff --git a/crates/ui2/src/components/traffic_lights.rs b/crates/ui2/src/to_extract/traffic_lights.rs similarity index 97% rename from crates/ui2/src/components/traffic_lights.rs rename to crates/ui2/src/to_extract/traffic_lights.rs index 9080276cdd..677fae886c 100644 --- a/crates/ui2/src/components/traffic_lights.rs +++ b/crates/ui2/src/to_extract/traffic_lights.rs @@ -28,7 +28,7 @@ impl TrafficLight { (true, TrafficLightColor::Red) => system_colors.mac_os_traffic_light_red, (true, TrafficLightColor::Yellow) => system_colors.mac_os_traffic_light_yellow, (true, TrafficLightColor::Green) => system_colors.mac_os_traffic_light_green, - (false, _) => cx.theme().colors().element, + (false, _) => cx.theme().colors().element_background, }; div().w_3().h_3().rounded_full().bg(fill) diff --git a/crates/ui2/src/components/workspace.rs b/crates/ui2/src/to_extract/workspace.rs similarity index 100% rename from crates/ui2/src/components/workspace.rs rename to crates/ui2/src/to_extract/workspace.rs diff --git a/crates/workspace2/src/pane.rs b/crates/workspace2/src/pane.rs index 64937214c9..5af5514da4 100644 --- a/crates/workspace2/src/pane.rs +++ b/crates/workspace2/src/pane.rs @@ -1377,13 +1377,13 @@ impl Pane { let (text_color, tab_bg, tab_hover_bg, tab_active_bg) = match ix == self.active_item_index { false => ( cx.theme().colors().text_muted, - cx.theme().colors().tab_inactive, + cx.theme().colors().tab_inactive_background, cx.theme().colors().ghost_element_hover, cx.theme().colors().ghost_element_active, ), true => ( cx.theme().colors().text, - cx.theme().colors().tab_active, + cx.theme().colors().tab_active_background, cx.theme().colors().element_hover, cx.theme().colors().element_active, ), @@ -1453,7 +1453,7 @@ impl Pane { .id("tab_bar") .w_full() .flex() - .bg(cx.theme().colors().tab_bar) + .bg(cx.theme().colors().tab_bar_background) // Left Side .child( div() diff --git a/crates/workspace2/src/status_bar.rs b/crates/workspace2/src/status_bar.rs index 4f7ba02c2a..fcf6ac3b61 100644 --- a/crates/workspace2/src/status_bar.rs +++ b/crates/workspace2/src/status_bar.rs @@ -45,7 +45,7 @@ impl Render for StatusBar { .justify_between() .w_full() .h_8() - .bg(cx.theme().colors().status_bar) + .bg(cx.theme().colors().status_bar_background) .child(self.render_left_tools(cx)) .child(self.render_right_tools(cx)) } diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index f7e9a0d585..7561c903d3 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -544,6 +544,7 @@ impl DelayedDebouncedEditAction { pub enum Event { PaneAdded(View), ContactRequestedJoin(u64), + WorkspaceCreated(WeakView), } pub struct Workspace { @@ -698,8 +699,7 @@ impl Workspace { Ok(()) }); - // todo!("replace with a different mechanism") - // cx.emit_global(WorkspaceCreated(weak_handle.clone())); + cx.emit(Event::WorkspaceCreated(weak_handle.clone())); let left_dock = cx.build_view(|_| Dock::new(DockPosition::Left)); let bottom_dock = cx.build_view(|_| Dock::new(DockPosition::Bottom)); @@ -2697,7 +2697,7 @@ impl Workspace { fn render_titlebar(&self, cx: &mut ViewContext) -> impl Component { div() - .bg(cx.theme().colors().title_bar) + .bg(cx.theme().colors().title_bar_background) .when( !matches!(cx.window_bounds(), WindowBounds::Fullscreen), |s| s.pl_20(), @@ -4253,7 +4253,7 @@ impl ViewId { // } // } -// pub struct WorkspaceCreated(pub WeakView); +pub struct WorkspaceCreated(pub WeakView); pub fn activate_workspace_for_project( cx: &mut AppContext, diff --git a/crates/zed/src/languages/elixir.rs b/crates/zed/src/languages/elixir.rs index df438d89ee..e2c79570bc 100644 --- a/crates/zed/src/languages/elixir.rs +++ b/crates/zed/src/languages/elixir.rs @@ -140,8 +140,8 @@ impl LspAdapter for ElixirLspAdapter { ) -> Result { let version = version.downcast::().unwrap(); let zip_path = container_dir.join(format!("elixir-ls_{}.zip", version.name)); - let version_dir = container_dir.join(format!("elixir-ls_{}", version.name)); - let binary_path = version_dir.join("language_server.sh"); + let folder_path = container_dir.join("elixir-ls"); + let binary_path = folder_path.join("language_server.sh"); if fs::metadata(&binary_path).await.is_err() { let mut response = delegate @@ -160,13 +160,13 @@ impl LspAdapter for ElixirLspAdapter { } futures::io::copy(response.body_mut(), &mut file).await?; - fs::create_dir_all(&version_dir) + fs::create_dir_all(&folder_path) .await - .with_context(|| format!("failed to create directory {}", version_dir.display()))?; + .with_context(|| format!("failed to create directory {}", folder_path.display()))?; let unzip_status = smol::process::Command::new("unzip") .arg(&zip_path) .arg("-d") - .arg(&version_dir) + .arg(&folder_path) .output() .await? .status; @@ -174,7 +174,7 @@ impl LspAdapter for ElixirLspAdapter { Err(anyhow!("failed to unzip elixir-ls archive"))?; } - remove_matching(&container_dir, |entry| entry != version_dir).await; + remove_matching(&container_dir, |entry| entry != folder_path).await; } Ok(LanguageServerBinary { @@ -285,20 +285,16 @@ impl LspAdapter for ElixirLspAdapter { async fn get_cached_server_binary_elixir_ls( container_dir: PathBuf, ) -> Option { - (|| async move { - let mut last = None; - let mut entries = fs::read_dir(&container_dir).await?; - while let Some(entry) = entries.next().await { - last = Some(entry?.path()); - } - last.map(|path| LanguageServerBinary { - path, + let server_path = container_dir.join("elixir-ls/language_server.sh"); + if server_path.exists() { + Some(LanguageServerBinary { + path: server_path, arguments: vec![], }) - .ok_or_else(|| anyhow!("no cached binary")) - })() - .await - .log_err() + } else { + log::error!("missing executable in directory {:?}", server_path); + None + } } pub struct NextLspAdapter; diff --git a/crates/zed2/src/languages/elixir.rs b/crates/zed2/src/languages/elixir.rs index bd38377c99..90352c78b4 100644 --- a/crates/zed2/src/languages/elixir.rs +++ b/crates/zed2/src/languages/elixir.rs @@ -140,8 +140,8 @@ impl LspAdapter for ElixirLspAdapter { ) -> Result { let version = version.downcast::().unwrap(); let zip_path = container_dir.join(format!("elixir-ls_{}.zip", version.name)); - let version_dir = container_dir.join(format!("elixir-ls_{}", version.name)); - let binary_path = version_dir.join("language_server.sh"); + let folder_path = container_dir.join("elixir-ls"); + let binary_path = folder_path.join("language_server.sh"); if fs::metadata(&binary_path).await.is_err() { let mut response = delegate @@ -160,13 +160,13 @@ impl LspAdapter for ElixirLspAdapter { } futures::io::copy(response.body_mut(), &mut file).await?; - fs::create_dir_all(&version_dir) + fs::create_dir_all(&folder_path) .await - .with_context(|| format!("failed to create directory {}", version_dir.display()))?; + .with_context(|| format!("failed to create directory {}", folder_path.display()))?; let unzip_status = smol::process::Command::new("unzip") .arg(&zip_path) .arg("-d") - .arg(&version_dir) + .arg(&folder_path) .output() .await? .status; @@ -174,7 +174,7 @@ impl LspAdapter for ElixirLspAdapter { Err(anyhow!("failed to unzip elixir-ls archive"))?; } - remove_matching(&container_dir, |entry| entry != version_dir).await; + remove_matching(&container_dir, |entry| entry != folder_path).await; } Ok(LanguageServerBinary { @@ -285,20 +285,16 @@ impl LspAdapter for ElixirLspAdapter { async fn get_cached_server_binary_elixir_ls( container_dir: PathBuf, ) -> Option { - (|| async move { - let mut last = None; - let mut entries = fs::read_dir(&container_dir).await?; - while let Some(entry) = entries.next().await { - last = Some(entry?.path()); - } - last.map(|path| LanguageServerBinary { - path, + let server_path = container_dir.join("elixir-ls/language_server.sh"); + if server_path.exists() { + Some(LanguageServerBinary { + path: server_path, arguments: vec![], }) - .ok_or_else(|| anyhow!("no cached binary")) - })() - .await - .log_err() + } else { + log::error!("missing executable in directory {:?}", server_path); + None + } } pub struct NextLspAdapter; diff --git a/theme.txt b/theme.txt deleted file mode 100644 index 626d6b9fa9..0000000000 --- a/theme.txt +++ /dev/null @@ -1,254 +0,0 @@ - Finished dev [unoptimized + debuginfo] target(s) in 0.39s - Running `target/debug/storybook` -[crates/ui2/src/components/workspace.rs:182] color = [crates/ui2/src/color.rs:162] "ThemeColor debug" = "ThemeColor debug" -ThemeColor { - transparent: "rgba(0x00000000).into()", - mac_os_traffic_light_red: "rgba(0xec695eff).into()", - mac_os_traffic_light_yellow: "rgba(0xf4bf4eff).into()", - mac_os_traffic_light_green: "rgba(0x61c553ff).into()", - border: "rgba(0x464b57ff).into()", - border_variant: "rgba(0x464b57ff).into()", - border_focused: "rgba(0x293b5bff).into()", - border_transparent: "rgba(0x00000000).into()", - elevated_surface: "rgba(0x3b414dff).into()", - surface: "rgba(0x2f343eff).into()", - background: "rgba(0x3b414dff).into()", - filled_element: "rgba(0x3b414dff).into()", - filled_element_hover: "rgba(0xffffff1e).into()", - filled_element_active: "rgba(0xffffff28).into()", - filled_element_selected: "rgba(0x18243dff).into()", - filled_element_disabled: "rgba(0x00000000).into()", - ghost_element: "rgba(0x00000000).into()", - ghost_element_hover: "rgba(0xffffff14).into()", - ghost_element_active: "rgba(0xffffff1e).into()", - ghost_element_selected: "rgba(0x18243dff).into()", - ghost_element_disabled: "rgba(0x00000000).into()", - text: "rgba(0xc8ccd4ff).into()", - text_muted: "rgba(0x838994ff).into()", - text_placeholder: "rgba(0xd07277ff).into()", - text_disabled: "rgba(0x555a63ff).into()", - text_accent: "rgba(0x74ade8ff).into()", - icon_muted: "rgba(0x838994ff).into()", - syntax: SyntaxColor { - comment: "rgba(0x5d636fff).into()", - string: "rgba(0xa1c181ff).into()", - function: "rgba(0x73ade9ff).into()", - keyword: "rgba(0xb477cfff).into()", - }, - status_bar: "rgba(0x3b414dff).into()", - title_bar: "rgba(0x3b414dff).into()", - toolbar: "rgba(0x282c33ff).into()", - tab_bar: "rgba(0x2f343eff).into()", - editor_subheader: "rgba(0x2f343eff).into()", - editor_active_line: "rgba(0x2f343eff).into()", - terminal: "rgba(0x282c33ff).into()", - image_fallback_background: "rgba(0x3b414dff).into()", - git_created: "rgba(0xa1c181ff).into()", - git_modified: "rgba(0x74ade8ff).into()", - git_deleted: "rgba(0xd07277ff).into()", - git_conflict: "rgba(0xdec184ff).into()", - git_ignored: "rgba(0x555a63ff).into()", - git_renamed: "rgba(0xdec184ff).into()", - player: [ - PlayerThemeColors { - cursor: "rgba(0x74ade8ff).into()", - selection: "rgba(0x74ade83d).into()", - }, - PlayerThemeColors { - cursor: "rgba(0xa1c181ff).into()", - selection: "rgba(0xa1c1813d).into()", - }, - PlayerThemeColors { - cursor: "rgba(0xbe5046ff).into()", - selection: "rgba(0xbe50463d).into()", - }, - PlayerThemeColors { - cursor: "rgba(0xbf956aff).into()", - selection: "rgba(0xbf956a3d).into()", - }, - PlayerThemeColors { - cursor: "rgba(0xb477cfff).into()", - selection: "rgba(0xb477cf3d).into()", - }, - PlayerThemeColors { - cursor: "rgba(0x6eb4bfff).into()", - selection: "rgba(0x6eb4bf3d).into()", - }, - PlayerThemeColors { - cursor: "rgba(0xd07277ff).into()", - selection: "rgba(0xd072773d).into()", - }, - PlayerThemeColors { - cursor: "rgba(0xdec184ff).into()", - selection: "rgba(0xdec1843d).into()", - }, - ], -} -[crates/ui2/src/components/workspace.rs:182] color = [crates/ui2/src/color.rs:162] "ThemeColor debug" = "ThemeColor debug" -ThemeColor { - transparent: "rgba(0x00000000).into()", - mac_os_traffic_light_red: "rgba(0xec695eff).into()", - mac_os_traffic_light_yellow: "rgba(0xf4bf4eff).into()", - mac_os_traffic_light_green: "rgba(0x61c553ff).into()", - border: "rgba(0x464b57ff).into()", - border_variant: "rgba(0x464b57ff).into()", - border_focused: "rgba(0x293b5bff).into()", - border_transparent: "rgba(0x00000000).into()", - elevated_surface: "rgba(0x3b414dff).into()", - surface: "rgba(0x2f343eff).into()", - background: "rgba(0x3b414dff).into()", - filled_element: "rgba(0x3b414dff).into()", - filled_element_hover: "rgba(0xffffff1e).into()", - filled_element_active: "rgba(0xffffff28).into()", - filled_element_selected: "rgba(0x18243dff).into()", - filled_element_disabled: "rgba(0x00000000).into()", - ghost_element: "rgba(0x00000000).into()", - ghost_element_hover: "rgba(0xffffff14).into()", - ghost_element_active: "rgba(0xffffff1e).into()", - ghost_element_selected: "rgba(0x18243dff).into()", - ghost_element_disabled: "rgba(0x00000000).into()", - text: "rgba(0xc8ccd4ff).into()", - text_muted: "rgba(0x838994ff).into()", - text_placeholder: "rgba(0xd07277ff).into()", - text_disabled: "rgba(0x555a63ff).into()", - text_accent: "rgba(0x74ade8ff).into()", - icon_muted: "rgba(0x838994ff).into()", - syntax: SyntaxColor { - comment: "rgba(0x5d636fff).into()", - string: "rgba(0xa1c181ff).into()", - function: "rgba(0x73ade9ff).into()", - keyword: "rgba(0xb477cfff).into()", - }, - status_bar: "rgba(0x3b414dff).into()", - title_bar: "rgba(0x3b414dff).into()", - toolbar: "rgba(0x282c33ff).into()", - tab_bar: "rgba(0x2f343eff).into()", - editor_subheader: "rgba(0x2f343eff).into()", - editor_active_line: "rgba(0x2f343eff).into()", - terminal: "rgba(0x282c33ff).into()", - image_fallback_background: "rgba(0x3b414dff).into()", - git_created: "rgba(0xa1c181ff).into()", - git_modified: "rgba(0x74ade8ff).into()", - git_deleted: "rgba(0xd07277ff).into()", - git_conflict: "rgba(0xdec184ff).into()", - git_ignored: "rgba(0x555a63ff).into()", - git_renamed: "rgba(0xdec184ff).into()", - player: [ - PlayerThemeColors { - cursor: "rgba(0x74ade8ff).into()", - selection: "rgba(0x74ade83d).into()", - }, - PlayerThemeColors { - cursor: "rgba(0xa1c181ff).into()", - selection: "rgba(0xa1c1813d).into()", - }, - PlayerThemeColors { - cursor: "rgba(0xbe5046ff).into()", - selection: "rgba(0xbe50463d).into()", - }, - PlayerThemeColors { - cursor: "rgba(0xbf956aff).into()", - selection: "rgba(0xbf956a3d).into()", - }, - PlayerThemeColors { - cursor: "rgba(0xb477cfff).into()", - selection: "rgba(0xb477cf3d).into()", - }, - PlayerThemeColors { - cursor: "rgba(0x6eb4bfff).into()", - selection: "rgba(0x6eb4bf3d).into()", - }, - PlayerThemeColors { - cursor: "rgba(0xd07277ff).into()", - selection: "rgba(0xd072773d).into()", - }, - PlayerThemeColors { - cursor: "rgba(0xdec184ff).into()", - selection: "rgba(0xdec1843d).into()", - }, - ], -} -[crates/ui2/src/components/workspace.rs:182] color = [crates/ui2/src/color.rs:162] "ThemeColor debug" = "ThemeColor debug" -ThemeColor { - transparent: "rgba(0x00000000).into()", - mac_os_traffic_light_red: "rgba(0xec695eff).into()", - mac_os_traffic_light_yellow: "rgba(0xf4bf4eff).into()", - mac_os_traffic_light_green: "rgba(0x61c553ff).into()", - border: "rgba(0x464b57ff).into()", - border_variant: "rgba(0x464b57ff).into()", - border_focused: "rgba(0x293b5bff).into()", - border_transparent: "rgba(0x00000000).into()", - elevated_surface: "rgba(0x3b414dff).into()", - surface: "rgba(0x2f343eff).into()", - background: "rgba(0x3b414dff).into()", - filled_element: "rgba(0x3b414dff).into()", - filled_element_hover: "rgba(0xffffff1e).into()", - filled_element_active: "rgba(0xffffff28).into()", - filled_element_selected: "rgba(0x18243dff).into()", - filled_element_disabled: "rgba(0x00000000).into()", - ghost_element: "rgba(0x00000000).into()", - ghost_element_hover: "rgba(0xffffff14).into()", - ghost_element_active: "rgba(0xffffff1e).into()", - ghost_element_selected: "rgba(0x18243dff).into()", - ghost_element_disabled: "rgba(0x00000000).into()", - text: "rgba(0xc8ccd4ff).into()", - text_muted: "rgba(0x838994ff).into()", - text_placeholder: "rgba(0xd07277ff).into()", - text_disabled: "rgba(0x555a63ff).into()", - text_accent: "rgba(0x74ade8ff).into()", - icon_muted: "rgba(0x838994ff).into()", - syntax: SyntaxColor { - comment: "rgba(0x5d636fff).into()", - string: "rgba(0xa1c181ff).into()", - function: "rgba(0x73ade9ff).into()", - keyword: "rgba(0xb477cfff).into()", - }, - status_bar: "rgba(0x3b414dff).into()", - title_bar: "rgba(0x3b414dff).into()", - toolbar: "rgba(0x282c33ff).into()", - tab_bar: "rgba(0x2f343eff).into()", - editor_subheader: "rgba(0x2f343eff).into()", - editor_active_line: "rgba(0x2f343eff).into()", - terminal: "rgba(0x282c33ff).into()", - image_fallback_background: "rgba(0x3b414dff).into()", - git_created: "rgba(0xa1c181ff).into()", - git_modified: "rgba(0x74ade8ff).into()", - git_deleted: "rgba(0xd07277ff).into()", - git_conflict: "rgba(0xdec184ff).into()", - git_ignored: "rgba(0x555a63ff).into()", - git_renamed: "rgba(0xdec184ff).into()", - player: [ - PlayerThemeColors { - cursor: "rgba(0x74ade8ff).into()", - selection: "rgba(0x74ade83d).into()", - }, - PlayerThemeColors { - cursor: "rgba(0xa1c181ff).into()", - selection: "rgba(0xa1c1813d).into()", - }, - PlayerThemeColors { - cursor: "rgba(0xbe5046ff).into()", - selection: "rgba(0xbe50463d).into()", - }, - PlayerThemeColors { - cursor: "rgba(0xbf956aff).into()", - selection: "rgba(0xbf956a3d).into()", - }, - PlayerThemeColors { - cursor: "rgba(0xb477cfff).into()", - selection: "rgba(0xb477cf3d).into()", - }, - PlayerThemeColors { - cursor: "rgba(0x6eb4bfff).into()", - selection: "rgba(0x6eb4bf3d).into()", - }, - PlayerThemeColors { - cursor: "rgba(0xd07277ff).into()", - selection: "rgba(0xd072773d).into()", - }, - PlayerThemeColors { - cursor: "rgba(0xdec184ff).into()", - selection: "rgba(0xdec1843d).into()", - }, - ], -}