From 3422eb65e81394329385a44223638fed5ea23f2a Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 7 Sep 2023 11:16:51 -0700 Subject: [PATCH 001/404] Restore channel chat model and panel view --- Cargo.lock | 1 + crates/channel/src/channel.rs | 9 +- crates/channel/src/channel_buffer.rs | 14 +- crates/channel/src/channel_chat.rs | 459 ++++++++++++++++++++++ crates/channel/src/channel_store.rs | 88 +++-- crates/channel/src/channel_store_tests.rs | 220 ++++++++++- crates/collab/src/tests/test_server.rs | 2 +- crates/collab_ui/Cargo.toml | 1 + crates/collab_ui/src/channel_view.rs | 11 +- crates/collab_ui/src/chat_panel.rs | 391 ++++++++++++++++++ crates/collab_ui/src/collab_ui.rs | 1 + crates/rpc/proto/zed.proto | 57 ++- crates/rpc/src/proto.rs | 12 + crates/theme/src/theme.rs | 12 + 14 files changed, 1227 insertions(+), 51 deletions(-) create mode 100644 crates/channel/src/channel_chat.rs create mode 100644 crates/collab_ui/src/chat_panel.rs diff --git a/Cargo.lock b/Cargo.lock index 121e9a28dd160e9eef6d4d7497c7876196a57c88..6b25b47b75a400fbce6a480eec6626777859e263 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1553,6 +1553,7 @@ dependencies = [ "settings", "theme", "theme_selector", + "time 0.3.27", "util", "vcs_menu", "workspace", diff --git a/crates/channel/src/channel.rs b/crates/channel/src/channel.rs index 15631b7dd312f36126ec1e13b2413fc01e5ca8af..b4acc0c25fcb8587dc7b507ced5e78156fd94c45 100644 --- a/crates/channel/src/channel.rs +++ b/crates/channel/src/channel.rs @@ -1,10 +1,13 @@ +mod channel_buffer; +mod channel_chat; mod channel_store; -pub mod channel_buffer; -use std::sync::Arc; +pub use channel_buffer::{ChannelBuffer, ChannelBufferEvent}; +pub use channel_chat::{ChannelChat, ChannelChatEvent, ChannelMessage, ChannelMessageId}; +pub use channel_store::{Channel, ChannelEvent, ChannelId, ChannelMembership, ChannelStore}; -pub use channel_store::*; use client::Client; +use std::sync::Arc; #[cfg(test)] mod channel_store_tests; diff --git a/crates/channel/src/channel_buffer.rs b/crates/channel/src/channel_buffer.rs index e11282cf7963a9ba4f34c7530a6e8267fbe35274..06f9093fb5f9c628ba3ec2320f3605480f65486c 100644 --- a/crates/channel/src/channel_buffer.rs +++ b/crates/channel/src/channel_buffer.rs @@ -23,13 +23,13 @@ pub struct ChannelBuffer { subscription: Option, } -pub enum Event { +pub enum ChannelBufferEvent { CollaboratorsChanged, Disconnected, } impl Entity for ChannelBuffer { - type Event = Event; + type Event = ChannelBufferEvent; fn release(&mut self, _: &mut AppContext) { if self.connected { @@ -101,7 +101,7 @@ impl ChannelBuffer { } } self.collaborators = collaborators; - cx.emit(Event::CollaboratorsChanged); + cx.emit(ChannelBufferEvent::CollaboratorsChanged); cx.notify(); } @@ -141,7 +141,7 @@ impl ChannelBuffer { this.update(&mut cx, |this, cx| { this.collaborators.push(collaborator); - cx.emit(Event::CollaboratorsChanged); + cx.emit(ChannelBufferEvent::CollaboratorsChanged); cx.notify(); }); @@ -165,7 +165,7 @@ impl ChannelBuffer { true } }); - cx.emit(Event::CollaboratorsChanged); + cx.emit(ChannelBufferEvent::CollaboratorsChanged); cx.notify(); }); @@ -185,7 +185,7 @@ impl ChannelBuffer { break; } } - cx.emit(Event::CollaboratorsChanged); + cx.emit(ChannelBufferEvent::CollaboratorsChanged); cx.notify(); }); @@ -230,7 +230,7 @@ impl ChannelBuffer { if self.connected { self.connected = false; self.subscription.take(); - cx.emit(Event::Disconnected); + cx.emit(ChannelBufferEvent::Disconnected); cx.notify() } } diff --git a/crates/channel/src/channel_chat.rs b/crates/channel/src/channel_chat.rs new file mode 100644 index 0000000000000000000000000000000000000000..ebe491e5b896ac1b638237b0ad0df113a2e923b3 --- /dev/null +++ b/crates/channel/src/channel_chat.rs @@ -0,0 +1,459 @@ +use crate::Channel; +use anyhow::{anyhow, Result}; +use client::{ + proto, + user::{User, UserStore}, + Client, Subscription, TypedEnvelope, +}; +use futures::lock::Mutex; +use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task}; +use rand::prelude::*; +use std::{collections::HashSet, mem, ops::Range, sync::Arc}; +use sum_tree::{Bias, SumTree}; +use time::OffsetDateTime; +use util::{post_inc, ResultExt as _, TryFutureExt}; + +pub struct ChannelChat { + channel: Arc, + messages: SumTree, + loaded_all_messages: bool, + next_pending_message_id: usize, + user_store: ModelHandle, + rpc: Arc, + outgoing_messages_lock: Arc>, + rng: StdRng, + _subscription: Subscription, +} + +#[derive(Clone, Debug)] +pub struct ChannelMessage { + pub id: ChannelMessageId, + pub body: String, + pub timestamp: OffsetDateTime, + pub sender: Arc, + pub nonce: u128, +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +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, + }, +} + +impl Entity for ChannelChat { + type Event = ChannelChatEvent; + + fn release(&mut self, _: &mut AppContext) { + self.rpc + .send(proto::LeaveChannelChat { + channel_id: self.channel.id, + }) + .log_err(); + } +} + +impl ChannelChat { + pub fn init(rpc: &Arc) { + rpc.add_model_message_handler(Self::handle_message_sent); + } + + pub async fn new( + channel: Arc, + user_store: ModelHandle, + 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.add_model(|cx| { + let mut this = Self { + channel, + user_store, + rpc: client, + outgoing_messages_lock: Default::default(), + messages: Default::default(), + loaded_all_messages, + next_pending_message_id: 0, + rng: StdRng::from_entropy(), + _subscription: subscription.set_model(&cx.handle(), &mut cx.to_async()), + }; + this.insert_messages(messages, cx); + this + })) + } + + pub fn name(&self) -> &str { + &self.channel.name + } + + pub fn send_message( + &mut self, + body: String, + cx: &mut ModelContext, + ) -> Result>> { + if body.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: body.clone(), + sender: current_user, + timestamp: OffsetDateTime::now_utc(), + 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(|this, mut cx| async move { + let outgoing_message_guard = outgoing_messages_lock.lock().await; + let request = rpc.request(proto::SendChannelMessage { + channel_id, + body, + nonce: Some(nonce.into()), + }); + let response = request.await?; + drop(outgoing_message_guard); + 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); + Ok(()) + }) + })) + } + + pub fn load_more_messages(&mut self, cx: &mut ModelContext) -> bool { + if !self.loaded_all_messages { + let rpc = self.rpc.clone(); + let user_store = self.user_store.clone(); + let channel_id = self.channel.id; + if let Some(before_message_id) = + self.messages.first().and_then(|message| match message.id { + ChannelMessageId::Saved(id) => Some(id), + ChannelMessageId::Pending(_) => None, + }) + { + cx.spawn(|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() + }) + .detach(); + return true; + } + } + false + } + + 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(|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, + 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 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: ModelHandle, + message: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result<()> { + let user_store = this.read_with(&cx, |this, _| this.user_store.clone()); + let message = message + .payload + .message + .ok_or_else(|| anyhow!("empty message"))?; + + 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) + }); + + 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(); + } + } +} + +async fn messages_from_proto( + proto_messages: Vec, + user_store: &ModelHandle, + 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?); + } + let mut result = SumTree::new(); + result.extend(messages, &()); + Ok(result) +} + +impl ChannelMessage { + pub async fn from_proto( + message: proto::ChannelMessage, + user_store: &ModelHandle, + 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, + 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(_)) + } +} + +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; + } +} diff --git a/crates/channel/src/channel_store.rs b/crates/channel/src/channel_store.rs index a4c8da6df4f594553ac3629a4d4fc4a1176d89a3..6096692ccba946eb77201750b0cb3a024e1f3b7f 100644 --- a/crates/channel/src/channel_store.rs +++ b/crates/channel/src/channel_store.rs @@ -1,4 +1,4 @@ -use crate::channel_buffer::ChannelBuffer; +use crate::{channel_buffer::ChannelBuffer, channel_chat::ChannelChat}; use anyhow::{anyhow, Result}; use client::{Client, Subscription, User, UserId, UserStore}; use collections::{hash_map, HashMap, HashSet}; @@ -20,7 +20,8 @@ pub struct ChannelStore { channels_with_admin_privileges: HashSet, outgoing_invites: HashSet<(ChannelId, UserId)>, update_channels_tx: mpsc::UnboundedSender, - opened_buffers: HashMap, + opened_buffers: HashMap>, + opened_chats: HashMap>, client: Arc, user_store: ModelHandle, _rpc_subscription: Subscription, @@ -50,15 +51,9 @@ impl Entity for ChannelStore { type Event = ChannelEvent; } -pub enum ChannelMemberStatus { - Invited, - Member, - NotMember, -} - -enum OpenedChannelBuffer { - Open(WeakModelHandle), - Loading(Shared, Arc>>>), +enum OpenedModelHandle { + Open(WeakModelHandle), + Loading(Shared, Arc>>>), } impl ChannelStore { @@ -94,6 +89,7 @@ impl ChannelStore { channels_with_admin_privileges: Default::default(), outgoing_invites: Default::default(), opened_buffers: Default::default(), + opened_chats: Default::default(), update_channels_tx, client, user_store, @@ -154,7 +150,7 @@ impl ChannelStore { 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 OpenedChannelBuffer::Open(buffer) = buffer { + if let OpenedModelHandle::Open(buffer) = buffer { return buffer.upgrade(cx).is_some(); } } @@ -166,24 +162,58 @@ impl ChannelStore { channel_id: ChannelId, cx: &mut ModelContext, ) -> Task>> { - // Make sure that a given channel buffer is only opened once per + let client = self.client.clone(); + self.open_channel_resource( + channel_id, + |this| &mut this.opened_buffers, + |channel, cx| ChannelBuffer::new(channel, client, cx), + cx, + ) + } + + 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(); + self.open_channel_resource( + channel_id, + |this| &mut this.opened_chats, + |channel, cx| ChannelChat::new(channel, user_store, client, cx), + cx, + ) + } + + fn open_channel_resource( + &mut self, + channel_id: ChannelId, + map: fn(&mut Self) -> &mut HashMap>, + load: F, + cx: &mut ModelContext, + ) -> Task>> + where + F: 'static + FnOnce(Arc, AsyncAppContext) -> Fut, + Fut: Future>>, + { + // Make sure that a given channel resource is only opened once per // app instance, even if this method is called multiple times // with the same channel id while the first task is still running. let task = loop { - match self.opened_buffers.entry(channel_id) { + match map(self).entry(channel_id) { hash_map::Entry::Occupied(e) => match e.get() { - OpenedChannelBuffer::Open(buffer) => { + OpenedModelHandle::Open(buffer) => { if let Some(buffer) = buffer.upgrade(cx) { break Task::ready(Ok(buffer)).shared(); } else { - self.opened_buffers.remove(&channel_id); + map(self).remove(&channel_id); continue; } } - OpenedChannelBuffer::Loading(task) => break task.clone(), + OpenedModelHandle::Loading(task) => break task.clone(), }, hash_map::Entry::Vacant(e) => { - let client = self.client.clone(); let task = cx .spawn(|this, cx| async move { let channel = this.read_with(&cx, |this, _| { @@ -192,12 +222,10 @@ impl ChannelStore { }) })?; - ChannelBuffer::new(channel, client, cx) - .await - .map_err(Arc::new) + load(channel, cx).await.map_err(Arc::new) }) .shared(); - e.insert(OpenedChannelBuffer::Loading(task.clone())); + e.insert(OpenedModelHandle::Loading(task.clone())); cx.spawn({ let task = task.clone(); |this, mut cx| async move { @@ -208,14 +236,14 @@ impl ChannelStore { this.opened_buffers.remove(&channel_id); }) .detach(); - this.opened_buffers.insert( + map(this).insert( channel_id, - OpenedChannelBuffer::Open(buffer.downgrade()), + OpenedModelHandle::Open(buffer.downgrade()), ); } Err(error) => { log::error!("failed to open channel buffer {error:?}"); - this.opened_buffers.remove(&channel_id); + map(this).remove(&channel_id); } }); } @@ -496,7 +524,7 @@ impl ChannelStore { let mut buffer_versions = Vec::new(); for buffer in self.opened_buffers.values() { - if let OpenedChannelBuffer::Open(buffer) = buffer { + if let OpenedModelHandle::Open(buffer) = buffer { if let Some(buffer) = buffer.upgrade(cx) { let channel_buffer = buffer.read(cx); let buffer = channel_buffer.buffer().read(cx); @@ -522,7 +550,7 @@ impl ChannelStore { this.update(&mut cx, |this, cx| { this.opened_buffers.retain(|_, buffer| match buffer { - OpenedChannelBuffer::Open(channel_buffer) => { + OpenedModelHandle::Open(channel_buffer) => { let Some(channel_buffer) = channel_buffer.upgrade(cx) else { return false; }; @@ -583,7 +611,7 @@ impl ChannelStore { false }) } - OpenedChannelBuffer::Loading(_) => true, + OpenedModelHandle::Loading(_) => true, }); }); anyhow::Ok(()) @@ -605,7 +633,7 @@ impl ChannelStore { if let Some(this) = this.upgrade(&cx) { this.update(&mut cx, |this, cx| { for (_, buffer) in this.opened_buffers.drain() { - if let OpenedChannelBuffer::Open(buffer) = buffer { + if let OpenedModelHandle::Open(buffer) = buffer { if let Some(buffer) = buffer.upgrade(cx) { buffer.update(cx, |buffer, cx| buffer.disconnect(cx)); } @@ -654,7 +682,7 @@ impl ChannelStore { for channel_id in &payload.remove_channels { let channel_id = *channel_id; - if let Some(OpenedChannelBuffer::Open(buffer)) = + if let Some(OpenedModelHandle::Open(buffer)) = self.opened_buffers.remove(&channel_id) { if let Some(buffer) = buffer.upgrade(cx) { diff --git a/crates/channel/src/channel_store_tests.rs b/crates/channel/src/channel_store_tests.rs index 18894b1f472f907d3b54ad35df57d78e5e974565..3ae899ecde8ed2f44628ca9b2c31f44d9caf8bcb 100644 --- a/crates/channel/src/channel_store_tests.rs +++ b/crates/channel/src/channel_store_tests.rs @@ -1,6 +1,8 @@ +use crate::channel_chat::ChannelChatEvent; + use super::*; -use client::{Client, UserStore}; -use gpui::{AppContext, ModelHandle}; +use client::{test::FakeServer, Client, UserStore}; +use gpui::{AppContext, ModelHandle, TestAppContext}; use rpc::proto; use util::http::FakeHttpClient; @@ -137,6 +139,220 @@ fn test_dangling_channel_paths(cx: &mut AppContext) { assert_channels(&channel_store, &[(0, "a".to_string(), true)], cx); } +#[gpui::test] +async fn test_channel_messages(cx: &mut TestAppContext) { + cx.foreground().forbid_parking(); + + let user_id = 5; + let http_client = FakeHttpClient::with_404_response(); + let client = cx.update(|cx| Client::new(http_client.clone(), cx)); + let server = FakeServer::for_client(user_id, &client, cx).await; + let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx)); + crate::init(&client); + + let channel_store = cx.add_model(|cx| ChannelStore::new(client, user_store, cx)); + + let channel_id = 5; + + // Get the available channels. + server.send(proto::UpdateChannels { + channels: vec![proto::Channel { + id: channel_id, + name: "the-channel".to_string(), + parent_id: None, + }], + ..Default::default() + }); + channel_store.next_notification(cx).await; + cx.read(|cx| { + assert_channels(&channel_store, &[(0, "the-channel".to_string(), false)], 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(), + }], + }, + ) + .await; + + // Join a channel and populate its existing messages. + let channel = channel_store + .update(cx, |store, cx| { + let channel_id = store.channels().next().unwrap().1.id; + store.open_channel_chat(channel_id, cx) + }) + .await + .unwrap(); + channel.read_with(cx, |channel, _| assert!(channel.messages().is_empty())); + 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, + nonce: Some(1.into()), + }, + proto::ChannelMessage { + id: 11, + body: "b".into(), + timestamp: 1001, + sender_id: 6, + nonce: Some(2.into()), + }, + ], + done: false, + }, + ) + .await; + + // 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(), + }], + }, + ) + .await; + + assert_eq!( + channel.next_event(cx).await, + ChannelChatEvent::MessagesUpdated { + old_range: 0..0, + new_count: 2, + } + ); + channel.read_with(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, + 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(), + }], + }, + ) + .await; + + assert_eq!( + channel.next_event(cx).await, + ChannelChatEvent::MessagesUpdated { + old_range: 2..2, + new_count: 1, + } + ); + channel.read_with(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| { + assert!(channel.load_more_messages(cx)); + }); + 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()), + }, + proto::ChannelMessage { + id: 9, + body: "z".into(), + timestamp: 999, + sender_id: 6, + nonce: Some(5.into()), + }, + ], + }, + ) + .await; + + assert_eq!( + channel.next_event(cx).await, + ChannelChatEvent::MessagesUpdated { + old_range: 0..0, + new_count: 2, + } + ); + channel.read_with(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 update_channels( channel_store: &ModelHandle, message: proto::UpdateChannels, diff --git a/crates/collab/src/tests/test_server.rs b/crates/collab/src/tests/test_server.rs index eef1dde96755701cf39a78094aeb19f737f95d3e..a7dbd97239079b41ff62a5e3e1a3cae4b9051986 100644 --- a/crates/collab/src/tests/test_server.rs +++ b/crates/collab/src/tests/test_server.rs @@ -6,7 +6,7 @@ use crate::{ }; use anyhow::anyhow; use call::ActiveCall; -use channel::{channel_buffer::ChannelBuffer, ChannelStore}; +use channel::{ChannelBuffer, ChannelStore}; use client::{ self, proto::PeerId, Client, Connection, Credentials, EstablishConnectionError, UserStore, }; diff --git a/crates/collab_ui/Cargo.toml b/crates/collab_ui/Cargo.toml index da32308558f7c7e8279c420961f8d42d9356d37b..0a52b9a19fe9df7c070e33de1d8c719b0dd8dae2 100644 --- a/crates/collab_ui/Cargo.toml +++ b/crates/collab_ui/Cargo.toml @@ -55,6 +55,7 @@ schemars.workspace = true postage.workspace = true serde.workspace = true serde_derive.workspace = true +time.workspace = true [dev-dependencies] call = { path = "../call", features = ["test-support"] } diff --git a/crates/collab_ui/src/channel_view.rs b/crates/collab_ui/src/channel_view.rs index 5086cc8b37739aa74710d84c7f90e4040bc02f15..76b79eaf395a54a5b14fcd4c556d8271a1196168 100644 --- a/crates/collab_ui/src/channel_view.rs +++ b/crates/collab_ui/src/channel_view.rs @@ -1,8 +1,5 @@ use anyhow::{anyhow, Result}; -use channel::{ - channel_buffer::{self, ChannelBuffer}, - ChannelId, -}; +use channel::{ChannelBuffer, ChannelBufferEvent, ChannelId}; use client::proto; use clock::ReplicaId; use collections::HashMap; @@ -118,14 +115,14 @@ impl ChannelView { fn handle_channel_buffer_event( &mut self, _: ModelHandle, - event: &channel_buffer::Event, + event: &ChannelBufferEvent, cx: &mut ViewContext, ) { match event { - channel_buffer::Event::CollaboratorsChanged => { + ChannelBufferEvent::CollaboratorsChanged => { self.refresh_replica_id_map(cx); } - channel_buffer::Event::Disconnected => self.editor.update(cx, |editor, cx| { + ChannelBufferEvent::Disconnected => self.editor.update(cx, |editor, cx| { editor.set_read_only(true); cx.notify(); }), diff --git a/crates/collab_ui/src/chat_panel.rs b/crates/collab_ui/src/chat_panel.rs new file mode 100644 index 0000000000000000000000000000000000000000..0105a9d27b2fa67d34b660b7cf18640169286bd7 --- /dev/null +++ b/crates/collab_ui/src/chat_panel.rs @@ -0,0 +1,391 @@ +use channel::{ChannelChat, ChannelChatEvent, ChannelMessage, ChannelStore}; +use client::Client; +use editor::Editor; +use gpui::{ + actions, + elements::*, + platform::{CursorStyle, MouseButton}, + views::{ItemType, Select, SelectStyle}, + AnyViewHandle, AppContext, Entity, ModelHandle, Subscription, View, ViewContext, ViewHandle, +}; +use language::language_settings::SoftWrap; +use menu::Confirm; +use std::sync::Arc; +use theme::Theme; +use time::{OffsetDateTime, UtcOffset}; +use util::{ResultExt, TryFutureExt}; + +const MESSAGE_LOADING_THRESHOLD: usize = 50; + +pub struct ChatPanel { + client: Arc, + channel_store: ModelHandle, + active_channel: Option<(ModelHandle, Subscription)>, + message_list: ListState, + input_editor: ViewHandle, + channel_select: ViewHandle { + let channel = &channel_store.read(cx).channel_at_index(ix).unwrap().1; + let theme = match (item_type, is_hovered) { + (ItemType::Header, _) => &theme.header, + (ItemType::Selected, false) => &theme.active_item, + (ItemType::Selected, true) => &theme.hovered_active_item, + (ItemType::Unselected, false) => &theme.item, + (ItemType::Unselected, true) => &theme.hovered_item, + }; + Flex::row() + .with_child( + Label::new("#".to_string(), theme.hash.text.clone()) + .contained() + .with_style(theme.hash.container), + ) + .with_child(Label::new(channel.name.clone(), theme.name.clone())) + .contained() + .with_style(theme.container) + .into_any() + } + + fn render_sign_in_prompt( + &self, + theme: &Arc, + cx: &mut ViewContext, + ) -> AnyElement { + enum SignInPromptLabel {} + + MouseEventHandler::new::(0, cx, |mouse_state, _| { + Label::new( + "Sign in to use chat".to_string(), + theme + .chat_panel + .sign_in_prompt + .style_for(mouse_state) + .clone(), + ) + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, move |_, this, cx| { + let client = this.client.clone(); + cx.spawn(|this, mut cx| async move { + if client + .authenticate_and_connect(true, &cx) + .log_err() + .await + .is_some() + { + this.update(&mut cx, |this, cx| { + if cx.handle().is_focused(cx) { + cx.focus(&this.input_editor); + } + }) + .ok(); + } + }) + .detach(); + }) + .aligned() + .into_any() + } + + fn send(&mut self, _: &Confirm, cx: &mut ViewContext) { + if let Some((channel, _)) = self.active_channel.as_ref() { + let body = self.input_editor.update(cx, |editor, cx| { + let body = editor.text(cx); + editor.clear(cx); + body + }); + + if let Some(task) = channel + .update(cx, |channel, cx| channel.send_message(body, cx)) + .log_err() + { + task.detach(); + } + } + } + + fn load_more_messages(&mut self, _: &LoadMoreMessages, cx: &mut ViewContext) { + if let Some((channel, _)) = self.active_channel.as_ref() { + channel.update(cx, |channel, cx| { + channel.load_more_messages(cx); + }) + } + } +} + +impl Entity for ChatPanel { + type Event = Event; +} + +impl View for ChatPanel { + fn ui_name() -> &'static str { + "ChatPanel" + } + + fn render(&mut self, cx: &mut ViewContext) -> AnyElement { + let theme = theme::current(cx); + let element = if self.client.user_id().is_some() { + self.render_channel(cx) + } else { + self.render_sign_in_prompt(&theme, cx) + }; + element + .contained() + .with_style(theme.chat_panel.container) + .constrained() + .with_min_width(150.) + .into_any() + } + + fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { + if matches!( + *self.client.status().borrow(), + client::Status::Connected { .. } + ) { + cx.focus(&self.input_editor); + } + } +} + +fn format_timestamp( + mut timestamp: OffsetDateTime, + mut now: OffsetDateTime, + local_timezone: UtcOffset, +) -> String { + timestamp = timestamp.to_offset(local_timezone); + now = now.to_offset(local_timezone); + + let today = now.date(); + let date = timestamp.date(); + let mut hour = timestamp.hour(); + let mut part = "am"; + if hour > 12 { + hour -= 12; + part = "pm"; + } + if date == today { + format!("{:02}:{:02}{}", hour, timestamp.minute(), part) + } else if date.next_day() == Some(today) { + format!("yesterday at {:02}:{:02}{}", hour, timestamp.minute(), part) + } else { + format!("{:02}/{}/{}", date.month() as u32, date.day(), date.year()) + } +} diff --git a/crates/collab_ui/src/collab_ui.rs b/crates/collab_ui/src/collab_ui.rs index ee34f600fa9b838cc00d0ae1ba084ef13f1c1f4c..b40ecc6992c6c6e643faa100c88a42f1ac32ff1a 100644 --- a/crates/collab_ui/src/collab_ui.rs +++ b/crates/collab_ui/src/collab_ui.rs @@ -1,4 +1,5 @@ pub mod channel_view; +pub mod chat_panel; pub mod collab_panel; mod collab_titlebar_item; mod contact_notification; diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 2e96d79f5e50b21a0e1a0444b1f62cb3ad23f32e..eb26e81b9980512e0f69c6eb663390cb0da46c2f 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -155,7 +155,16 @@ message Envelope { RemoveChannelBufferCollaborator remove_channel_buffer_collaborator = 136; UpdateChannelBufferCollaborator update_channel_buffer_collaborator = 139; RejoinChannelBuffers rejoin_channel_buffers = 140; - RejoinChannelBuffersResponse rejoin_channel_buffers_response = 141; // Current max + RejoinChannelBuffersResponse rejoin_channel_buffers_response = 141; + + JoinChannelChat join_channel_chat = 142; + JoinChannelChatResponse join_channel_chat_response = 143; + LeaveChannelChat leave_channel_chat = 144; + SendChannelMessage send_channel_message = 145; + SendChannelMessageResponse send_channel_message_response = 146; + ChannelMessageSent channel_message_sent = 147; + GetChannelMessages get_channel_messages = 148; + GetChannelMessagesResponse get_channel_messages_response = 149; // Current max } } @@ -1021,10 +1030,56 @@ message RenameChannel { string name = 2; } +message JoinChannelChat { + uint64 channel_id = 1; +} + +message JoinChannelChatResponse { + repeated ChannelMessage messages = 1; + bool done = 2; +} + +message LeaveChannelChat { + uint64 channel_id = 1; +} + +message SendChannelMessage { + uint64 channel_id = 1; + string body = 2; + Nonce nonce = 3; +} + +message SendChannelMessageResponse { + ChannelMessage message = 1; +} + +message ChannelMessageSent { + uint64 channel_id = 1; + ChannelMessage message = 2; +} + +message GetChannelMessages { + uint64 channel_id = 1; + uint64 before_message_id = 2; +} + +message GetChannelMessagesResponse { + repeated ChannelMessage messages = 1; + bool done = 2; +} + message JoinChannelBuffer { uint64 channel_id = 1; } +message ChannelMessage { + uint64 id = 1; + string body = 2; + uint64 timestamp = 3; + uint64 sender_id = 4; + Nonce nonce = 5; +} + message RejoinChannelBuffers { repeated ChannelBufferVersion buffers = 1; } diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index f643a8c168dc4088db65d48e7678c19363b434ff..7b98f50e75c4d47bf84ef751c143a05b4508198c 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -147,6 +147,7 @@ messages!( (CreateBufferForPeer, Foreground), (CreateChannel, Foreground), (ChannelResponse, Foreground), + (ChannelMessageSent, Foreground), (CreateProjectEntry, Foreground), (CreateRoom, Foreground), (CreateRoomResponse, Foreground), @@ -163,6 +164,10 @@ messages!( (GetCodeActionsResponse, Background), (GetHover, Background), (GetHoverResponse, Background), + (GetChannelMessages, Background), + (GetChannelMessagesResponse, Background), + (SendChannelMessage, Background), + (SendChannelMessageResponse, Background), (GetCompletions, Background), (GetCompletionsResponse, Background), (GetDefinition, Background), @@ -184,6 +189,9 @@ messages!( (JoinProjectResponse, Foreground), (JoinRoom, Foreground), (JoinRoomResponse, Foreground), + (JoinChannelChat, Foreground), + (JoinChannelChatResponse, Foreground), + (LeaveChannelChat, Foreground), (LeaveProject, Foreground), (LeaveRoom, Foreground), (OpenBufferById, Background), @@ -293,6 +301,7 @@ request_messages!( (InviteChannelMember, Ack), (JoinProject, JoinProjectResponse), (JoinRoom, JoinRoomResponse), + (JoinChannelChat, JoinChannelChatResponse), (LeaveRoom, Ack), (RejoinRoom, RejoinRoomResponse), (IncomingCall, Ack), @@ -313,6 +322,8 @@ request_messages!( (RespondToContactRequest, Ack), (RespondToChannelInvite, Ack), (SetChannelMemberAdmin, Ack), + (SendChannelMessage, SendChannelMessageResponse), + (GetChannelMessages, GetChannelMessagesResponse), (GetChannelMembers, GetChannelMembersResponse), (JoinChannel, JoinRoomResponse), (RemoveChannel, Ack), @@ -388,6 +399,7 @@ entity_messages!( entity_messages!( channel_id, + ChannelMessageSent, UpdateChannelBuffer, RemoveChannelBufferCollaborator, AddChannelBufferCollaborator, diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index a54224978841ba6568e2008c5aacb6a36010f9aa..15d7e1a71fb2974db852f8625b115c6b436d958d 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -49,6 +49,7 @@ pub struct Theme { pub copilot: Copilot, pub collab_panel: CollabPanel, pub project_panel: ProjectPanel, + pub chat_panel: ChatPanel, pub command_palette: CommandPalette, pub picker: Picker, pub editor: Editor, @@ -611,6 +612,17 @@ pub struct IconButton { pub button_width: f32, } +#[derive(Deserialize, Default, JsonSchema)] +pub struct ChatPanel { + #[serde(flatten)] + pub container: ContainerStyle, + pub channel_select: ChannelSelect, + pub input_editor: FieldEditor, + pub message: ChatMessage, + pub pending_message: ChatMessage, + pub sign_in_prompt: Interactive, +} + #[derive(Deserialize, Default, JsonSchema)] pub struct ChatMessage { #[serde(flatten)] From da5a77badf9f39ac7e2614b2c33fd65fc1857629 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 7 Sep 2023 12:24:25 -0700 Subject: [PATCH 002/404] Start work on restoring server-side code for chat messages --- crates/channel/src/channel.rs | 1 + crates/channel/src/channel_chat.rs | 8 +- .../20221109000000_test_schema.sql | 20 +++ .../20230907114200_add_channel_messages.sql | 19 +++ crates/collab/src/db/ids.rs | 2 + crates/collab/src/db/queries.rs | 1 + crates/collab/src/db/queries/messages.rs | 152 ++++++++++++++++++ crates/collab/src/db/tables.rs | 2 + crates/collab/src/db/tables/channel.rs | 8 + .../src/db/tables/channel_chat_participant.rs | 41 +++++ .../collab/src/db/tables/channel_message.rs | 45 ++++++ crates/collab/src/db/tests.rs | 1 + crates/collab/src/db/tests/message_tests.rs | 53 ++++++ crates/collab/src/rpc.rs | 119 +++++++++++++- crates/collab/src/tests.rs | 1 + .../collab/src/tests/channel_message_tests.rs | 56 +++++++ 16 files changed, 524 insertions(+), 5 deletions(-) create mode 100644 crates/collab/migrations/20230907114200_add_channel_messages.sql create mode 100644 crates/collab/src/db/queries/messages.rs create mode 100644 crates/collab/src/db/tables/channel_chat_participant.rs create mode 100644 crates/collab/src/db/tables/channel_message.rs create mode 100644 crates/collab/src/db/tests/message_tests.rs create mode 100644 crates/collab/src/tests/channel_message_tests.rs diff --git a/crates/channel/src/channel.rs b/crates/channel/src/channel.rs index b4acc0c25fcb8587dc7b507ced5e78156fd94c45..37f1c0ce44ba8a8f3a86247ea411ba0d2b669f7d 100644 --- a/crates/channel/src/channel.rs +++ b/crates/channel/src/channel.rs @@ -14,4 +14,5 @@ mod channel_store_tests; pub fn init(client: &Arc) { channel_buffer::init(client); + channel_chat::init(client); } diff --git a/crates/channel/src/channel_chat.rs b/crates/channel/src/channel_chat.rs index ebe491e5b896ac1b638237b0ad0df113a2e923b3..3916189363a040aba0b84d619c9bedbec8d72df8 100644 --- a/crates/channel/src/channel_chat.rs +++ b/crates/channel/src/channel_chat.rs @@ -57,6 +57,10 @@ pub enum ChannelChatEvent { }, } +pub fn init(client: &Arc) { + client.add_model_message_handler(ChannelChat::handle_message_sent); +} + impl Entity for ChannelChat { type Event = ChannelChatEvent; @@ -70,10 +74,6 @@ impl Entity for ChannelChat { } impl ChannelChat { - pub fn init(rpc: &Arc) { - rpc.add_model_message_handler(Self::handle_message_sent); - } - pub async fn new( channel: Arc, user_store: ModelHandle, diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index 80477dcb3c3b9f4fc1efd25622243b59901cf4fc..ab12039b10bef0f02b117720d2d53e9d99080792 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -192,6 +192,26 @@ CREATE TABLE "channels" ( "created_at" TIMESTAMP NOT NULL DEFAULT now ); +CREATE TABLE IF NOT EXISTS "channel_chat_participants" ( + "id" INTEGER PRIMARY KEY AUTOINCREMENT, + "user_id" INTEGER NOT NULL REFERENCES users (id), + "channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE, + "connection_id" INTEGER NOT NULL, + "connection_server_id" INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE +); +CREATE INDEX "index_channel_chat_participants_on_channel_id" ON "channel_chat_participants" ("channel_id"); + +CREATE TABLE IF NOT EXISTS "channel_messages" ( + "id" INTEGER PRIMARY KEY AUTOINCREMENT, + "channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE, + "sender_id" INTEGER NOT NULL REFERENCES users (id), + "body" TEXT NOT NULL, + "sent_at" TIMESTAMP, + "nonce" BLOB NOT NULL +); +CREATE INDEX "index_channel_messages_on_channel_id" ON "channel_messages" ("channel_id"); +CREATE UNIQUE INDEX "index_channel_messages_on_nonce" ON "channel_messages" ("nonce"); + CREATE TABLE "channel_paths" ( "id_path" TEXT NOT NULL PRIMARY KEY, "channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE diff --git a/crates/collab/migrations/20230907114200_add_channel_messages.sql b/crates/collab/migrations/20230907114200_add_channel_messages.sql new file mode 100644 index 0000000000000000000000000000000000000000..abe7753ca69fb45a1f0a56b732963d8dc5605e31 --- /dev/null +++ b/crates/collab/migrations/20230907114200_add_channel_messages.sql @@ -0,0 +1,19 @@ +CREATE TABLE IF NOT EXISTS "channel_messages" ( + "id" SERIAL PRIMARY KEY, + "channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE, + "sender_id" INTEGER NOT NULL REFERENCES users (id), + "body" TEXT NOT NULL, + "sent_at" TIMESTAMP, + "nonce" UUID NOT NULL +); +CREATE INDEX "index_channel_messages_on_channel_id" ON "channel_messages" ("channel_id"); +CREATE UNIQUE INDEX "index_channel_messages_on_nonce" ON "channel_messages" ("nonce"); + +CREATE TABLE IF NOT EXISTS "channel_chat_participants" ( + "id" SERIAL PRIMARY KEY, + "user_id" INTEGER NOT NULL REFERENCES users (id), + "channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE, + "connection_id" INTEGER NOT NULL, + "connection_server_id" INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE +); +CREATE INDEX "index_channel_chat_participants_on_channel_id" ON "channel_chat_participants" ("channel_id"); diff --git a/crates/collab/src/db/ids.rs b/crates/collab/src/db/ids.rs index b33ea57183b8771792ea50c6b3ab2b2631971194..865a39fd7154f6aa9ac993a5c7a4b53f8a302c6a 100644 --- a/crates/collab/src/db/ids.rs +++ b/crates/collab/src/db/ids.rs @@ -112,8 +112,10 @@ fn value_to_integer(v: Value) -> Result { id_type!(BufferId); id_type!(AccessTokenId); +id_type!(ChannelChatParticipantId); id_type!(ChannelId); id_type!(ChannelMemberId); +id_type!(MessageId); id_type!(ContactId); id_type!(FollowerId); id_type!(RoomId); diff --git a/crates/collab/src/db/queries.rs b/crates/collab/src/db/queries.rs index 09a8f073b469f72773a0220750f5d65cf85629af..54db0663d2e4d631b657cc44a81b2dc1382a49bc 100644 --- a/crates/collab/src/db/queries.rs +++ b/crates/collab/src/db/queries.rs @@ -4,6 +4,7 @@ pub mod access_tokens; pub mod buffers; pub mod channels; pub mod contacts; +pub mod messages; pub mod projects; pub mod rooms; pub mod servers; diff --git a/crates/collab/src/db/queries/messages.rs b/crates/collab/src/db/queries/messages.rs new file mode 100644 index 0000000000000000000000000000000000000000..e8b9ec74165efa388b2af7c3a27d813278a0f461 --- /dev/null +++ b/crates/collab/src/db/queries/messages.rs @@ -0,0 +1,152 @@ +use super::*; +use time::OffsetDateTime; + +impl Database { + pub async fn join_channel_chat( + &self, + channel_id: ChannelId, + connection_id: ConnectionId, + user_id: UserId, + ) -> Result<()> { + self.transaction(|tx| async move { + self.check_user_is_channel_member(channel_id, user_id, &*tx) + .await?; + channel_chat_participant::ActiveModel { + id: ActiveValue::NotSet, + channel_id: ActiveValue::Set(channel_id), + user_id: ActiveValue::Set(user_id), + connection_id: ActiveValue::Set(connection_id.id as i32), + connection_server_id: ActiveValue::Set(ServerId(connection_id.owner_id as i32)), + } + .insert(&*tx) + .await?; + Ok(()) + }) + .await + } + + pub async fn leave_channel_chat( + &self, + channel_id: ChannelId, + connection_id: ConnectionId, + _user_id: UserId, + ) -> Result<()> { + self.transaction(|tx| async move { + channel_chat_participant::Entity::delete_many() + .filter( + Condition::all() + .add( + channel_chat_participant::Column::ConnectionServerId + .eq(connection_id.owner_id), + ) + .add(channel_chat_participant::Column::ConnectionId.eq(connection_id.id)) + .add(channel_chat_participant::Column::ChannelId.eq(channel_id)), + ) + .exec(&*tx) + .await?; + + Ok(()) + }) + .await + } + + pub async fn get_channel_messages( + &self, + channel_id: ChannelId, + user_id: UserId, + count: usize, + before_message_id: Option, + ) -> Result> { + self.transaction(|tx| async move { + self.check_user_is_channel_member(channel_id, user_id, &*tx) + .await?; + + let mut condition = + Condition::all().add(channel_message::Column::ChannelId.eq(channel_id)); + + if let Some(before_message_id) = before_message_id { + condition = condition.add(channel_message::Column::Id.lt(before_message_id)); + } + + let mut rows = channel_message::Entity::find() + .filter(condition) + .limit(count as u64) + .stream(&*tx) + .await?; + + let mut messages = Vec::new(); + while let Some(row) = rows.next().await { + let row = row?; + let nonce = row.nonce.as_u64_pair(); + messages.push(proto::ChannelMessage { + id: row.id.to_proto(), + sender_id: row.sender_id.to_proto(), + body: row.body, + timestamp: row.sent_at.unix_timestamp() as u64, + nonce: Some(proto::Nonce { + upper_half: nonce.0, + lower_half: nonce.1, + }), + }); + } + + Ok(messages) + }) + .await + } + + pub async fn create_channel_message( + &self, + channel_id: ChannelId, + user_id: UserId, + body: &str, + timestamp: OffsetDateTime, + nonce: u128, + ) -> Result<(MessageId, Vec)> { + self.transaction(|tx| async move { + let mut rows = channel_chat_participant::Entity::find() + .filter(channel_chat_participant::Column::ChannelId.eq(channel_id)) + .stream(&*tx) + .await?; + + let mut is_participant = false; + let mut participant_connection_ids = Vec::new(); + while let Some(row) = rows.next().await { + let row = row?; + if row.user_id == user_id { + is_participant = true; + } + participant_connection_ids.push(row.connection()); + } + drop(rows); + + if !is_participant { + Err(anyhow!("not a chat participant"))?; + } + + let message = channel_message::Entity::insert(channel_message::ActiveModel { + channel_id: ActiveValue::Set(channel_id), + sender_id: ActiveValue::Set(user_id), + body: ActiveValue::Set(body.to_string()), + sent_at: ActiveValue::Set(timestamp), + nonce: ActiveValue::Set(Uuid::from_u128(nonce)), + id: ActiveValue::NotSet, + }) + .on_conflict( + OnConflict::column(channel_message::Column::Nonce) + .update_column(channel_message::Column::Nonce) + .to_owned(), + ) + .exec(&*tx) + .await?; + + #[derive(Debug, Clone, Copy, EnumIter, DeriveColumn)] + enum QueryConnectionId { + ConnectionId, + } + + Ok((message.last_insert_id, participant_connection_ids)) + }) + .await + } +} diff --git a/crates/collab/src/db/tables.rs b/crates/collab/src/db/tables.rs index 1765cee065fb6c7ae31818568a229e3c3c0bd3f0..81200df3e7b51a72a5ca33a100ff08ecbc80b75c 100644 --- a/crates/collab/src/db/tables.rs +++ b/crates/collab/src/db/tables.rs @@ -4,7 +4,9 @@ pub mod buffer_operation; pub mod buffer_snapshot; pub mod channel; pub mod channel_buffer_collaborator; +pub mod channel_chat_participant; pub mod channel_member; +pub mod channel_message; pub mod channel_path; pub mod contact; pub mod feature_flag; diff --git a/crates/collab/src/db/tables/channel.rs b/crates/collab/src/db/tables/channel.rs index 05895ede4cf6b5080889cf281a1ce3651aebd1c2..54f12defc1b56570a0629e2e92a896ad167aa6d6 100644 --- a/crates/collab/src/db/tables/channel.rs +++ b/crates/collab/src/db/tables/channel.rs @@ -21,6 +21,8 @@ pub enum Relation { Member, #[sea_orm(has_many = "super::channel_buffer_collaborator::Entity")] BufferCollaborators, + #[sea_orm(has_many = "super::channel_chat_participant::Entity")] + ChatParticipants, } impl Related for Entity { @@ -46,3 +48,9 @@ impl Related for Entity { Relation::BufferCollaborators.def() } } + +impl Related for Entity { + fn to() -> RelationDef { + Relation::ChatParticipants.def() + } +} diff --git a/crates/collab/src/db/tables/channel_chat_participant.rs b/crates/collab/src/db/tables/channel_chat_participant.rs new file mode 100644 index 0000000000000000000000000000000000000000..f3ef36c289f86e5f20411cf9b3f442698f6a4024 --- /dev/null +++ b/crates/collab/src/db/tables/channel_chat_participant.rs @@ -0,0 +1,41 @@ +use crate::db::{ChannelChatParticipantId, ChannelId, ServerId, UserId}; +use rpc::ConnectionId; +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "channel_chat_participants")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: ChannelChatParticipantId, + pub channel_id: ChannelId, + pub user_id: UserId, + pub connection_id: i32, + pub connection_server_id: ServerId, +} + +impl Model { + pub fn connection(&self) -> ConnectionId { + ConnectionId { + owner_id: self.connection_server_id.0 as u32, + id: self.connection_id as u32, + } + } +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::channel::Entity", + from = "Column::ChannelId", + to = "super::channel::Column::Id" + )] + Channel, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Channel.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab/src/db/tables/channel_message.rs b/crates/collab/src/db/tables/channel_message.rs new file mode 100644 index 0000000000000000000000000000000000000000..d043d5b6686fdd1b56581217cf63b6187fce4738 --- /dev/null +++ b/crates/collab/src/db/tables/channel_message.rs @@ -0,0 +1,45 @@ +use crate::db::{ChannelId, MessageId, UserId}; +use sea_orm::entity::prelude::*; +use time::OffsetDateTime; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "channel_messages")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: MessageId, + pub channel_id: ChannelId, + pub sender_id: UserId, + pub body: String, + pub sent_at: OffsetDateTime, + pub nonce: Uuid, +} + +impl ActiveModelBehavior for ActiveModel {} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::channel::Entity", + from = "Column::ChannelId", + to = "super::channel::Column::Id" + )] + Channel, + #[sea_orm( + belongs_to = "super::user::Entity", + from = "Column::SenderId", + to = "super::user::Column::Id" + )] + Sender, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Channel.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Sender.def() + } +} diff --git a/crates/collab/src/db/tests.rs b/crates/collab/src/db/tests.rs index ee961006cbbf74b019141c0973aca18d73309012..1d6c550865098a9afb2c5ca63e7357c64e46eaf8 100644 --- a/crates/collab/src/db/tests.rs +++ b/crates/collab/src/db/tests.rs @@ -1,6 +1,7 @@ mod buffer_tests; mod db_tests; mod feature_flag_tests; +mod message_tests; use super::*; use gpui::executor::Background; diff --git a/crates/collab/src/db/tests/message_tests.rs b/crates/collab/src/db/tests/message_tests.rs new file mode 100644 index 0000000000000000000000000000000000000000..1dd686a1d28534b8e71a6d2b700a8cdd0c467777 --- /dev/null +++ b/crates/collab/src/db/tests/message_tests.rs @@ -0,0 +1,53 @@ +use crate::{ + db::{Database, NewUserParams}, + test_both_dbs, +}; +use std::sync::Arc; +use time::OffsetDateTime; + +test_both_dbs!( + test_channel_message_nonces, + test_channel_message_nonces_postgres, + test_channel_message_nonces_sqlite +); + +async fn test_channel_message_nonces(db: &Arc) { + let user = db + .create_user( + "user@example.com", + false, + NewUserParams { + github_login: "user".into(), + github_user_id: 1, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; + let channel = db + .create_channel("channel", None, "room", user) + .await + .unwrap(); + + let msg1_id = db + .create_channel_message(channel, user, "1", OffsetDateTime::now_utc(), 1) + .await + .unwrap(); + let msg2_id = db + .create_channel_message(channel, user, "2", OffsetDateTime::now_utc(), 2) + .await + .unwrap(); + let msg3_id = db + .create_channel_message(channel, user, "3", OffsetDateTime::now_utc(), 1) + .await + .unwrap(); + let msg4_id = db + .create_channel_message(channel, user, "4", OffsetDateTime::now_utc(), 2) + .await + .unwrap(); + + assert_ne!(msg1_id, msg2_id); + assert_eq!(msg1_id, msg3_id); + assert_eq!(msg2_id, msg4_id); +} diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index e454fcbb9e7a4f2202602ca1ef7947ea6d6b6c9b..b508c45dc53f0a1017d44d1df96643165fc59b53 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -2,7 +2,10 @@ mod connection_pool; use crate::{ auth, - db::{self, ChannelId, ChannelsForUser, Database, ProjectId, RoomId, ServerId, User, UserId}, + db::{ + self, ChannelId, ChannelsForUser, Database, MessageId, ProjectId, RoomId, ServerId, User, + UserId, + }, executor::Executor, AppState, Result, }; @@ -56,6 +59,7 @@ use std::{ }, time::{Duration, Instant}, }; +use time::OffsetDateTime; use tokio::sync::{watch, Semaphore}; use tower::ServiceBuilder; use tracing::{info_span, instrument, Instrument}; @@ -63,6 +67,9 @@ use tracing::{info_span, instrument, Instrument}; pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30); pub const CLEANUP_TIMEOUT: Duration = Duration::from_secs(10); +const MESSAGE_COUNT_PER_PAGE: usize = 100; +const MAX_MESSAGE_LEN: usize = 1024; + lazy_static! { static ref METRIC_CONNECTIONS: IntGauge = register_int_gauge!("connections", "number of connections").unwrap(); @@ -255,6 +262,10 @@ impl Server { .add_request_handler(get_channel_members) .add_request_handler(respond_to_channel_invite) .add_request_handler(join_channel) + .add_request_handler(join_channel_chat) + .add_message_handler(leave_channel_chat) + .add_request_handler(send_channel_message) + .add_request_handler(get_channel_messages) .add_request_handler(follow) .add_message_handler(unfollow) .add_message_handler(update_followers) @@ -2641,6 +2652,112 @@ fn channel_buffer_updated( }); } +async fn send_channel_message( + request: proto::SendChannelMessage, + response: Response, + session: Session, +) -> Result<()> { + // Validate the message body. + let body = request.body.trim().to_string(); + if body.len() > MAX_MESSAGE_LEN { + return Err(anyhow!("message is too long"))?; + } + if body.is_empty() { + return Err(anyhow!("message can't be blank"))?; + } + + let timestamp = OffsetDateTime::now_utc(); + let nonce = request + .nonce + .ok_or_else(|| anyhow!("nonce can't be blank"))?; + + let channel_id = ChannelId::from_proto(request.channel_id); + let (message_id, connection_ids) = session + .db() + .await + .create_channel_message( + channel_id, + session.user_id, + &body, + timestamp, + nonce.clone().into(), + ) + .await?; + let message = proto::ChannelMessage { + sender_id: session.user_id.to_proto(), + id: message_id.to_proto(), + body, + timestamp: timestamp.unix_timestamp() as u64, + nonce: Some(nonce), + }; + broadcast(Some(session.connection_id), connection_ids, |connection| { + session.peer.send( + connection, + proto::ChannelMessageSent { + channel_id: channel_id.to_proto(), + message: Some(message.clone()), + }, + ) + }); + response.send(proto::SendChannelMessageResponse { + message: Some(message), + })?; + Ok(()) +} + +async fn join_channel_chat( + request: proto::JoinChannelChat, + response: Response, + session: Session, +) -> Result<()> { + let channel_id = ChannelId::from_proto(request.channel_id); + + let db = session.db().await; + db.join_channel_chat(channel_id, session.connection_id, session.user_id) + .await?; + let messages = db + .get_channel_messages(channel_id, session.user_id, MESSAGE_COUNT_PER_PAGE, None) + .await?; + response.send(proto::JoinChannelChatResponse { + done: messages.len() < MESSAGE_COUNT_PER_PAGE, + messages, + })?; + Ok(()) +} + +async fn leave_channel_chat(request: proto::LeaveChannelChat, session: Session) -> Result<()> { + let channel_id = ChannelId::from_proto(request.channel_id); + session + .db() + .await + .leave_channel_chat(channel_id, session.connection_id, session.user_id) + .await?; + Ok(()) +} + +async fn get_channel_messages( + request: proto::GetChannelMessages, + response: Response, + session: Session, +) -> Result<()> { + let channel_id = ChannelId::from_proto(request.channel_id); + let messages = session + .db() + .await + .get_channel_messages( + channel_id, + session.user_id, + MESSAGE_COUNT_PER_PAGE, + Some(MessageId::from_proto(request.before_message_id)), + ) + .await?; + response.send(proto::GetChannelMessagesResponse { + done: messages.len() < MESSAGE_COUNT_PER_PAGE, + messages, + })?; + Ok(()) +} + async fn update_diff_base(request: proto::UpdateDiffBase, session: Session) -> Result<()> { let project_id = ProjectId::from_proto(request.project_id); let project_connection_ids = session diff --git a/crates/collab/src/tests.rs b/crates/collab/src/tests.rs index 3000f0d8c351d2c5e3cc77ca89bd2ba344191ed8..b0f5b96fde04dcd9249550ea5975299793e7a894 100644 --- a/crates/collab/src/tests.rs +++ b/crates/collab/src/tests.rs @@ -2,6 +2,7 @@ use call::Room; use gpui::{ModelHandle, TestAppContext}; mod channel_buffer_tests; +mod channel_message_tests; mod channel_tests; mod integration_tests; mod random_channel_buffer_tests; diff --git a/crates/collab/src/tests/channel_message_tests.rs b/crates/collab/src/tests/channel_message_tests.rs new file mode 100644 index 0000000000000000000000000000000000000000..e7afd136f36cfdf5d3be4eb8c9a79b49c0f6fa89 --- /dev/null +++ b/crates/collab/src/tests/channel_message_tests.rs @@ -0,0 +1,56 @@ +use crate::tests::TestServer; +use gpui::{executor::Deterministic, TestAppContext}; +use std::sync::Arc; + +#[gpui::test] +async fn test_basic_channel_messages( + deterministic: Arc, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + deterministic.forbid_parking(); + let mut server = TestServer::start(&deterministic).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + + let channel_id = server + .make_channel("the-channel", (&client_a, cx_a), &mut [(&client_b, cx_b)]) + .await; + + let channel_chat_a = client_a + .channel_store() + .update(cx_a, |store, cx| store.open_channel_chat(channel_id, cx)) + .await + .unwrap(); + let channel_chat_b = client_b + .channel_store() + .update(cx_b, |store, cx| store.open_channel_chat(channel_id, cx)) + .await + .unwrap(); + + channel_chat_a + .update(cx_a, |c, cx| c.send_message("one".into(), cx).unwrap()) + .await + .unwrap(); + channel_chat_a + .update(cx_a, |c, cx| c.send_message("two".into(), cx).unwrap()) + .await + .unwrap(); + + deterministic.run_until_parked(); + channel_chat_b + .update(cx_b, |c, cx| c.send_message("three".into(), cx).unwrap()) + .await + .unwrap(); + + deterministic.run_until_parked(); + channel_chat_a.update(cx_a, |c, _| { + assert_eq!( + c.messages() + .iter() + .map(|m| m.body.as_str()) + .collect::>(), + vec!["one", "two", "three"] + ); + }) +} From ddda5a559b79ff11b9afa29e20a9a3f32799ec85 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 7 Sep 2023 18:06:05 -0700 Subject: [PATCH 003/404] Restore chat functionality with a very rough UI --- crates/channel/Cargo.toml | 1 + crates/channel/src/channel_store.rs | 4 + crates/channel/src/channel_store_tests.rs | 221 ++++++++--------- crates/client/src/test.rs | 9 +- crates/collab/src/db/queries/messages.rs | 5 +- .../collab/src/db/tables/channel_message.rs | 4 +- crates/collab/src/db/tests/message_tests.rs | 6 + crates/collab_ui/src/chat_panel.rs | 229 ++++++++++++++---- crates/collab_ui/src/collab_panel.rs | 15 +- crates/collab_ui/src/collab_ui.rs | 3 +- crates/zed/src/zed.rs | 15 +- styles/src/style_tree/app.ts | 2 + styles/src/style_tree/chat_panel.ts | 111 +++++++++ 13 files changed, 440 insertions(+), 185 deletions(-) create mode 100644 styles/src/style_tree/chat_panel.ts diff --git a/crates/channel/Cargo.toml b/crates/channel/Cargo.toml index c2191fdfa3edaaf0824e5e59ed974a7c53030ccd..00e9135bc1791f7a59e9270f48e9c9282f7b5b5d 100644 --- a/crates/channel/Cargo.toml +++ b/crates/channel/Cargo.toml @@ -47,5 +47,6 @@ tempfile = "3" collections = { path = "../collections", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] } rpc = { path = "../rpc", features = ["test-support"] } +client = { path = "../client", features = ["test-support"] } settings = { path = "../settings", features = ["test-support"] } util = { path = "../util", features = ["test-support"] } diff --git a/crates/channel/src/channel_store.rs b/crates/channel/src/channel_store.rs index 6096692ccba946eb77201750b0cb3a024e1f3b7f..48228041ecceaa18fe23aec902bd9566d877d158 100644 --- a/crates/channel/src/channel_store.rs +++ b/crates/channel/src/channel_store.rs @@ -111,6 +111,10 @@ impl ChannelStore { } } + pub fn client(&self) -> Arc { + self.client.clone() + } + pub fn has_children(&self, channel_id: ChannelId) -> bool { self.channel_paths.iter().any(|path| { if let Some(ix) = path.iter().position(|id| *id == channel_id) { diff --git a/crates/channel/src/channel_store_tests.rs b/crates/channel/src/channel_store_tests.rs index 3ae899ecde8ed2f44628ca9b2c31f44d9caf8bcb..22174f161b4139033b15d004b42ad3388b02fb26 100644 --- a/crates/channel/src/channel_store_tests.rs +++ b/crates/channel/src/channel_store_tests.rs @@ -4,15 +4,12 @@ use super::*; use client::{test::FakeServer, Client, UserStore}; use gpui::{AppContext, ModelHandle, TestAppContext}; use rpc::proto; +use settings::SettingsStore; use util::http::FakeHttpClient; #[gpui::test] fn test_update_channels(cx: &mut AppContext) { - let http = FakeHttpClient::with_404_response(); - let client = Client::new(http.clone(), cx); - let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx)); - - let channel_store = cx.add_model(|cx| ChannelStore::new(client, user_store, cx)); + let channel_store = init_test(cx); update_channels( &channel_store, @@ -80,11 +77,7 @@ fn test_update_channels(cx: &mut AppContext) { #[gpui::test] fn test_dangling_channel_paths(cx: &mut AppContext) { - let http = FakeHttpClient::with_404_response(); - let client = Client::new(http.clone(), cx); - let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx)); - - let channel_store = cx.add_model(|cx| ChannelStore::new(client, user_store, cx)); + let channel_store = init_test(cx); update_channels( &channel_store, @@ -141,18 +134,11 @@ fn test_dangling_channel_paths(cx: &mut AppContext) { #[gpui::test] async fn test_channel_messages(cx: &mut TestAppContext) { - cx.foreground().forbid_parking(); - let user_id = 5; - let http_client = FakeHttpClient::with_404_response(); - let client = cx.update(|cx| Client::new(http_client.clone(), cx)); - let server = FakeServer::for_client(user_id, &client, cx).await; - let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx)); - crate::init(&client); - - let channel_store = cx.add_model(|cx| ChannelStore::new(client, user_store, cx)); - let channel_id = 5; + let channel_store = cx.update(init_test); + let client = channel_store.read_with(cx, |s, _| s.client()); + let server = FakeServer::for_client(user_id, &client, cx).await; // Get the available channels. server.send(proto::UpdateChannels { @@ -163,85 +149,71 @@ async fn test_channel_messages(cx: &mut TestAppContext) { }], ..Default::default() }); - channel_store.next_notification(cx).await; + cx.foreground().run_until_parked(); cx.read(|cx| { assert_channels(&channel_store, &[(0, "the-channel".to_string(), false)], 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(), - }], - }, - ) - .await; + 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.channels().next().unwrap().1.id; - store.open_channel_chat(channel_id, cx) - }) - .await - .unwrap(); - channel.read_with(cx, |channel, _| assert!(channel.messages().is_empty())); + let channel = channel_store.update(cx, |store, cx| { + let channel_id = store.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, - nonce: Some(1.into()), - }, - proto::ChannelMessage { - id: 11, - body: "b".into(), - timestamp: 1001, - sender_id: 6, - nonce: Some(2.into()), - }, - ], - done: false, - }, - ) - .await; + server.respond( + join_channel.receipt(), + proto::JoinChannelChatResponse { + messages: vec![ + proto::ChannelMessage { + id: 10, + body: "a".into(), + timestamp: 1000, + sender_id: 5, + nonce: Some(1.into()), + }, + proto::ChannelMessage { + id: 11, + body: "b".into(), + timestamp: 1001, + sender_id: 6, + nonce: Some(2.into()), + }, + ], + done: false, + }, + ); + + cx.foreground().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(), - }], - }, - ) - .await; - - assert_eq!( - channel.next_event(cx).await, - ChannelChatEvent::MessagesUpdated { - old_range: 0..0, - new_count: 2, - } + 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.read_with(cx, |channel, _| { assert_eq!( channel @@ -270,18 +242,16 @@ async fn test_channel_messages(cx: &mut TestAppContext) { // 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(), - }], - }, - ) - .await; + 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).await, @@ -307,30 +277,28 @@ async fn test_channel_messages(cx: &mut TestAppContext) { 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()), - }, - proto::ChannelMessage { - id: 9, - body: "z".into(), - timestamp: 999, - sender_id: 6, - nonce: Some(5.into()), - }, - ], - }, - ) - .await; + 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()), + }, + proto::ChannelMessage { + id: 9, + body: "z".into(), + timestamp: 999, + sender_id: 6, + nonce: Some(5.into()), + }, + ], + }, + ); assert_eq!( channel.next_event(cx).await, @@ -353,6 +321,19 @@ async fn test_channel_messages(cx: &mut TestAppContext) { }); } +fn init_test(cx: &mut AppContext) -> ModelHandle { + let http = FakeHttpClient::with_404_response(); + let client = Client::new(http.clone(), cx); + let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx)); + + cx.foreground().forbid_parking(); + cx.set_global(SettingsStore::test(cx)); + crate::init(&client); + client::init(&client, cx); + + cx.add_model(|cx| ChannelStore::new(client, user_store, cx)) +} + fn update_channels( channel_store: &ModelHandle, message: proto::UpdateChannels, diff --git a/crates/client/src/test.rs b/crates/client/src/test.rs index 00e7cd1508613c60a05ddbba8cabff86bbaf1d14..38cd12f21cdf74ed895ff9d55ae1995e2efcfafa 100644 --- a/crates/client/src/test.rs +++ b/crates/client/src/test.rs @@ -170,8 +170,7 @@ impl FakeServer { staff: false, flags: Default::default(), }, - ) - .await; + ); continue; } @@ -182,11 +181,7 @@ impl FakeServer { } } - pub async fn respond( - &self, - receipt: Receipt, - response: T::Response, - ) { + pub fn respond(&self, receipt: Receipt, response: T::Response) { self.peer.respond(receipt, response).unwrap() } diff --git a/crates/collab/src/db/queries/messages.rs b/crates/collab/src/db/queries/messages.rs index e8b9ec74165efa388b2af7c3a27d813278a0f461..4593d8f2f3717ce6e9894220095b43a688e3a379 100644 --- a/crates/collab/src/db/queries/messages.rs +++ b/crates/collab/src/db/queries/messages.rs @@ -82,7 +82,7 @@ impl Database { id: row.id.to_proto(), sender_id: row.sender_id.to_proto(), body: row.body, - timestamp: row.sent_at.unix_timestamp() as u64, + timestamp: row.sent_at.assume_utc().unix_timestamp() as u64, nonce: Some(proto::Nonce { upper_half: nonce.0, lower_half: nonce.1, @@ -124,6 +124,9 @@ impl Database { Err(anyhow!("not a chat participant"))?; } + let timestamp = timestamp.to_offset(time::UtcOffset::UTC); + let timestamp = time::PrimitiveDateTime::new(timestamp.date(), timestamp.time()); + let message = channel_message::Entity::insert(channel_message::ActiveModel { channel_id: ActiveValue::Set(channel_id), sender_id: ActiveValue::Set(user_id), diff --git a/crates/collab/src/db/tables/channel_message.rs b/crates/collab/src/db/tables/channel_message.rs index d043d5b6686fdd1b56581217cf63b6187fce4738..ff49c63ba71d675f20f542d3300d74d322d70722 100644 --- a/crates/collab/src/db/tables/channel_message.rs +++ b/crates/collab/src/db/tables/channel_message.rs @@ -1,6 +1,6 @@ use crate::db::{ChannelId, MessageId, UserId}; use sea_orm::entity::prelude::*; -use time::OffsetDateTime; +use time::PrimitiveDateTime; #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] #[sea_orm(table_name = "channel_messages")] @@ -10,7 +10,7 @@ pub struct Model { pub channel_id: ChannelId, pub sender_id: UserId, pub body: String, - pub sent_at: OffsetDateTime, + pub sent_at: PrimitiveDateTime, pub nonce: Uuid, } diff --git a/crates/collab/src/db/tests/message_tests.rs b/crates/collab/src/db/tests/message_tests.rs index 1dd686a1d28534b8e71a6d2b700a8cdd0c467777..c40e53d3559e4e65ab68e5fb061901418c02ab3f 100644 --- a/crates/collab/src/db/tests/message_tests.rs +++ b/crates/collab/src/db/tests/message_tests.rs @@ -30,6 +30,12 @@ async fn test_channel_message_nonces(db: &Arc) { .await .unwrap(); + let owner_id = db.create_server("test").await.unwrap().0 as u32; + + db.join_channel_chat(channel, rpc::ConnectionId { owner_id, id: 0 }, user) + .await + .unwrap(); + let msg1_id = db .create_channel_message(channel, user, "1", OffsetDateTime::now_utc(), 1) .await diff --git a/crates/collab_ui/src/chat_panel.rs b/crates/collab_ui/src/chat_panel.rs index 0105a9d27b2fa67d34b660b7cf18640169286bd7..1cbd8574ebbc112d533f4efcdbabb5d20e00b5c3 100644 --- a/crates/collab_ui/src/chat_panel.rs +++ b/crates/collab_ui/src/chat_panel.rs @@ -1,21 +1,33 @@ +use crate::collab_panel::{CollaborationPanelDockPosition, CollaborationPanelSettings}; +use anyhow::Result; use channel::{ChannelChat, ChannelChatEvent, ChannelMessage, ChannelStore}; use client::Client; +use db::kvp::KEY_VALUE_STORE; use editor::Editor; use gpui::{ actions, elements::*, platform::{CursorStyle, MouseButton}, + serde_json, views::{ItemType, Select, SelectStyle}, - AnyViewHandle, AppContext, Entity, ModelHandle, Subscription, View, ViewContext, ViewHandle, + AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelHandle, Subscription, Task, View, + ViewContext, ViewHandle, WeakViewHandle, }; use language::language_settings::SoftWrap; use menu::Confirm; +use project::Fs; +use serde::{Deserialize, Serialize}; use std::sync::Arc; use theme::Theme; use time::{OffsetDateTime, UtcOffset}; use util::{ResultExt, TryFutureExt}; +use workspace::{ + dock::{DockPosition, Panel}, + Workspace, +}; const MESSAGE_LOADING_THRESHOLD: usize = 50; +const CHAT_PANEL_KEY: &'static str = "ChatPanel"; pub struct ChatPanel { client: Arc, @@ -25,11 +37,25 @@ pub struct ChatPanel { input_editor: ViewHandle, channel_select: ViewHandle, @@ -105,7 +105,7 @@ impl ChatPanel { let mut message_list = ListState::::new(0, Orientation::Bottom, 1000., move |this, ix, cx| { - let message = this.active_channel.as_ref().unwrap().0.read(cx).message(ix); + let message = this.active_chat.as_ref().unwrap().0.read(cx).message(ix); this.render_message(message, cx) }); message_list.set_scroll_handler(|visible_range, this, cx| { @@ -119,7 +119,7 @@ impl ChatPanel { fs, client, channel_store, - active_channel: Default::default(), + active_chat: Default::default(), pending_serialization: Task::ready(None), message_list, input_editor, @@ -151,6 +151,7 @@ impl ChatPanel { cx.observe(&this.channel_select, |this, channel_select, cx| { let selected_ix = channel_select.read(cx).selected_index(); + let selected_channel_id = this .channel_store .read(cx) @@ -216,14 +217,14 @@ impl ChatPanel { fn init_active_channel(&mut self, cx: &mut ViewContext) { let channel_count = self.channel_store.read(cx).channel_count(); self.message_list.reset(0); - self.active_channel = None; + self.active_chat = None; self.channel_select.update(cx, |select, cx| { select.set_item_count(channel_count, cx); }); } - fn set_active_channel(&mut self, chat: ModelHandle, cx: &mut ViewContext) { - if self.active_channel.as_ref().map(|e| &e.0) != Some(&chat) { + fn set_active_chat(&mut self, chat: ModelHandle, cx: &mut ViewContext) { + if self.active_chat.as_ref().map(|e| &e.0) != Some(&chat) { let id = chat.read(cx).channel().id; { let chat = chat.read(cx); @@ -234,7 +235,7 @@ impl ChatPanel { }); } let subscription = cx.subscribe(&chat, Self::channel_did_change); - self.active_channel = Some((chat, subscription)); + self.active_chat = Some((chat, subscription)); self.channel_select.update(cx, |select, cx| { if let Some(ix) = self.channel_store.read(cx).index_of_channel(id) { select.set_selected_index(ix, cx); @@ -275,7 +276,7 @@ impl ChatPanel { } fn render_active_channel_messages(&self) -> AnyElement { - let messages = if self.active_channel.is_some() { + let messages = if self.active_chat.is_some() { List::new(self.message_list.clone()).into_any() } else { Empty::new().into_any() @@ -396,15 +397,15 @@ impl ChatPanel { } fn send(&mut self, _: &Confirm, cx: &mut ViewContext) { - if let Some((channel, _)) = self.active_channel.as_ref() { + if let Some((chat, _)) = self.active_chat.as_ref() { let body = self.input_editor.update(cx, |editor, cx| { let body = editor.text(cx); editor.clear(cx); body }); - if let Some(task) = channel - .update(cx, |channel, cx| channel.send_message(body, cx)) + if let Some(task) = chat + .update(cx, |chat, cx| chat.send_message(body, cx)) .log_err() { task.detach(); @@ -413,8 +414,8 @@ impl ChatPanel { } fn load_more_messages(&mut self, _: &LoadMoreMessages, cx: &mut ViewContext) { - if let Some((channel, _)) = self.active_channel.as_ref() { - channel.update(cx, |channel, cx| { + if let Some((chat, _)) = self.active_chat.as_ref() { + chat.update(cx, |channel, cx| { channel.load_more_messages(cx); }) } @@ -425,13 +426,19 @@ impl ChatPanel { selected_channel_id: u64, cx: &mut ViewContext, ) -> Task> { + if let Some((chat, _)) = &self.active_chat { + if chat.read(cx).channel().id == selected_channel_id { + return Task::ready(Ok(())); + } + } + let open_chat = self.channel_store.update(cx, |store, cx| { store.open_channel_chat(selected_channel_id, cx) }); cx.spawn(|this, mut cx| async move { let chat = open_chat.await?; this.update(&mut cx, |this, cx| { - this.set_active_channel(chat, cx); + this.set_active_chat(chat, cx); }) }) } diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 396ff6df9157ba860468b86976f35aaee1fbfb1f..ad1c2a0037176615ca6911df5db6e5eec1a26ae2 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -80,7 +80,7 @@ struct RenameChannel { } #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] -struct OpenChannelBuffer { +struct OpenChannelNotes { channel_id: u64, } @@ -104,7 +104,7 @@ impl_actions!( ManageMembers, RenameChannel, ToggleCollapse, - OpenChannelBuffer + OpenChannelNotes ] ); @@ -130,7 +130,7 @@ pub fn init(cx: &mut AppContext) { cx.add_action(CollabPanel::toggle_channel_collapsed); cx.add_action(CollabPanel::collapse_selected_channel); cx.add_action(CollabPanel::expand_selected_channel); - cx.add_action(CollabPanel::open_channel_buffer); + cx.add_action(CollabPanel::open_channel_notes); } #[derive(Debug)] @@ -226,10 +226,6 @@ enum ListEntry { channel: Arc, depth: usize, }, - ChannelCall { - channel: Arc, - depth: usize, - }, ChannelNotes { channel_id: ChannelId, }, @@ -369,13 +365,6 @@ impl CollabPanel { return channel_row; } } - ListEntry::ChannelCall { channel, depth } => this.render_channel_call( - &*channel, - *depth, - &theme.collab_panel, - is_selected, - cx, - ), ListEntry::ChannelNotes { channel_id } => this.render_channel_notes( *channel_id, &theme.collab_panel, @@ -751,12 +740,6 @@ impl CollabPanel { channel: channel.clone(), depth, }); - if !channel_store.channel_participants(channel.id).is_empty() { - self.entries.push(ListEntry::ChannelCall { - channel: channel.clone(), - depth, - }); - } } } } @@ -1566,10 +1549,23 @@ impl CollabPanel { let disclosed = has_children.then(|| !self.collapsed_channels.binary_search(&channel_id).is_ok()); + let is_active = iife!({ + let call_channel = ActiveCall::global(cx) + .read(cx) + .room()? + .read(cx) + .channel_id()?; + Some(call_channel == channel_id) + }) + .unwrap_or(false); + + const FACEPILE_LIMIT: usize = 3; + enum ChannelCall {} - enum ChannelNotes {} MouseEventHandler::new::(channel.id as usize, cx, |state, cx| { + let row_hovered = state.hovered(); + Flex::::row() .with_child( Svg::new("icons/hash.svg") @@ -1588,29 +1584,49 @@ impl CollabPanel { .flex(1., true), ) .with_child( - MouseEventHandler::new::(channel_id as usize, cx, |_, _| { - Svg::new("icons/radix/speaker-loud.svg") - .with_color(theme.channel_hash.color) - .constrained() - .with_width(theme.channel_hash.width) - .aligned() - .right() - }) - .on_click(MouseButton::Left, move |_, this, cx| { - this.join_channel_call(channel_id, cx) - }), - ) - .with_child( - MouseEventHandler::new::(channel_id as usize, cx, |_, _| { - Svg::new("icons/radix/file.svg") - .with_color(theme.channel_hash.color) - .constrained() - .with_width(theme.channel_hash.width) - .aligned() - .right() - }) + MouseEventHandler::new::( + channel.id as usize, + cx, + move |_, cx| { + let participants = + self.channel_store.read(cx).channel_participants(channel_id); + if !participants.is_empty() { + let extra_count = participants.len().saturating_sub(FACEPILE_LIMIT); + + FacePile::new(theme.face_overlap) + .with_children( + participants + .iter() + .filter_map(|user| { + Some( + Image::from_data(user.avatar.clone()?) + .with_style(theme.channel_avatar), + ) + }) + .take(FACEPILE_LIMIT), + ) + .with_children((extra_count > 0).then(|| { + Label::new( + format!("+{}", extra_count), + theme.extra_participant_label.text.clone(), + ) + .contained() + .with_style(theme.extra_participant_label.container) + })) + .into_any() + } else if row_hovered { + Svg::new("icons/radix/speaker-loud.svg") + .with_color(theme.channel_hash.color) + .constrained() + .with_width(theme.channel_hash.width) + .into_any() + } else { + Empty::new().into_any() + } + }, + ) .on_click(MouseButton::Left, move |_, this, cx| { - this.open_channel_buffer(&OpenChannelBuffer { channel_id }, cx); + this.join_channel_call(channel_id, cx); }), ) .align_children_center() @@ -1622,7 +1638,7 @@ impl CollabPanel { .constrained() .with_height(theme.row_height) .contained() - .with_style(*theme.channel_row.style_for(is_selected, state)) + .with_style(*theme.channel_row.style_for(is_selected || is_active, state)) .with_padding_left( theme.channel_row.default_style().padding.left + theme.channel_indent * depth as f32, @@ -1638,94 +1654,6 @@ impl CollabPanel { .into_any() } - fn render_channel_call( - &self, - channel: &Channel, - depth: usize, - theme: &theme::CollabPanel, - is_selected: bool, - cx: &mut ViewContext, - ) -> AnyElement { - let channel_id = channel.id; - - let is_active = iife!({ - let call_channel = ActiveCall::global(cx) - .read(cx) - .room()? - .read(cx) - .channel_id()?; - Some(call_channel == channel_id) - }) - .unwrap_or(false); - - const FACEPILE_LIMIT: usize = 5; - - enum ChannelCall {} - - let host_avatar_width = theme - .contact_avatar - .width - .or(theme.contact_avatar.height) - .unwrap_or(0.); - - MouseEventHandler::new::(channel.id as usize, cx, |state, cx| { - let participants = self.channel_store.read(cx).channel_participants(channel_id); - let extra_count = participants.len().saturating_sub(FACEPILE_LIMIT); - let tree_branch = *theme.tree_branch.in_state(is_selected).style_for(state); - let row = theme.project_row.in_state(is_selected).style_for(state); - - Flex::::row() - .with_child(render_tree_branch( - tree_branch, - &row.name.text, - true, - vec2f(host_avatar_width, theme.row_height), - cx.font_cache(), - )) - .with_child( - FacePile::new(theme.face_overlap) - .with_children( - participants - .iter() - .filter_map(|user| { - Some( - Image::from_data(user.avatar.clone()?) - .with_style(theme.channel_avatar), - ) - }) - .take(FACEPILE_LIMIT), - ) - .with_children((extra_count > 0).then(|| { - Label::new( - format!("+{}", extra_count), - theme.extra_participant_label.text.clone(), - ) - .contained() - .with_style(theme.extra_participant_label.container) - })), - ) - .align_children_center() - .constrained() - .with_height(theme.row_height) - .aligned() - .left() - .contained() - .with_style(*theme.channel_row.style_for(is_selected || is_active, state)) - .with_padding_left( - theme.channel_row.default_style().padding.left - + theme.channel_indent * (depth + 1) as f32, - ) - }) - .on_click(MouseButton::Left, move |_, this, cx| { - this.join_channel_call(channel_id, cx); - }) - .on_click(MouseButton::Right, move |e, this, cx| { - this.deploy_channel_context_menu(Some(e.position), channel_id, cx); - }) - .with_cursor_style(CursorStyle::PointingHand) - .into_any() - } - fn render_channel_notes( &self, channel_id: ChannelId, @@ -1775,7 +1703,7 @@ impl CollabPanel { .with_padding_left(theme.channel_row.default_style().padding.left) }) .on_click(MouseButton::Left, move |_, this, cx| { - this.open_channel_buffer(&OpenChannelBuffer { channel_id }, cx); + this.open_channel_notes(&OpenChannelNotes { channel_id }, cx); }) .with_cursor_style(CursorStyle::PointingHand) .into_any() @@ -1987,7 +1915,7 @@ impl CollabPanel { let mut items = vec![ ContextMenuItem::action(expand_action_name, ToggleCollapse { channel_id }), - ContextMenuItem::action("Open Notes", OpenChannelBuffer { channel_id }), + ContextMenuItem::action("Open Notes", OpenChannelNotes { channel_id }), ]; if self.channel_store.read(cx).is_user_admin(channel_id) { @@ -2114,9 +2042,6 @@ impl CollabPanel { ListEntry::Channel { channel, .. } => { self.join_channel_chat(channel.id, cx); } - ListEntry::ChannelCall { channel, .. } => { - self.join_channel_call(channel.id, cx); - } ListEntry::ContactPlaceholder => self.toggle_contact_finder(cx), _ => {} } @@ -2325,7 +2250,7 @@ impl CollabPanel { } } - fn open_channel_buffer(&mut self, action: &OpenChannelBuffer, cx: &mut ViewContext) { + fn open_channel_notes(&mut self, action: &OpenChannelNotes, cx: &mut ViewContext) { if let Some(workspace) = self.workspace.upgrade(cx) { let pane = workspace.read(cx).active_pane().clone(); let channel_id = action.channel_id; @@ -2510,7 +2435,9 @@ impl CollabPanel { .detach_and_log_err(cx); } - fn join_channel_chat(&self, channel_id: u64, cx: &mut ViewContext) { + fn join_channel_chat(&mut self, channel_id: u64, cx: &mut ViewContext) { + self.open_channel_notes(&OpenChannelNotes { channel_id }, cx); + if let Some(workspace) = self.workspace.upgrade(cx) { cx.app_context().defer(move |cx| { workspace.update(cx, |workspace, cx| { @@ -2757,18 +2684,6 @@ impl PartialEq for ListEntry { return channel_1.id == channel_2.id && depth_1 == depth_2; } } - ListEntry::ChannelCall { - channel: channel_1, - depth: depth_1, - } => { - if let ListEntry::ChannelCall { - channel: channel_2, - depth: depth_2, - } = other - { - return channel_1.id == channel_2.id && depth_1 == depth_2; - } - } ListEntry::ChannelNotes { channel_id } => { if let ListEntry::ChannelNotes { channel_id: other_id, From d868d00985ba29df4605c202f8e3d4f04572cda3 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 11 Sep 2023 18:01:58 -0600 Subject: [PATCH 024/404] vim: ALlow counts on insert actions This re-uses the existing repeat infrastructure. --- assets/keymaps/default.json | 2 +- assets/keymaps/vim.json | 2 +- crates/vim/src/insert.rs | 119 +++++++- crates/vim/src/normal.rs | 2 +- crates/vim/src/normal/repeat.rs | 271 +++++++++++------- crates/vim/src/vim.rs | 14 +- .../test_data/test_insert_with_counts.json | 36 +++ .../test_data/test_insert_with_repeat.json | 23 ++ 8 files changed, 340 insertions(+), 129 deletions(-) create mode 100644 crates/vim/test_data/test_insert_with_counts.json create mode 100644 crates/vim/test_data/test_insert_with_repeat.json diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index fa62a74f3f7e27485cb0f36abd4c5ab5ab82ea38..2fb1c6f5fcaa8baf4c3a128644b372408260c501 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -533,7 +533,7 @@ // TODO: Move this to a dock open action "cmd-shift-c": "collab_panel::ToggleFocus", "cmd-alt-i": "zed::DebugElements", - "ctrl-:": "editor::ToggleInlayHints", + "ctrl-:": "editor::ToggleInlayHints" } }, { diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index bbc0a51b28109174d84a937c2397ea54f0c6aed1..1a7b81ee8f5cb7e16e1290b1e7141d834ee6a887 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -499,7 +499,7 @@ "around": true } } - ], + ] } }, { diff --git a/crates/vim/src/insert.rs b/crates/vim/src/insert.rs index 9141a02ab3550c29262f235348e9beadfde15d9e..7495b302a29db477521fab56ddd8ee237bc0ed21 100644 --- a/crates/vim/src/insert.rs +++ b/crates/vim/src/insert.rs @@ -1,6 +1,6 @@ -use crate::{state::Mode, Vim}; +use crate::{normal::repeat, state::Mode, Vim}; use editor::{scroll::autoscroll::Autoscroll, Bias}; -use gpui::{actions, AppContext, ViewContext}; +use gpui::{actions, Action, AppContext, ViewContext}; use language::SelectionGoal; use workspace::Workspace; @@ -10,24 +10,41 @@ pub fn init(cx: &mut AppContext) { cx.add_action(normal_before); } -fn normal_before(_: &mut Workspace, _: &NormalBefore, cx: &mut ViewContext) { - Vim::update(cx, |vim, cx| { - vim.stop_recording(); - vim.update_active_editor(cx, |editor, cx| { - editor.change_selections(Some(Autoscroll::fit()), cx, |s| { - s.move_cursors_with(|map, mut cursor, _| { - *cursor.column_mut() = cursor.column().saturating_sub(1); - (map.clip_point(cursor, Bias::Left), SelectionGoal::None) +fn normal_before(_: &mut Workspace, action: &NormalBefore, cx: &mut ViewContext) { + let should_repeat = Vim::update(cx, |vim, cx| { + let count = vim.take_count().unwrap_or(1); + vim.stop_recording_immediately(action.boxed_clone()); + if count <= 1 || vim.workspace_state.replaying { + vim.update_active_editor(cx, |editor, cx| { + editor.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.move_cursors_with(|map, mut cursor, _| { + *cursor.column_mut() = cursor.column().saturating_sub(1); + (map.clip_point(cursor, Bias::Left), SelectionGoal::None) + }); }); }); - }); - vim.switch_mode(Mode::Normal, false, cx); - }) + vim.switch_mode(Mode::Normal, false, cx); + false + } else { + true + } + }); + + if should_repeat { + repeat::repeat(cx, true) + } } #[cfg(test)] mod test { - use crate::{state::Mode, test::VimTestContext}; + use std::sync::Arc; + + use gpui::executor::Deterministic; + + use crate::{ + state::Mode, + test::{NeovimBackedTestContext, VimTestContext}, + }; #[gpui::test] async fn test_enter_and_exit_insert_mode(cx: &mut gpui::TestAppContext) { @@ -40,4 +57,78 @@ mod test { assert_eq!(cx.mode(), Mode::Normal); cx.assert_editor_state("Tesˇt"); } + + #[gpui::test] + async fn test_insert_with_counts( + deterministic: Arc, + cx: &mut gpui::TestAppContext, + ) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.set_shared_state("ˇhello\n").await; + cx.simulate_shared_keystrokes(["5", "i", "-", "escape"]) + .await; + deterministic.run_until_parked(); + cx.assert_shared_state("----ˇ-hello\n").await; + + cx.set_shared_state("ˇhello\n").await; + cx.simulate_shared_keystrokes(["5", "a", "-", "escape"]) + .await; + deterministic.run_until_parked(); + cx.assert_shared_state("h----ˇ-ello\n").await; + + cx.simulate_shared_keystrokes(["4", "shift-i", "-", "escape"]) + .await; + deterministic.run_until_parked(); + cx.assert_shared_state("---ˇ-h-----ello\n").await; + + cx.simulate_shared_keystrokes(["3", "shift-a", "-", "escape"]) + .await; + deterministic.run_until_parked(); + cx.assert_shared_state("----h-----ello--ˇ-\n").await; + + cx.set_shared_state("ˇhello\n").await; + cx.simulate_shared_keystrokes(["3", "o", "o", "i", "escape"]) + .await; + deterministic.run_until_parked(); + cx.assert_shared_state("hello\noi\noi\noˇi\n").await; + + cx.set_shared_state("ˇhello\n").await; + cx.simulate_shared_keystrokes(["3", "shift-o", "o", "i", "escape"]) + .await; + deterministic.run_until_parked(); + cx.assert_shared_state("oi\noi\noˇi\nhello\n").await; + } + + #[gpui::test] + async fn test_insert_with_repeat( + deterministic: Arc, + cx: &mut gpui::TestAppContext, + ) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.set_shared_state("ˇhello\n").await; + cx.simulate_shared_keystrokes(["3", "i", "-", "escape"]) + .await; + deterministic.run_until_parked(); + cx.assert_shared_state("--ˇ-hello\n").await; + cx.simulate_shared_keystrokes(["."]).await; + deterministic.run_until_parked(); + cx.assert_shared_state("----ˇ--hello\n").await; + cx.simulate_shared_keystrokes(["2", "."]).await; + deterministic.run_until_parked(); + cx.assert_shared_state("-----ˇ---hello\n").await; + + cx.set_shared_state("ˇhello\n").await; + cx.simulate_shared_keystrokes(["2", "o", "k", "k", "escape"]) + .await; + deterministic.run_until_parked(); + cx.assert_shared_state("hello\nkk\nkˇk\n").await; + cx.simulate_shared_keystrokes(["."]).await; + deterministic.run_until_parked(); + cx.assert_shared_state("hello\nkk\nkk\nkk\nkˇk\n").await; + cx.simulate_shared_keystrokes(["1", "."]).await; + deterministic.run_until_parked(); + cx.assert_shared_state("hello\nkk\nkk\nkk\nkk\nkˇk\n").await; + } } diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index 3ef3f9ddd327e9d29ab9049cfd27301de7e1845c..16a4150dabafd578eb4347d38956bef56147290a 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -2,7 +2,7 @@ mod case; mod change; mod delete; mod paste; -mod repeat; +pub(crate) mod repeat; mod scroll; mod search; pub mod substitute; diff --git a/crates/vim/src/normal/repeat.rs b/crates/vim/src/normal/repeat.rs index 28f9e3c2a43359258856b1ea80d0373dd566f6b3..6954ace71fed459ec541e3ddf30e2ea14caed4af 100644 --- a/crates/vim/src/normal/repeat.rs +++ b/crates/vim/src/normal/repeat.rs @@ -1,10 +1,11 @@ use crate::{ + insert::NormalBefore, motion::Motion, state::{Mode, RecordedSelection, ReplayableAction}, visual::visual_motion, Vim, }; -use gpui::{actions, Action, AppContext}; +use gpui::{actions, Action, AppContext, WindowContext}; use workspace::Workspace; actions!(vim, [Repeat, EndRepeat,]); @@ -17,6 +18,27 @@ fn should_replay(action: &Box) -> bool { true } +fn repeatable_insert(action: &ReplayableAction) -> Option> { + match action { + ReplayableAction::Action(action) => { + if super::InsertBefore.id() == action.id() + || super::InsertAfter.id() == action.id() + || super::InsertFirstNonWhitespace.id() == action.id() + || super::InsertEndOfLine.id() == action.id() + { + Some(super::InsertBefore.boxed_clone()) + } else if super::InsertLineAbove.id() == action.id() + || super::InsertLineBelow.id() == action.id() + { + Some(super::InsertLineBelow.boxed_clone()) + } else { + None + } + } + ReplayableAction::Insertion { .. } => None, + } +} + pub(crate) fn init(cx: &mut AppContext) { cx.add_action(|_: &mut Workspace, _: &EndRepeat, cx| { Vim::update(cx, |vim, cx| { @@ -28,127 +50,156 @@ pub(crate) fn init(cx: &mut AppContext) { }); }); - cx.add_action(|_: &mut Workspace, _: &Repeat, cx| { - let Some((actions, editor, selection)) = Vim::update(cx, |vim, cx| { - let actions = vim.workspace_state.recorded_actions.clone(); - let Some(editor) = vim.active_editor.clone() else { - return None; - }; - let count = vim.take_count(); - - vim.workspace_state.replaying = true; - - let selection = vim.workspace_state.recorded_selection.clone(); - match selection { - RecordedSelection::SingleLine { .. } | RecordedSelection::Visual { .. } => { - vim.workspace_state.recorded_count = None; - vim.switch_mode(Mode::Visual, false, cx) - } - RecordedSelection::VisualLine { .. } => { - vim.workspace_state.recorded_count = None; - vim.switch_mode(Mode::VisualLine, false, cx) - } - RecordedSelection::VisualBlock { .. } => { - vim.workspace_state.recorded_count = None; - vim.switch_mode(Mode::VisualBlock, false, cx) - } - RecordedSelection::None => { - if let Some(count) = count { - vim.workspace_state.recorded_count = Some(count); - } - } - } - - if let Some(editor) = editor.upgrade(cx) { - editor.update(cx, |editor, _| { - editor.show_local_selections = false; - }) - } else { - return None; - } + cx.add_action(|_: &mut Workspace, _: &Repeat, cx| repeat(cx, false)); +} - Some((actions, editor, selection)) - }) else { - return; +pub(crate) fn repeat(cx: &mut WindowContext, from_insert_mode: bool) { + let Some((mut actions, editor, selection)) = Vim::update(cx, |vim, cx| { + let actions = vim.workspace_state.recorded_actions.clone(); + let Some(editor) = vim.active_editor.clone() else { + return None; }; + let count = vim.take_count(); + let selection = vim.workspace_state.recorded_selection.clone(); match selection { - RecordedSelection::SingleLine { cols } => { - if cols > 1 { - visual_motion(Motion::Right, Some(cols as usize - 1), cx) - } + RecordedSelection::SingleLine { .. } | RecordedSelection::Visual { .. } => { + vim.workspace_state.recorded_count = None; + vim.switch_mode(Mode::Visual, false, cx) } - RecordedSelection::Visual { rows, cols } => { - visual_motion( - Motion::Down { - display_lines: false, - }, - Some(rows as usize), - cx, - ); - visual_motion( - Motion::StartOfLine { - display_lines: false, - }, - None, - cx, - ); - if cols > 1 { - visual_motion(Motion::Right, Some(cols as usize - 1), cx) - } + RecordedSelection::VisualLine { .. } => { + vim.workspace_state.recorded_count = None; + vim.switch_mode(Mode::VisualLine, false, cx) } - RecordedSelection::VisualBlock { rows, cols } => { - visual_motion( - Motion::Down { - display_lines: false, - }, - Some(rows as usize), - cx, - ); - if cols > 1 { - visual_motion(Motion::Right, Some(cols as usize - 1), cx); + RecordedSelection::VisualBlock { .. } => { + vim.workspace_state.recorded_count = None; + vim.switch_mode(Mode::VisualBlock, false, cx) + } + RecordedSelection::None => { + if let Some(count) = count { + vim.workspace_state.recorded_count = Some(count); } } - RecordedSelection::VisualLine { rows } => { - visual_motion( - Motion::Down { - display_lines: false, - }, - Some(rows as usize), - cx, - ); + } + + if let Some(editor) = editor.upgrade(cx) { + editor.update(cx, |editor, _| { + editor.show_local_selections = false; + }) + } else { + return None; + } + + Some((actions, editor, selection)) + }) else { + return; + }; + + match selection { + RecordedSelection::SingleLine { cols } => { + if cols > 1 { + visual_motion(Motion::Right, Some(cols as usize - 1), cx) + } + } + RecordedSelection::Visual { rows, cols } => { + visual_motion( + Motion::Down { + display_lines: false, + }, + Some(rows as usize), + cx, + ); + visual_motion( + Motion::StartOfLine { + display_lines: false, + }, + None, + cx, + ); + if cols > 1 { + visual_motion(Motion::Right, Some(cols as usize - 1), cx) + } + } + RecordedSelection::VisualBlock { rows, cols } => { + visual_motion( + Motion::Down { + display_lines: false, + }, + Some(rows as usize), + cx, + ); + if cols > 1 { + visual_motion(Motion::Right, Some(cols as usize - 1), cx); + } + } + RecordedSelection::VisualLine { rows } => { + visual_motion( + Motion::Down { + display_lines: false, + }, + Some(rows as usize), + cx, + ); + } + RecordedSelection::None => {} + } + + // insert internally uses repeat to handle counts + // vim doesn't treat 3a1 as though you literally repeated a1 + // 3 times, instead it inserts the content thrice at the insert position. + if let Some(to_repeat) = repeatable_insert(&actions[0]) { + if let Some(ReplayableAction::Action(action)) = actions.last() { + if action.id() == NormalBefore.id() { + actions.pop(); } - RecordedSelection::None => {} } - let window = cx.window(); - cx.app_context() - .spawn(move |mut cx| async move { - for action in actions { - match action { - ReplayableAction::Action(action) => { - if should_replay(&action) { - window - .dispatch_action(editor.id(), action.as_ref(), &mut cx) - .ok_or_else(|| anyhow::anyhow!("window was closed")) - } else { - Ok(()) - } + let mut new_actions = actions.clone(); + actions[0] = ReplayableAction::Action(to_repeat.boxed_clone()); + + let mut count = Vim::read(cx).workspace_state.recorded_count.unwrap_or(1); + + // if we came from insert mode we're just doing repititions 2 onwards. + if from_insert_mode { + count -= 1; + new_actions[0] = actions[0].clone(); + } + + for _ in 1..count { + new_actions.append(actions.clone().as_mut()); + } + new_actions.push(ReplayableAction::Action(NormalBefore.boxed_clone())); + actions = new_actions; + } + + Vim::update(cx, |vim, _| vim.workspace_state.replaying = true); + let window = cx.window(); + cx.app_context() + .spawn(move |mut cx| async move { + for action in actions { + match action { + ReplayableAction::Action(action) => { + if should_replay(&action) { + window + .dispatch_action(editor.id(), action.as_ref(), &mut cx) + .ok_or_else(|| anyhow::anyhow!("window was closed")) + } else { + Ok(()) } - ReplayableAction::Insertion { - text, - utf16_range_to_replace, - } => editor.update(&mut cx, |editor, cx| { - editor.replay_insert_event(&text, utf16_range_to_replace.clone(), cx) - }), - }? - } - window - .dispatch_action(editor.id(), &EndRepeat, &mut cx) - .ok_or_else(|| anyhow::anyhow!("window was closed")) - }) - .detach_and_log_err(cx); - }); + } + ReplayableAction::Insertion { + text, + utf16_range_to_replace, + } => editor.update(&mut cx, |editor, cx| { + editor.replay_insert_event(&text, utf16_range_to_replace.clone(), cx) + }), + }? + } + window + .dispatch_action(editor.id(), &EndRepeat, &mut cx) + .ok_or_else(|| anyhow::anyhow!("window was closed")) + }) + .detach_and_log_err(cx); } #[cfg(test)] diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 74363bc7b77e844ccea6936182a3f4e7b8c82ed1..fea6f26ef1816defa70cd51363f46ebba021831f 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -15,8 +15,8 @@ use anyhow::Result; use collections::{CommandPaletteFilter, HashMap}; use editor::{movement, Editor, EditorMode, Event}; use gpui::{ - actions, impl_actions, keymap_matcher::KeymapContext, keymap_matcher::MatchResult, AppContext, - Subscription, ViewContext, ViewHandle, WeakViewHandle, WindowContext, + actions, impl_actions, keymap_matcher::KeymapContext, keymap_matcher::MatchResult, Action, + AppContext, Subscription, ViewContext, ViewHandle, WeakViewHandle, WindowContext, }; use language::{CursorShape, Point, Selection, SelectionGoal}; pub use mode_indicator::ModeIndicator; @@ -284,6 +284,16 @@ impl Vim { } } + pub fn stop_recording_immediately(&mut self, action: Box) { + if self.workspace_state.recording { + self.workspace_state + .recorded_actions + .push(ReplayableAction::Action(action.boxed_clone())); + self.workspace_state.recording = false; + self.workspace_state.stop_recording_after_next_action = false; + } + } + pub fn record_current_action(&mut self, cx: &mut WindowContext) { self.start_recording(cx); self.stop_recording(); diff --git a/crates/vim/test_data/test_insert_with_counts.json b/crates/vim/test_data/test_insert_with_counts.json new file mode 100644 index 0000000000000000000000000000000000000000..470888cf6e94b78351a9479401b1b8d121a73510 --- /dev/null +++ b/crates/vim/test_data/test_insert_with_counts.json @@ -0,0 +1,36 @@ +{"Put":{"state":"ˇhello\n"}} +{"Key":"5"} +{"Key":"i"} +{"Key":"-"} +{"Key":"escape"} +{"Get":{"state":"----ˇ-hello\n","mode":"Normal"}} +{"Put":{"state":"ˇhello\n"}} +{"Key":"5"} +{"Key":"a"} +{"Key":"-"} +{"Key":"escape"} +{"Get":{"state":"h----ˇ-ello\n","mode":"Normal"}} +{"Key":"4"} +{"Key":"shift-i"} +{"Key":"-"} +{"Key":"escape"} +{"Get":{"state":"---ˇ-h-----ello\n","mode":"Normal"}} +{"Key":"3"} +{"Key":"shift-a"} +{"Key":"-"} +{"Key":"escape"} +{"Get":{"state":"----h-----ello--ˇ-\n","mode":"Normal"}} +{"Put":{"state":"ˇhello\n"}} +{"Key":"3"} +{"Key":"o"} +{"Key":"o"} +{"Key":"i"} +{"Key":"escape"} +{"Get":{"state":"hello\noi\noi\noˇi\n","mode":"Normal"}} +{"Put":{"state":"ˇhello\n"}} +{"Key":"3"} +{"Key":"shift-o"} +{"Key":"o"} +{"Key":"i"} +{"Key":"escape"} +{"Get":{"state":"oi\noi\noˇi\nhello\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_insert_with_repeat.json b/crates/vim/test_data/test_insert_with_repeat.json new file mode 100644 index 0000000000000000000000000000000000000000..ac6637633c384a3d5e129ef59bd82a2775f87342 --- /dev/null +++ b/crates/vim/test_data/test_insert_with_repeat.json @@ -0,0 +1,23 @@ +{"Put":{"state":"ˇhello\n"}} +{"Key":"3"} +{"Key":"i"} +{"Key":"-"} +{"Key":"escape"} +{"Get":{"state":"--ˇ-hello\n","mode":"Normal"}} +{"Key":"."} +{"Get":{"state":"----ˇ--hello\n","mode":"Normal"}} +{"Key":"2"} +{"Key":"."} +{"Get":{"state":"-----ˇ---hello\n","mode":"Normal"}} +{"Put":{"state":"ˇhello\n"}} +{"Key":"2"} +{"Key":"o"} +{"Key":"k"} +{"Key":"k"} +{"Key":"escape"} +{"Get":{"state":"hello\nkk\nkˇk\n","mode":"Normal"}} +{"Key":"."} +{"Get":{"state":"hello\nkk\nkk\nkk\nkˇk\n","mode":"Normal"}} +{"Key":"1"} +{"Key":"."} +{"Get":{"state":"hello\nkk\nkk\nkk\nkk\nkˇk\n","mode":"Normal"}} From f2112b9aad44fab5092d9f640d3617955b470419 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 11 Sep 2023 17:11:33 -0700 Subject: [PATCH 025/404] Rejoin channel chats upon reconnecting --- crates/channel/src/channel_store.rs | 10 +++ .../collab/src/tests/channel_message_tests.rs | 85 ++++++++++++++++++- 2 files changed, 93 insertions(+), 2 deletions(-) diff --git a/crates/channel/src/channel_store.rs b/crates/channel/src/channel_store.rs index b90a3fdaeb5577eff3be6db5fa79147e69f87078..e61e520b471029e692ecdc29bac58777917d92ba 100644 --- a/crates/channel/src/channel_store.rs +++ b/crates/channel/src/channel_store.rs @@ -532,6 +532,16 @@ impl ChannelStore { fn handle_connect(&mut self, cx: &mut ModelContext) -> Task> { 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(cx) { + 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 { diff --git a/crates/collab/src/tests/channel_message_tests.rs b/crates/collab/src/tests/channel_message_tests.rs index e7afd136f36cfdf5d3be4eb8c9a79b49c0f6fa89..f74416fa7d8429cd0703c9e010e608322f87a96b 100644 --- a/crates/collab/src/tests/channel_message_tests.rs +++ b/crates/collab/src/tests/channel_message_tests.rs @@ -1,5 +1,6 @@ -use crate::tests::TestServer; -use gpui::{executor::Deterministic, TestAppContext}; +use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer}; +use channel::ChannelChat; +use gpui::{executor::Deterministic, ModelHandle, TestAppContext}; use std::sync::Arc; #[gpui::test] @@ -54,3 +55,83 @@ async fn test_basic_channel_messages( ); }) } + +#[gpui::test] +async fn test_rejoin_channel_chat( + deterministic: Arc, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + deterministic.forbid_parking(); + let mut server = TestServer::start(&deterministic).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + + let channel_id = server + .make_channel("the-channel", (&client_a, cx_a), &mut [(&client_b, cx_b)]) + .await; + + let channel_chat_a = client_a + .channel_store() + .update(cx_a, |store, cx| store.open_channel_chat(channel_id, cx)) + .await + .unwrap(); + let channel_chat_b = client_b + .channel_store() + .update(cx_b, |store, cx| store.open_channel_chat(channel_id, cx)) + .await + .unwrap(); + + channel_chat_a + .update(cx_a, |c, cx| c.send_message("one".into(), cx).unwrap()) + .await + .unwrap(); + channel_chat_b + .update(cx_b, |c, cx| c.send_message("two".into(), cx).unwrap()) + .await + .unwrap(); + + server.forbid_connections(); + server.disconnect_client(client_a.peer_id().unwrap()); + + // While client A is disconnected, clients A and B both send new messages. + channel_chat_a + .update(cx_a, |c, cx| c.send_message("three".into(), cx).unwrap()) + .await + .unwrap_err(); + channel_chat_a + .update(cx_a, |c, cx| c.send_message("four".into(), cx).unwrap()) + .await + .unwrap_err(); + channel_chat_b + .update(cx_b, |c, cx| c.send_message("five".into(), cx).unwrap()) + .await + .unwrap(); + channel_chat_b + .update(cx_b, |c, cx| c.send_message("six".into(), cx).unwrap()) + .await + .unwrap(); + + // Client A reconnects. + server.allow_connections(); + deterministic.advance_clock(RECONNECT_TIMEOUT); + + // Client A fetches the messages that were sent while they were disconnected + // and resends their own messages which failed to send. + let expected_messages = &["one", "two", "five", "six", "three", "four"]; + assert_messages(&channel_chat_a, expected_messages, cx_a); + assert_messages(&channel_chat_b, expected_messages, cx_b); +} + +#[track_caller] +fn assert_messages(chat: &ModelHandle, messages: &[&str], cx: &mut TestAppContext) { + chat.update(cx, |chat, _| { + assert_eq!( + chat.messages() + .iter() + .map(|m| m.body.as_str()) + .collect::>(), + messages + ); + }) +} From 76d55244a1c28f7a5132717fec99a51373cb5b76 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 11 Sep 2023 18:30:31 -0600 Subject: [PATCH 026/404] Clear counts when switching modes --- crates/vim/src/test.rs | 19 +++++++++++++++++++ crates/vim/src/vim.rs | 4 ++++ crates/vim/test_data/test_clear_counts.json | 7 +++++++ 3 files changed, 30 insertions(+) create mode 100644 crates/vim/test_data/test_clear_counts.json diff --git a/crates/vim/src/test.rs b/crates/vim/src/test.rs index 9aa9fffc0a76905e65ab49d6fda6a85c64dad484..70a0b7c7a64a535b3e63e3e011c1c16ffc5336f0 100644 --- a/crates/vim/src/test.rs +++ b/crates/vim/src/test.rs @@ -574,3 +574,22 @@ async fn test_folds(cx: &mut gpui::TestAppContext) { "}) .await; } + +#[gpui::test] +async fn test_clear_counts(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.set_shared_state(indoc! {" + The quick brown + fox juˇmps over + the lazy dog"}) + .await; + + cx.simulate_shared_keystrokes(["4", "escape", "3", "d", "l"]) + .await; + cx.set_shared_state(indoc! {" + The quick brown + fox juˇ over + the lazy dog"}) + .await; +} diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index fea6f26ef1816defa70cd51363f46ebba021831f..c6488fe171c5d352750d7e8fc4f380134ab199b7 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -308,6 +308,9 @@ impl Vim { state.mode = mode; state.operator_stack.clear(); }); + if mode != Mode::Insert { + self.take_count(); + } cx.emit_global(VimEvent::ModeChanged { mode }); @@ -412,6 +415,7 @@ impl Vim { popped_operator } fn clear_operator(&mut self, cx: &mut WindowContext) { + self.take_count(); self.update_state(|state| state.operator_stack.clear()); self.sync_vim_settings(cx); } diff --git a/crates/vim/test_data/test_clear_counts.json b/crates/vim/test_data/test_clear_counts.json new file mode 100644 index 0000000000000000000000000000000000000000..8bc78de7d367678b32ec2a19ef9d5109a8708854 --- /dev/null +++ b/crates/vim/test_data/test_clear_counts.json @@ -0,0 +1,7 @@ +{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog"}} +{"Key":"4"} +{"Key":"escape"} +{"Key":"3"} +{"Key":"d"} +{"Key":"l"} +{"Put":{"state":"The quick brown\nfox juˇ over\nthe lazy dog"}} From 1c50587cad882e7fe751720fd4d7aee1b3dc4740 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 11 Sep 2023 17:37:05 -0700 Subject: [PATCH 027/404] Remove channel chat participant when connection is lost --- crates/collab/src/db/queries/buffers.rs | 23 ++++++++ crates/collab/src/db/queries/messages.rs | 19 ++++++ crates/collab/src/db/queries/rooms.rs | 73 ++++++++++-------------- crates/collab/src/rpc.rs | 3 +- 4 files changed, 74 insertions(+), 44 deletions(-) diff --git a/crates/collab/src/db/queries/buffers.rs b/crates/collab/src/db/queries/buffers.rs index 00de20140320640d98517556a5321433ad891f86..62ead11932af5cdab9049d719eef7b1238b73ffc 100644 --- a/crates/collab/src/db/queries/buffers.rs +++ b/crates/collab/src/db/queries/buffers.rs @@ -249,6 +249,29 @@ impl Database { .await } + pub async fn channel_buffer_connection_lost( + &self, + connection: ConnectionId, + tx: &DatabaseTransaction, + ) -> Result<()> { + channel_buffer_collaborator::Entity::update_many() + .filter( + Condition::all() + .add(channel_buffer_collaborator::Column::ConnectionId.eq(connection.id as i32)) + .add( + channel_buffer_collaborator::Column::ConnectionServerId + .eq(connection.owner_id as i32), + ), + ) + .set(channel_buffer_collaborator::ActiveModel { + connection_lost: ActiveValue::set(true), + ..Default::default() + }) + .exec(&*tx) + .await?; + Ok(()) + } + pub async fn leave_channel_buffers( &self, connection: ConnectionId, diff --git a/crates/collab/src/db/queries/messages.rs b/crates/collab/src/db/queries/messages.rs index 4593d8f2f3717ce6e9894220095b43a688e3a379..60bb245207718ceb2a1744d060bbb57e20831e20 100644 --- a/crates/collab/src/db/queries/messages.rs +++ b/crates/collab/src/db/queries/messages.rs @@ -25,6 +25,25 @@ impl Database { .await } + pub async fn channel_chat_connection_lost( + &self, + connection_id: ConnectionId, + tx: &DatabaseTransaction, + ) -> Result<()> { + channel_chat_participant::Entity::delete_many() + .filter( + Condition::all() + .add( + channel_chat_participant::Column::ConnectionServerId + .eq(connection_id.owner_id), + ) + .add(channel_chat_participant::Column::ConnectionId.eq(connection_id.id)), + ) + .exec(tx) + .await?; + Ok(()) + } + pub async fn leave_channel_chat( &self, channel_id: ChannelId, diff --git a/crates/collab/src/db/queries/rooms.rs b/crates/collab/src/db/queries/rooms.rs index e348b50beeedb47c609d455ad759f59adea01adf..fb81fef176ab8bfa38ece1b4beb284088a96e0bb 100644 --- a/crates/collab/src/db/queries/rooms.rs +++ b/crates/collab/src/db/queries/rooms.rs @@ -890,54 +890,43 @@ impl Database { pub async fn connection_lost(&self, connection: ConnectionId) -> Result<()> { self.transaction(|tx| async move { - let participant = room_participant::Entity::find() - .filter( - Condition::all() - .add( - room_participant::Column::AnsweringConnectionId - .eq(connection.id as i32), - ) - .add( - room_participant::Column::AnsweringConnectionServerId - .eq(connection.owner_id as i32), - ), - ) - .one(&*tx) - .await?; - - if let Some(participant) = participant { - room_participant::Entity::update(room_participant::ActiveModel { - answering_connection_lost: ActiveValue::set(true), - ..participant.into_active_model() - }) - .exec(&*tx) + self.room_connection_lost(connection, &*tx).await?; + self.channel_buffer_connection_lost(connection, &*tx) .await?; - } - - channel_buffer_collaborator::Entity::update_many() - .filter( - Condition::all() - .add( - channel_buffer_collaborator::Column::ConnectionId - .eq(connection.id as i32), - ) - .add( - channel_buffer_collaborator::Column::ConnectionServerId - .eq(connection.owner_id as i32), - ), - ) - .set(channel_buffer_collaborator::ActiveModel { - connection_lost: ActiveValue::set(true), - ..Default::default() - }) - .exec(&*tx) - .await?; - + self.channel_chat_connection_lost(connection, &*tx).await?; Ok(()) }) .await } + pub async fn room_connection_lost( + &self, + connection: ConnectionId, + tx: &DatabaseTransaction, + ) -> Result<()> { + let participant = room_participant::Entity::find() + .filter( + Condition::all() + .add(room_participant::Column::AnsweringConnectionId.eq(connection.id as i32)) + .add( + room_participant::Column::AnsweringConnectionServerId + .eq(connection.owner_id as i32), + ), + ) + .one(&*tx) + .await?; + + if let Some(participant) = participant { + room_participant::Entity::update(room_participant::ActiveModel { + answering_connection_lost: ActiveValue::set(true), + ..participant.into_active_model() + }) + .exec(&*tx) + .await?; + } + Ok(()) + } + fn build_incoming_call( room: &proto::Room, called_user_id: UserId, diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index b508c45dc53f0a1017d44d1df96643165fc59b53..5ecd05ae1bb411f4333a61742115f619e582d231 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -904,9 +904,8 @@ async fn connection_lost( room_updated(&room, &session.peer); } } - update_user_contacts(session.user_id, &session).await?; - + update_user_contacts(session.user_id, &session).await?; } _ = teardown.changed().fuse() => {} } From c2c521015a0655ce46542b572b341e8e31e2a4b8 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 11 Sep 2023 18:48:24 -0600 Subject: [PATCH 028/404] Fix bug where cursors became invisible if replaying was interrupted --- crates/vim/src/editor_events.rs | 2 ++ crates/vim/src/normal/repeat.rs | 34 ++++++++++++++++++++++----------- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/crates/vim/src/editor_events.rs b/crates/vim/src/editor_events.rs index da5c7d46eda3813c784eeff238c965528fb3cca3..ae6a2808cffdc9def7deda372b931b02ec3e2bad 100644 --- a/crates/vim/src/editor_events.rs +++ b/crates/vim/src/editor_events.rs @@ -34,7 +34,9 @@ fn focused(EditorFocused(editor): &EditorFocused, cx: &mut AppContext) { fn blurred(EditorBlurred(editor): &EditorBlurred, cx: &mut AppContext) { editor.window().update(cx, |cx| { Vim::update(cx, |vim, cx| { + vim.clear_operator(cx); vim.workspace_state.recording = false; + vim.workspace_state.recorded_actions.clear(); if let Some(previous_editor) = vim.active_editor.clone() { if previous_editor == editor.clone() { vim.active_editor = None; diff --git a/crates/vim/src/normal/repeat.rs b/crates/vim/src/normal/repeat.rs index 6954ace71fed459ec541e3ddf30e2ea14caed4af..d7d8c3077382c281e729a9ccb686074db4cc420d 100644 --- a/crates/vim/src/normal/repeat.rs +++ b/crates/vim/src/normal/repeat.rs @@ -43,9 +43,6 @@ pub(crate) fn init(cx: &mut AppContext) { cx.add_action(|_: &mut Workspace, _: &EndRepeat, cx| { Vim::update(cx, |vim, cx| { vim.workspace_state.replaying = false; - vim.update_active_editor(cx, |editor, _| { - editor.show_local_selections = true; - }); vim.switch_mode(Mode::Normal, false, cx) }); }); @@ -56,6 +53,10 @@ pub(crate) fn init(cx: &mut AppContext) { pub(crate) fn repeat(cx: &mut WindowContext, from_insert_mode: bool) { let Some((mut actions, editor, selection)) = Vim::update(cx, |vim, cx| { let actions = vim.workspace_state.recorded_actions.clone(); + if actions.is_empty() { + return None; + } + let Some(editor) = vim.active_editor.clone() else { return None; }; @@ -82,14 +83,6 @@ pub(crate) fn repeat(cx: &mut WindowContext, from_insert_mode: bool) { } } - if let Some(editor) = editor.upgrade(cx) { - editor.update(cx, |editor, _| { - editor.show_local_selections = false; - }) - } else { - return None; - } - Some((actions, editor, selection)) }) else { return; @@ -176,6 +169,9 @@ pub(crate) fn repeat(cx: &mut WindowContext, from_insert_mode: bool) { let window = cx.window(); cx.app_context() .spawn(move |mut cx| async move { + editor.update(&mut cx, |editor, _| { + editor.show_local_selections = false; + })?; for action in actions { match action { ReplayableAction::Action(action) => { @@ -195,6 +191,9 @@ pub(crate) fn repeat(cx: &mut WindowContext, from_insert_mode: bool) { }), }? } + editor.update(&mut cx, |editor, _| { + editor.show_local_selections = true; + })?; window .dispatch_action(editor.id(), &EndRepeat, &mut cx) .ok_or_else(|| anyhow::anyhow!("window was closed")) @@ -513,4 +512,17 @@ mod test { }) .await; } + + #[gpui::test] + async fn test_record_interrupted( + deterministic: Arc, + cx: &mut gpui::TestAppContext, + ) { + let mut cx = VimTestContext::new(cx, true).await; + + cx.set_state("ˇhello\n", Mode::Normal); + cx.simulate_keystrokes(["4", "i", "j", "cmd-shift-p", "escape", "escape"]); + deterministic.run_until_parked(); + cx.assert_state("ˇjhello\n", Mode::Normal); + } } From 0d161519e402982dded19a7b1d78bd5dc0faf4b7 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 12 Sep 2023 11:34:27 -0400 Subject: [PATCH 029/404] Checkpoint --- crates/storybook/src/components.rs | 6 +- crates/storybook/src/components/avatar.rs | 39 +++++ .../storybook/src/components/icon_button.rs | 36 ++-- .../storybook/src/components/tool_divider.rs | 19 +++ crates/storybook/src/modules.rs | 4 + crates/storybook/src/modules/chat_panel.rs | 74 +++++++++ crates/storybook/src/modules/tab_bar.rs | 29 +++- crates/storybook/src/modules/title_bar.rs | 154 ++++++++++++++++++ crates/storybook/src/prelude.rs | 26 +++ crates/storybook/src/storybook.rs | 1 + crates/storybook/src/workspace.rs | 10 +- 11 files changed, 373 insertions(+), 25 deletions(-) create mode 100644 crates/storybook/src/components/avatar.rs create mode 100644 crates/storybook/src/components/tool_divider.rs create mode 100644 crates/storybook/src/modules/chat_panel.rs create mode 100644 crates/storybook/src/modules/title_bar.rs create mode 100644 crates/storybook/src/prelude.rs diff --git a/crates/storybook/src/components.rs b/crates/storybook/src/components.rs index d07c2651a02b81a63f02873484752eabdae1971e..e0ba73b866a6e6cd40006f3e0eb214a05b8b2768 100644 --- a/crates/storybook/src/components.rs +++ b/crates/storybook/src/components.rs @@ -4,11 +4,15 @@ use gpui2::{ }; use std::{marker::PhantomData, rc::Rc}; +mod avatar; mod icon_button; mod tab; +mod tool_divider; -pub(crate) use icon_button::{icon_button, ButtonVariant}; +pub(crate) use avatar::avatar; +pub(crate) use icon_button::icon_button; pub(crate) use tab::tab; +pub(crate) use tool_divider::tool_divider; struct ButtonHandlers { click: Option)>>, diff --git a/crates/storybook/src/components/avatar.rs b/crates/storybook/src/components/avatar.rs new file mode 100644 index 0000000000000000000000000000000000000000..8eff055d75c0406d73c0ac0cda512c6bcbadd0b2 --- /dev/null +++ b/crates/storybook/src/components/avatar.rs @@ -0,0 +1,39 @@ +use crate::prelude::Shape; +use crate::theme::theme; +use gpui2::elements::img; +use gpui2::style::StyleHelpers; +use gpui2::{ArcCow, IntoElement}; +use gpui2::{Element, ViewContext}; + +pub type UnknownString = ArcCow<'static, str>; + +#[derive(Element)] +pub(crate) struct Avatar { + src: ArcCow<'static, str>, + shape: Shape, +} + +pub fn avatar(src: impl Into>, shape: Shape) -> impl Element { + Avatar { + src: src.into(), + shape, + } +} + +impl Avatar { + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + let theme = theme(cx); + + let mut img = img(); + + if self.shape == Shape::Circle { + img = img.rounded_full(); + } else { + img = img.rounded_md(); + } + + img.uri(self.src.clone()) + .size_4() + .fill(theme.middle.warning.default.foreground) + } +} diff --git a/crates/storybook/src/components/icon_button.rs b/crates/storybook/src/components/icon_button.rs index 0a9b2ca285345aaca7bd91b1ff22990b0a8be407..cecb6ccfc3570d05716d79e16c6294e32b4187f9 100644 --- a/crates/storybook/src/components/icon_button.rs +++ b/crates/storybook/src/components/icon_button.rs @@ -1,3 +1,4 @@ +use crate::prelude::{ButtonVariant, UIState}; use crate::theme::theme; use gpui2::elements::svg; use gpui2::style::{StyleHelpers, Styleable}; @@ -8,22 +9,33 @@ use gpui2::{Element, ParentElement, ViewContext}; pub(crate) struct IconButton { path: &'static str, variant: ButtonVariant, + state: UIState, } -#[derive(PartialEq)] -pub enum ButtonVariant { - Ghost, - Filled, -} - -pub fn icon_button(path: &'static str, variant: ButtonVariant) -> impl Element { - IconButton { path, variant } +pub fn icon_button( + path: &'static str, + variant: ButtonVariant, + state: UIState, +) -> impl Element { + IconButton { + path, + variant, + state, + } } impl IconButton { fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { let theme = theme(cx); + let icon_color; + + if self.state == UIState::Disabled { + icon_color = theme.highest.base.disabled.foreground; + } else { + icon_color = theme.highest.base.default.foreground; + } + let mut div = div(); if self.variant == ButtonVariant::Filled { div = div.fill(theme.highest.on.default.background); @@ -39,12 +51,6 @@ impl IconButton { .fill(theme.highest.base.hovered.background) .active() .fill(theme.highest.base.pressed.background) - .child( - svg() - .path(self.path) - .w_4() - .h_4() - .fill(theme.highest.variant.default.foreground), - ) + .child(svg().path(self.path).w_4().h_4().fill(icon_color)) } } diff --git a/crates/storybook/src/components/tool_divider.rs b/crates/storybook/src/components/tool_divider.rs new file mode 100644 index 0000000000000000000000000000000000000000..c07d11dfe209ba3fe30a39485af1fb35320f913e --- /dev/null +++ b/crates/storybook/src/components/tool_divider.rs @@ -0,0 +1,19 @@ +use crate::theme::theme; +use gpui2::style::StyleHelpers; +use gpui2::{elements::div, IntoElement}; +use gpui2::{Element, ViewContext}; + +#[derive(Element)] +pub(crate) struct ToolDivider {} + +pub fn tool_divider() -> impl Element { + ToolDivider {} +} + +impl ToolDivider { + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + let theme = theme(cx); + + div().w_px().h_3().fill(theme.lowest.base.default.border) + } +} diff --git a/crates/storybook/src/modules.rs b/crates/storybook/src/modules.rs index bc8ba73b08f8fe51f218076570e6182796c75f0f..00b323fc5d911c3bc31e9d0cc8793a8f8f0999c2 100644 --- a/crates/storybook/src/modules.rs +++ b/crates/storybook/src/modules.rs @@ -1,3 +1,7 @@ +mod chat_panel; mod tab_bar; +mod title_bar; +pub(crate) use chat_panel::chat_panel; pub(crate) use tab_bar::tab_bar; +pub(crate) use title_bar::title_bar; diff --git a/crates/storybook/src/modules/chat_panel.rs b/crates/storybook/src/modules/chat_panel.rs new file mode 100644 index 0000000000000000000000000000000000000000..772bb908e0b39a16cb020fd93441e31262f4343c --- /dev/null +++ b/crates/storybook/src/modules/chat_panel.rs @@ -0,0 +1,74 @@ +use std::marker::PhantomData; + +use crate::components::icon_button; +use crate::prelude::{ButtonVariant, UIState}; +use crate::theme::theme; +use gpui2::elements::div::ScrollState; +use gpui2::style::StyleHelpers; +use gpui2::{elements::div, IntoElement}; +use gpui2::{Element, ParentElement, ViewContext}; + +#[derive(Element)] +pub struct ChatPanel { + view_type: PhantomData, + scroll_state: ScrollState, +} + +pub fn chat_panel(scroll_state: ScrollState) -> ChatPanel { + ChatPanel { + view_type: PhantomData, + scroll_state, + } +} + +impl ChatPanel { + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + let theme = theme(cx); + + div() + .h_full() + .flex() + // Header + .child( + div() + .px_2() + .flex() + .gap_2() + // Nav Buttons + .child("#gpui2"), + ) + // Chat Body + .child( + div() + .w_full() + .flex() + .flex_col() + .overflow_y_scroll(self.scroll_state.clone()) + .child("body"), + ) + // Composer + .child( + div() + .px_2() + .flex() + .gap_2() + // Nav Buttons + .child( + div() + .flex() + .items_center() + .gap_px() + .child(icon_button( + "icons/plus.svg", + ButtonVariant::Ghost, + UIState::Default, + )) + .child(icon_button( + "icons/split.svg", + ButtonVariant::Ghost, + UIState::Default, + )), + ), + ) + } +} diff --git a/crates/storybook/src/modules/tab_bar.rs b/crates/storybook/src/modules/tab_bar.rs index 06029c5dc223c0d4852c1250c3d67c2c669b2876..5a3358588ef282bbcde1dde31f5487d75271d375 100644 --- a/crates/storybook/src/modules/tab_bar.rs +++ b/crates/storybook/src/modules/tab_bar.rs @@ -1,6 +1,7 @@ use std::marker::PhantomData; -use crate::components::{icon_button, tab, ButtonVariant}; +use crate::components::{icon_button, tab}; +use crate::prelude::{ButtonVariant, UIState}; use crate::theme::theme; use gpui2::elements::div::ScrollState; use gpui2::style::StyleHelpers; @@ -40,15 +41,23 @@ impl TabBar { .flex() .items_center() .gap_px() - .child(icon_button("icons/arrow_left.svg", ButtonVariant::Filled)) - .child(icon_button("icons/arrow_right.svg", ButtonVariant::Ghost)), + .child(icon_button( + "icons/arrow_left.svg", + ButtonVariant::Ghost, + UIState::Default, + )) + .child(icon_button( + "icons/arrow_right.svg", + ButtonVariant::Ghost, + UIState::Disabled, + )), ), ) .child( div().w_0().flex_1().h_full().child( div() .flex() - .gap_px() + .gap_8() .overflow_x_scroll(self.scroll_state.clone()) .child(tab("Cargo.toml", false)) .child(tab("Channels Panel", true)) @@ -74,8 +83,16 @@ impl TabBar { .flex() .items_center() .gap_px() - .child(icon_button("icons/plus.svg", ButtonVariant::Ghost)) - .child(icon_button("icons/split.svg", ButtonVariant::Ghost)), + .child(icon_button( + "icons/plus.svg", + ButtonVariant::Ghost, + UIState::Default, + )) + .child(icon_button( + "icons/split.svg", + ButtonVariant::Ghost, + UIState::Default, + )), ), ) } diff --git a/crates/storybook/src/modules/title_bar.rs b/crates/storybook/src/modules/title_bar.rs new file mode 100644 index 0000000000000000000000000000000000000000..dfd4e4490eb7028c82d7b3aaed68cc13790ed005 --- /dev/null +++ b/crates/storybook/src/modules/title_bar.rs @@ -0,0 +1,154 @@ +use std::marker::PhantomData; + +use crate::components::{avatar, icon_button, tool_divider}; +use crate::prelude::{ButtonVariant, Shape, UIState}; +use crate::theme::theme; +use gpui2::style::{StyleHelpers, Styleable}; +use gpui2::{elements::div, IntoElement}; +use gpui2::{Element, ParentElement, ViewContext}; + +#[derive(Element)] +pub struct TitleBar { + view_type: PhantomData, +} + +pub fn title_bar() -> TitleBar { + TitleBar { + view_type: PhantomData, + } +} + +impl TitleBar { + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + let theme = theme(cx); + + div() + .flex() + .items_center() + .justify_between() + .w_full() + .h_8() + .fill(theme.lowest.base.default.background) + .child( + div() + .flex() + .items_center() + .h_full() + .gap_4() + .px_2() + // === Traffic Lights === // + .child( + div() + .flex() + .items_center() + .gap_2() + .child( + div() + .w_3() + .h_3() + .rounded_full() + .fill(theme.lowest.positive.default.foreground), + ) + .child( + div() + .w_3() + .h_3() + .rounded_full() + .fill(theme.lowest.warning.default.foreground), + ) + .child( + div() + .w_3() + .h_3() + .rounded_full() + .fill(theme.lowest.negative.default.foreground), + ), + ) + // === Project Info === // + .child( + div() + .flex() + .items_center() + .gap_1() + .child( + div() + .h_full() + .flex() + .items_center() + .justify_center() + .px_2() + .rounded_md() + .hover() + .fill(theme.lowest.base.hovered.background) + .active() + .fill(theme.lowest.base.pressed.background) + .child(div().text_sm().child("project")), + ) + .child( + div() + .h_full() + .flex() + .items_center() + .justify_center() + .px_2() + .rounded_md() + .text_color(theme.lowest.variant.default.foreground) + .hover() + .fill(theme.lowest.base.hovered.background) + .active() + .fill(theme.lowest.base.pressed.background) + .child(div().text_sm().child("branch")), + ), + ), + ) + .child( + div() + .flex() + .items_center() + .child( + div() + .px_2() + .flex() + .items_center() + .gap_1() + .child(icon_button( + "icons/stop_sharing.svg", + ButtonVariant::Ghost, + UIState::Default, + )) + .child(icon_button( + "icons/exit.svg", + ButtonVariant::Ghost, + UIState::Default, + )), + ) + .child(tool_divider()) + .child( + div() + .px_2() + .flex() + .items_center() + .gap_1() + .child(icon_button( + "icons/radix/mic.svg", + ButtonVariant::Ghost, + UIState::Default, + )) + .child(icon_button( + "icons/radix/speaker-loud.svg", + ButtonVariant::Ghost, + UIState::Default, + )) + .child(icon_button( + "icons/radix/desktop.svg", + ButtonVariant::Ghost, + UIState::Default, + )), + ) + .child(div().px_2().flex().items_center().child(avatar( + "https://avatars.githubusercontent.com/u/1714999?v=4", + Shape::Squircle, + ))), + ) + } +} diff --git a/crates/storybook/src/prelude.rs b/crates/storybook/src/prelude.rs new file mode 100644 index 0000000000000000000000000000000000000000..442567424ac6dc203b3287711a35ec6b1a0336a2 --- /dev/null +++ b/crates/storybook/src/prelude.rs @@ -0,0 +1,26 @@ +#[derive(PartialEq)] +pub enum ButtonVariant { + Ghost, + Filled, +} + +#[derive(PartialEq)] +pub enum Shape { + Circle, + Squircle, +} + +#[derive(PartialEq)] +pub enum UIState { + Default, + Hovered, + Active, + Focused, + Disabled, +} + +#[derive(PartialEq)] +pub enum UIToggleState { + Default, + Enabled, +} diff --git a/crates/storybook/src/storybook.rs b/crates/storybook/src/storybook.rs index 1b40bc2dc482cf2bad94d8d2a662202cc8bbd2ef..fc7f861e80f9b1685b1186a7db035218ac613dfd 100644 --- a/crates/storybook/src/storybook.rs +++ b/crates/storybook/src/storybook.rs @@ -13,6 +13,7 @@ mod collab_panel; mod components; mod element_ext; mod modules; +mod prelude; mod theme; mod workspace; diff --git a/crates/storybook/src/workspace.rs b/crates/storybook/src/workspace.rs index c37b3f16ea3abac79ab659e4f3fe138e4017d20a..477255264771840efd12f61f4ef40a262cee2571 100644 --- a/crates/storybook/src/workspace.rs +++ b/crates/storybook/src/workspace.rs @@ -1,4 +1,8 @@ -use crate::{collab_panel::collab_panel, modules::tab_bar, theme::theme}; +use crate::{ + collab_panel::collab_panel, + modules::{chat_panel, tab_bar, title_bar}, + theme::theme, +}; use gpui2::{ elements::{div, div::ScrollState, img, svg}, style::{StyleHelpers, Styleable}, @@ -30,7 +34,7 @@ impl WorkspaceElement { .items_start() .text_color(theme.lowest.base.default.foreground) .fill(theme.middle.base.default.background) - .child(titlebar()) + .child(title_bar()) .child( div() .flex_1() @@ -52,7 +56,7 @@ impl WorkspaceElement { .child(tab_bar(self.tab_bar_scroll_state.clone())), ), ) - .child(collab_panel(self.right_scroll_state.clone())), + .child(chat_panel(self.right_scroll_state.clone())), ) .child(statusbar()) } From 7daed1b2c3a6b2ce4b8255c6b56fdc696de8a202 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 12 Sep 2023 09:56:23 -0600 Subject: [PATCH 030/404] Fix 0 used in a count --- crates/vim/src/insert.rs | 2 +- crates/vim/src/motion.rs | 2 +- crates/vim/src/normal.rs | 10 +++---- crates/vim/src/normal/case.rs | 2 +- crates/vim/src/normal/repeat.rs | 4 +-- crates/vim/src/normal/scroll.rs | 2 +- crates/vim/src/normal/search.rs | 4 +-- crates/vim/src/normal/substitute.rs | 4 +-- crates/vim/src/state.rs | 1 + crates/vim/src/test.rs | 27 ++++++++++++++++++- .../src/test/neovim_backed_test_context.rs | 14 ++++++++++ crates/vim/src/vim.rs | 19 ++++++------- crates/vim/test_data/test_clear_counts.json | 2 +- crates/vim/test_data/test_dot_repeat.json | 2 +- crates/vim/test_data/test_zero.json | 7 +++++ 15 files changed, 73 insertions(+), 29 deletions(-) create mode 100644 crates/vim/test_data/test_zero.json diff --git a/crates/vim/src/insert.rs b/crates/vim/src/insert.rs index 7495b302a29db477521fab56ddd8ee237bc0ed21..fb567fab6a7aecc66931be5edca2ff0300536cac 100644 --- a/crates/vim/src/insert.rs +++ b/crates/vim/src/insert.rs @@ -12,7 +12,7 @@ pub fn init(cx: &mut AppContext) { fn normal_before(_: &mut Workspace, action: &NormalBefore, cx: &mut ViewContext) { let should_repeat = Vim::update(cx, |vim, cx| { - let count = vim.take_count().unwrap_or(1); + let count = vim.take_count(cx).unwrap_or(1); vim.stop_recording_immediately(action.boxed_clone()); if count <= 1 || vim.workspace_state.replaying { vim.update_active_editor(cx, |editor, cx| { diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index 6821ec64e5ebc988dc1678126a1a3671aeb6ba9d..3e65e6d504c6a3de90fcc06f84155315f4025eb8 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -229,7 +229,7 @@ pub(crate) fn motion(motion: Motion, cx: &mut WindowContext) { Vim::update(cx, |vim, cx| vim.pop_operator(cx)); } - let count = Vim::update(cx, |vim, _| vim.take_count()); + let count = Vim::update(cx, |vim, cx| vim.take_count(cx)); let operator = Vim::read(cx).active_operator(); match Vim::read(cx).state().mode { Mode::Normal => normal_motion(motion, operator, count, cx), diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index 16a4150dabafd578eb4347d38956bef56147290a..20344366944b382c42034ce39285f00fb78e6a31 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -68,21 +68,21 @@ pub fn init(cx: &mut AppContext) { cx.add_action(|_: &mut Workspace, _: &DeleteLeft, cx| { Vim::update(cx, |vim, cx| { vim.record_current_action(cx); - let times = vim.take_count(); + let times = vim.take_count(cx); delete_motion(vim, Motion::Left, times, cx); }) }); cx.add_action(|_: &mut Workspace, _: &DeleteRight, cx| { Vim::update(cx, |vim, cx| { vim.record_current_action(cx); - let times = vim.take_count(); + let times = vim.take_count(cx); delete_motion(vim, Motion::Right, times, cx); }) }); cx.add_action(|_: &mut Workspace, _: &ChangeToEndOfLine, cx| { Vim::update(cx, |vim, cx| { vim.start_recording(cx); - let times = vim.take_count(); + let times = vim.take_count(cx); change_motion( vim, Motion::EndOfLine { @@ -96,7 +96,7 @@ pub fn init(cx: &mut AppContext) { cx.add_action(|_: &mut Workspace, _: &DeleteToEndOfLine, cx| { Vim::update(cx, |vim, cx| { vim.record_current_action(cx); - let times = vim.take_count(); + let times = vim.take_count(cx); delete_motion( vim, Motion::EndOfLine { @@ -110,7 +110,7 @@ pub fn init(cx: &mut AppContext) { cx.add_action(|_: &mut Workspace, _: &JoinLines, cx| { Vim::update(cx, |vim, cx| { vim.record_current_action(cx); - let mut times = vim.take_count().unwrap_or(1); + let mut times = vim.take_count(cx).unwrap_or(1); if vim.state().mode.is_visual() { times = 1; } else if times > 1 { diff --git a/crates/vim/src/normal/case.rs b/crates/vim/src/normal/case.rs index 34b81fbb4c7419bd6059b294ea87edb4168331da..22d09f8359f52060a7435cef321ca0c7f71bd37c 100644 --- a/crates/vim/src/normal/case.rs +++ b/crates/vim/src/normal/case.rs @@ -8,7 +8,7 @@ use crate::{normal::ChangeCase, state::Mode, Vim}; pub fn change_case(_: &mut Workspace, _: &ChangeCase, cx: &mut ViewContext) { Vim::update(cx, |vim, cx| { vim.record_current_action(cx); - let count = vim.take_count().unwrap_or(1) as u32; + let count = vim.take_count(cx).unwrap_or(1) as u32; vim.update_active_editor(cx, |editor, cx| { let mut ranges = Vec::new(); let mut cursor_positions = Vec::new(); diff --git a/crates/vim/src/normal/repeat.rs b/crates/vim/src/normal/repeat.rs index d7d8c3077382c281e729a9ccb686074db4cc420d..df9e9a32ad7d5c602526801ca857feb3c1226da7 100644 --- a/crates/vim/src/normal/repeat.rs +++ b/crates/vim/src/normal/repeat.rs @@ -60,7 +60,7 @@ pub(crate) fn repeat(cx: &mut WindowContext, from_insert_mode: bool) { let Some(editor) = vim.active_editor.clone() else { return None; }; - let count = vim.take_count(); + let count = vim.take_count(cx); let selection = vim.workspace_state.recorded_selection.clone(); match selection { @@ -253,7 +253,7 @@ mod test { deterministic.run_until_parked(); cx.simulate_shared_keystrokes(["."]).await; deterministic.run_until_parked(); - cx.set_shared_state("THE QUICK ˇbrown fox").await; + cx.assert_shared_state("THE QUICK ˇbrown fox").await; } #[gpui::test] diff --git a/crates/vim/src/normal/scroll.rs b/crates/vim/src/normal/scroll.rs index 6319ba1663eddd79ed92de4ea056d750df35c90b..877fff328bb1fdb1f4fc83e01258daf3a8ac28b7 100644 --- a/crates/vim/src/normal/scroll.rs +++ b/crates/vim/src/normal/scroll.rs @@ -48,7 +48,7 @@ pub fn init(cx: &mut AppContext) { fn scroll(cx: &mut ViewContext, by: fn(c: Option) -> ScrollAmount) { Vim::update(cx, |vim, cx| { - let amount = by(vim.take_count().map(|c| c as f32)); + let amount = by(vim.take_count(cx).map(|c| c as f32)); vim.update_active_editor(cx, |editor, cx| scroll_editor(editor, &amount, cx)); }) } diff --git a/crates/vim/src/normal/search.rs b/crates/vim/src/normal/search.rs index b488a879f2fade76a2da98defee03291b049ac88..d0c8a56249447100409bfd5cf385b2278752ca3d 100644 --- a/crates/vim/src/normal/search.rs +++ b/crates/vim/src/normal/search.rs @@ -52,7 +52,7 @@ fn search(workspace: &mut Workspace, action: &Search, cx: &mut ViewContext() { search_bar.update(cx, |search_bar, cx| { @@ -119,7 +119,7 @@ pub fn move_to_internal( ) { Vim::update(cx, |vim, cx| { let pane = workspace.active_pane().clone(); - let count = vim.take_count().unwrap_or(1); + let count = vim.take_count(cx).unwrap_or(1); pane.update(cx, |pane, cx| { if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { let search = search_bar.update(cx, |search_bar, cx| { diff --git a/crates/vim/src/normal/substitute.rs b/crates/vim/src/normal/substitute.rs index 26aff7baa420a37b40f71a0e01b1ba440326bd1f..bb6e1abf92a0a687e32c7ee6a1e92787a1a82ba9 100644 --- a/crates/vim/src/normal/substitute.rs +++ b/crates/vim/src/normal/substitute.rs @@ -11,7 +11,7 @@ pub(crate) fn init(cx: &mut AppContext) { cx.add_action(|_: &mut Workspace, _: &Substitute, cx| { Vim::update(cx, |vim, cx| { vim.start_recording(cx); - let count = vim.take_count(); + let count = vim.take_count(cx); substitute(vim, count, vim.state().mode == Mode::VisualLine, cx); }) }); @@ -22,7 +22,7 @@ pub(crate) fn init(cx: &mut AppContext) { if matches!(vim.state().mode, Mode::VisualBlock | Mode::Visual) { vim.switch_mode(Mode::VisualLine, false, cx) } - let count = vim.take_count(); + let count = vim.take_count(cx); substitute(vim, count, true, cx) }) }); diff --git a/crates/vim/src/state.rs b/crates/vim/src/state.rs index 8fd4049767bfada65bbfd57142eb96c43c310366..2cb5e058e8726e88a8282b4969b8f3b2b659b6ed 100644 --- a/crates/vim/src/state.rs +++ b/crates/vim/src/state.rs @@ -186,6 +186,7 @@ impl EditorState { if self.active_operator().is_none() && self.pre_count.is_some() || self.active_operator().is_some() && self.post_count.is_some() { + dbg!("VimCount"); context.add_identifier("VimCount"); } diff --git a/crates/vim/src/test.rs b/crates/vim/src/test.rs index 70a0b7c7a64a535b3e63e3e011c1c16ffc5336f0..a708d3c12adece2d758e95ffd762f86bae5b5fc8 100644 --- a/crates/vim/src/test.rs +++ b/crates/vim/src/test.rs @@ -587,9 +587,34 @@ async fn test_clear_counts(cx: &mut gpui::TestAppContext) { cx.simulate_shared_keystrokes(["4", "escape", "3", "d", "l"]) .await; - cx.set_shared_state(indoc! {" + cx.assert_shared_state(indoc! {" The quick brown fox juˇ over the lazy dog"}) .await; } + +#[gpui::test] +async fn test_zero(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.set_shared_state(indoc! {" + The quˇick brown + fox jumps over + the lazy dog"}) + .await; + + cx.simulate_shared_keystrokes(["0"]).await; + cx.assert_shared_state(indoc! {" + ˇThe quick brown + fox jumps over + the lazy dog"}) + .await; + + cx.simulate_shared_keystrokes(["1", "0", "l"]).await; + cx.assert_shared_state(indoc! {" + The quick ˇbrown + fox jumps over + the lazy dog"}) + .await; +} diff --git a/crates/vim/src/test/neovim_backed_test_context.rs b/crates/vim/src/test/neovim_backed_test_context.rs index b433a6bfc05ea61b0758bcb8ac81ba189c57573c..97df989e4d57e6123ff84e39d7a8427134b40533 100644 --- a/crates/vim/src/test/neovim_backed_test_context.rs +++ b/crates/vim/src/test/neovim_backed_test_context.rs @@ -68,6 +68,8 @@ pub struct NeovimBackedTestContext<'a> { last_set_state: Option, recent_keystrokes: Vec, + + is_dirty: bool, } impl<'a> NeovimBackedTestContext<'a> { @@ -81,6 +83,7 @@ impl<'a> NeovimBackedTestContext<'a> { last_set_state: None, recent_keystrokes: Default::default(), + is_dirty: false, } } @@ -128,6 +131,7 @@ impl<'a> NeovimBackedTestContext<'a> { self.last_set_state = Some(marked_text.to_string()); self.recent_keystrokes = Vec::new(); self.neovim.set_state(marked_text).await; + self.is_dirty = true; context_handle } @@ -153,6 +157,7 @@ impl<'a> NeovimBackedTestContext<'a> { } pub async fn assert_shared_state(&mut self, marked_text: &str) { + self.is_dirty = false; let marked_text = marked_text.replace("•", " "); let neovim = self.neovim_state().await; let editor = self.editor_state(); @@ -258,6 +263,7 @@ impl<'a> NeovimBackedTestContext<'a> { } pub async fn assert_state_matches(&mut self) { + self.is_dirty = false; let neovim = self.neovim_state().await; let editor = self.editor_state(); let initial_state = self @@ -383,6 +389,14 @@ impl<'a> DerefMut for NeovimBackedTestContext<'a> { } } +impl<'a> Drop for NeovimBackedTestContext<'a> { + fn drop(&mut self) { + if self.is_dirty { + panic!("Test context was dropped after set_shared_state before assert_shared_state") + } + } +} + #[cfg(test)] mod test { use gpui::TestAppContext; diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index c6488fe171c5d352750d7e8fc4f380134ab199b7..79117177655d6a6baa8163168e8c35f40d7409c8 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -73,7 +73,7 @@ pub fn init(cx: &mut AppContext) { }, ); cx.add_action(|_: &mut Workspace, n: &Number, cx: _| { - Vim::update(cx, |vim, _| vim.push_count_digit(n.0)); + Vim::update(cx, |vim, cx| vim.push_count_digit(n.0, cx)); }); cx.add_action(|_: &mut Workspace, _: &Tab, cx| { @@ -228,13 +228,7 @@ impl Vim { let editor = self.active_editor.clone()?.upgrade(cx)?; Some(editor.update(cx, update)) } - // ~, shift-j, x, shift-x, p - // shift-c, shift-d, shift-i, i, a, o, shift-o, s - // c, d - // r - // TODO: shift-j? - // pub fn start_recording(&mut self, cx: &mut WindowContext) { if !self.workspace_state.replaying { self.workspace_state.recording = true; @@ -309,7 +303,7 @@ impl Vim { state.operator_stack.clear(); }); if mode != Mode::Insert { - self.take_count(); + self.take_count(cx); } cx.emit_global(VimEvent::ModeChanged { mode }); @@ -363,7 +357,7 @@ impl Vim { }); } - fn push_count_digit(&mut self, number: usize) { + fn push_count_digit(&mut self, number: usize, cx: &mut WindowContext) { if self.active_operator().is_some() { self.update_state(|state| { state.post_count = Some(state.post_count.unwrap_or(0) * 10 + number) @@ -373,9 +367,11 @@ impl Vim { state.pre_count = Some(state.pre_count.unwrap_or(0) * 10 + number) }) } + // update the keymap so that 0 works + self.sync_vim_settings(cx) } - fn take_count(&mut self) -> Option { + fn take_count(&mut self, cx: &mut WindowContext) -> Option { if self.workspace_state.replaying { return self.workspace_state.recorded_count; } @@ -390,6 +386,7 @@ impl Vim { if self.workspace_state.recording { self.workspace_state.recorded_count = count; } + self.sync_vim_settings(cx); count } @@ -415,7 +412,7 @@ impl Vim { popped_operator } fn clear_operator(&mut self, cx: &mut WindowContext) { - self.take_count(); + self.take_count(cx); self.update_state(|state| state.operator_stack.clear()); self.sync_vim_settings(cx); } diff --git a/crates/vim/test_data/test_clear_counts.json b/crates/vim/test_data/test_clear_counts.json index 8bc78de7d367678b32ec2a19ef9d5109a8708854..6ef6b3601786f21a340367cfeef38b1bf8555cbb 100644 --- a/crates/vim/test_data/test_clear_counts.json +++ b/crates/vim/test_data/test_clear_counts.json @@ -4,4 +4,4 @@ {"Key":"3"} {"Key":"d"} {"Key":"l"} -{"Put":{"state":"The quick brown\nfox juˇ over\nthe lazy dog"}} +{"Get":{"state":"The quick brown\nfox juˇ over\nthe lazy dog","mode":"Normal"}} diff --git a/crates/vim/test_data/test_dot_repeat.json b/crates/vim/test_data/test_dot_repeat.json index f1a1a3c138509420d6e0e92daf679fb347a6e673..331ef52ecb96174e6669b3a4665b39b476e88972 100644 --- a/crates/vim/test_data/test_dot_repeat.json +++ b/crates/vim/test_data/test_dot_repeat.json @@ -35,4 +35,4 @@ {"Key":"."} {"Put":{"state":"THE QUIˇck brown fox"}} {"Key":"."} -{"Put":{"state":"THE QUICK ˇbrown fox"}} +{"Get":{"state":"THE QUICK ˇbrown fox","mode":"Normal"}} diff --git a/crates/vim/test_data/test_zero.json b/crates/vim/test_data/test_zero.json new file mode 100644 index 0000000000000000000000000000000000000000..bc1253deb580ab8218bf75dd36a7e5c3f63d613c --- /dev/null +++ b/crates/vim/test_data/test_zero.json @@ -0,0 +1,7 @@ +{"Put":{"state":"The quˇick brown\nfox jumps over\nthe lazy dog"}} +{"Key":"0"} +{"Get":{"state":"ˇThe quick brown\nfox jumps over\nthe lazy dog","mode":"Normal"}} +{"Key":"1"} +{"Key":"0"} +{"Key":"l"} +{"Get":{"state":"The quick ˇbrown\nfox jumps over\nthe lazy dog","mode":"Normal"}} From dcaba9d9e7d981397c048a8e3e08011d62f97100 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 12 Sep 2023 10:13:24 -0600 Subject: [PATCH 031/404] Remove supported exception (and refactor tests to be more linear) --- crates/vim/src/normal/change.rs | 170 ++++++++++-------- crates/vim/src/normal/delete.rs | 48 ++--- .../src/test/neovim_backed_test_context.rs | 10 +- 3 files changed, 123 insertions(+), 105 deletions(-) diff --git a/crates/vim/src/normal/change.rs b/crates/vim/src/normal/change.rs index 836ce1492b6fa128ee88a232bcf816accc790e7c..e9f3001392c9a7571c211d298dd171e2aa5ae411 100644 --- a/crates/vim/src/normal/change.rs +++ b/crates/vim/src/normal/change.rs @@ -121,7 +121,7 @@ fn expand_changed_word_selection( mod test { use indoc::indoc; - use crate::test::{ExemptionFeatures, NeovimBackedTestContext}; + use crate::test::NeovimBackedTestContext; #[gpui::test] async fn test_change_h(cx: &mut gpui::TestAppContext) { @@ -239,150 +239,178 @@ mod test { #[gpui::test] async fn test_change_0(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "0"]); - cx.assert(indoc! {" + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.assert_neovim_compatible( + indoc! {" The qˇuick - brown fox"}) - .await; - cx.assert(indoc! {" + brown fox"}, + ["c", "0"], + ) + .await; + cx.assert_neovim_compatible( + indoc! {" The quick ˇ - brown fox"}) - .await; + brown fox"}, + ["c", "0"], + ) + .await; } #[gpui::test] async fn test_change_k(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "k"]); - cx.assert(indoc! {" + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.assert_neovim_compatible( + indoc! {" The quick brown ˇfox - jumps over"}) - .await; - cx.assert(indoc! {" + jumps over"}, + ["c", "k"], + ) + .await; + cx.assert_neovim_compatible( + indoc! {" The quick brown fox - jumps ˇover"}) - .await; - cx.assert_exempted( + jumps ˇover"}, + ["c", "k"], + ) + .await; + cx.assert_neovim_compatible( indoc! {" The qˇuick brown fox jumps over"}, - ExemptionFeatures::OperatorAbortsOnFailedMotion, + ["c", "k"], ) .await; - cx.assert_exempted( + cx.assert_neovim_compatible( indoc! {" ˇ brown fox jumps over"}, - ExemptionFeatures::OperatorAbortsOnFailedMotion, + ["c", "k"], ) .await; } #[gpui::test] async fn test_change_j(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "j"]); - cx.assert(indoc! {" + let mut cx = NeovimBackedTestContext::new(cx).await; + cx.assert_neovim_compatible( + indoc! {" The quick brown ˇfox - jumps over"}) - .await; - cx.assert_exempted( + jumps over"}, + ["c", "j"], + ) + .await; + cx.assert_neovim_compatible( indoc! {" The quick brown fox jumps ˇover"}, - ExemptionFeatures::OperatorAbortsOnFailedMotion, + ["c", "j"], ) .await; - cx.assert(indoc! {" + cx.assert_neovim_compatible( + indoc! {" The qˇuick brown fox - jumps over"}) - .await; - cx.assert_exempted( + jumps over"}, + ["c", "j"], + ) + .await; + cx.assert_neovim_compatible( indoc! {" The quick brown fox ˇ"}, - ExemptionFeatures::OperatorAbortsOnFailedMotion, + ["c", "j"], ) .await; } #[gpui::test] async fn test_change_end_of_document(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx) - .await - .binding(["c", "shift-g"]); - cx.assert(indoc! {" + let mut cx = NeovimBackedTestContext::new(cx).await; + cx.assert_neovim_compatible( + indoc! {" The quick brownˇ fox jumps over - the lazy"}) - .await; - cx.assert(indoc! {" + the lazy"}, + ["c", "shift-g"], + ) + .await; + cx.assert_neovim_compatible( + indoc! {" The quick brownˇ fox jumps over - the lazy"}) - .await; - cx.assert_exempted( + the lazy"}, + ["c", "shift-g"], + ) + .await; + cx.assert_neovim_compatible( indoc! {" The quick brown fox jumps over the lˇazy"}, - ExemptionFeatures::OperatorAbortsOnFailedMotion, + ["c", "shift-g"], ) .await; - cx.assert_exempted( + cx.assert_neovim_compatible( indoc! {" The quick brown fox jumps over ˇ"}, - ExemptionFeatures::OperatorAbortsOnFailedMotion, + ["c", "shift-g"], ) .await; } #[gpui::test] async fn test_change_gg(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx) - .await - .binding(["c", "g", "g"]); - cx.assert(indoc! {" + let mut cx = NeovimBackedTestContext::new(cx).await; + cx.assert_neovim_compatible( + indoc! {" The quick brownˇ fox jumps over - the lazy"}) - .await; - cx.assert(indoc! {" + the lazy"}, + ["c", "g", "g"], + ) + .await; + cx.assert_neovim_compatible( + indoc! {" The quick brown fox jumps over - the lˇazy"}) - .await; - cx.assert_exempted( + the lˇazy"}, + ["c", "g", "g"], + ) + .await; + cx.assert_neovim_compatible( indoc! {" The qˇuick brown fox jumps over the lazy"}, - ExemptionFeatures::OperatorAbortsOnFailedMotion, + ["c", "g", "g"], ) .await; - cx.assert_exempted( + cx.assert_neovim_compatible( indoc! {" ˇ brown fox jumps over the lazy"}, - ExemptionFeatures::OperatorAbortsOnFailedMotion, + ["c", "g", "g"], ) .await; } @@ -427,27 +455,17 @@ mod test { async fn test_repeated_cb(cx: &mut gpui::TestAppContext) { let mut cx = NeovimBackedTestContext::new(cx).await; - cx.add_initial_state_exemptions( - indoc! {" - ˇThe quick brown - - fox jumps-over - the lazy dog - "}, - ExemptionFeatures::OperatorAbortsOnFailedMotion, - ); - for count in 1..=5 { - cx.assert_binding_matches_all( - ["c", &count.to_string(), "b"], - indoc! {" - ˇThe quˇickˇ browˇn - ˇ - ˇfox ˇjumpsˇ-ˇoˇver - ˇthe lazy dog - "}, - ) - .await; + for marked_text in cx.each_marked_position(indoc! {" + ˇThe quˇickˇ browˇn + ˇ + ˇfox ˇjumpsˇ-ˇoˇver + ˇthe lazy dog + "}) + { + cx.assert_neovim_compatible(&marked_text, ["c", &count.to_string(), "b"]) + .await; + } } } diff --git a/crates/vim/src/normal/delete.rs b/crates/vim/src/normal/delete.rs index 1126eb11551353a5e05c79139fd02b9f18d78f4f..19ea6af875f6508ccac20447ad9d4d102a238882 100644 --- a/crates/vim/src/normal/delete.rs +++ b/crates/vim/src/normal/delete.rs @@ -278,37 +278,41 @@ mod test { #[gpui::test] async fn test_delete_end_of_document(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx) - .await - .binding(["d", "shift-g"]); - cx.assert(indoc! {" + let mut cx = NeovimBackedTestContext::new(cx).await; + cx.assert_neovim_compatible( + indoc! {" The quick brownˇ fox jumps over - the lazy"}) - .await; - cx.assert(indoc! {" + the lazy"}, + ["d", "shift-g"], + ) + .await; + cx.assert_neovim_compatible( + indoc! {" The quick brownˇ fox jumps over - the lazy"}) - .await; - cx.assert_exempted( + the lazy"}, + ["d", "shift-g"], + ) + .await; + cx.assert_neovim_compatible( indoc! {" The quick brown fox jumps over the lˇazy"}, - ExemptionFeatures::OperatorAbortsOnFailedMotion, + ["d", "shift-g"], ) .await; - cx.assert_exempted( + cx.assert_neovim_compatible( indoc! {" The quick brown fox jumps over ˇ"}, - ExemptionFeatures::OperatorAbortsOnFailedMotion, + ["d", "shift-g"], ) .await; } @@ -318,34 +322,32 @@ mod test { let mut cx = NeovimBackedTestContext::new(cx) .await .binding(["d", "g", "g"]); - cx.assert(indoc! {" + cx.assert_neovim_compatible(indoc! {" The quick brownˇ fox jumps over - the lazy"}) + the lazy"}, ["d", "g", "g"]) .await; - cx.assert(indoc! {" + cx.assert_neovim_compatible(indoc! {" The quick brown fox jumps over - the lˇazy"}) + the lˇazy"}, ["d", "g", "g"]) .await; - cx.assert_exempted( + cx.assert_neovim_compatible( indoc! {" The qˇuick brown fox jumps over - the lazy"}, - ExemptionFeatures::OperatorAbortsOnFailedMotion, + the lazy"},["d", "g", "g"] ) .await; - cx.assert_exempted( + cx.assert_neovim_compatible( indoc! {" ˇ brown fox jumps over - the lazy"}, - ExemptionFeatures::OperatorAbortsOnFailedMotion, + the lazy"},["d", "g", "g"] ) .await; } diff --git a/crates/vim/src/test/neovim_backed_test_context.rs b/crates/vim/src/test/neovim_backed_test_context.rs index 97df989e4d57e6123ff84e39d7a8427134b40533..0df5c32136b2818bba631f8d2c64c104a274944f 100644 --- a/crates/vim/src/test/neovim_backed_test_context.rs +++ b/crates/vim/src/test/neovim_backed_test_context.rs @@ -13,10 +13,7 @@ use util::test::{generate_marked_text, marked_text_offsets}; use super::{neovim_connection::NeovimConnection, NeovimBackedBindingTestContext, VimTestContext}; use crate::state::Mode; -pub const SUPPORTED_FEATURES: &[ExemptionFeatures] = &[ - ExemptionFeatures::DeletionOnEmptyLine, - ExemptionFeatures::OperatorAbortsOnFailedMotion, -]; +pub const SUPPORTED_FEATURES: &[ExemptionFeatures] = &[ExemptionFeatures::DeletionOnEmptyLine]; /// Enum representing features we have tests for but which don't work, yet. Used /// to add exemptions and automatically @@ -25,8 +22,6 @@ pub enum ExemptionFeatures { // MOTIONS // Deletions on empty lines miss some newlines DeletionOnEmptyLine, - // When a motion fails, it should should not apply linewise operations - OperatorAbortsOnFailedMotion, // When an operator completes at the end of the file, an extra newline is left OperatorLastNewlineRemains, // Deleting a word on an empty line doesn't remove the newline @@ -389,6 +384,9 @@ impl<'a> DerefMut for NeovimBackedTestContext<'a> { } } +// a common mistake in tests is to call set_shared_state when +// you mean asswert_shared_state. This notices that and lets +// you know. impl<'a> Drop for NeovimBackedTestContext<'a> { fn drop(&mut self) { if self.is_dirty { From 4cb8647702d3b13cbc979e52c89d543bd0e295dd Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 12 Sep 2023 18:46:54 +0200 Subject: [PATCH 032/404] Z 1200/replace in buffer (#2922) This is still WIP, mostly pending styling. I added a pretty rudimentary text field and no buttons whatsoever other than that. I am targeting a Preview of 09.13, as I am gonna be on PTO for the next week. I dislike the current implementation slightly because of `regex`'s crate syntax and lack of support of backreferences. What strikes me as odd wrt to syntax is that it will just replace a capture name with empty string if that capture is missing from the regex. While this is perfectly fine behaviour for conditionally-matched capture groups (e.g. `(foo)?`), I think it should still error out if there's no group with a given name (conditional or not). Release Notes: - Added "Replace" functionality to buffer search. --- assets/icons/select-all.svg | 5 + crates/editor/src/items.rs | 24 +- crates/feedback/src/feedback_editor.rs | 9 +- crates/language_tools/src/lsp_log.rs | 16 +- crates/project/src/search.rs | 37 ++- crates/search/src/buffer_search.rs | 314 ++++++++++++++++++---- crates/search/src/search.rs | 38 ++- crates/terminal_view/src/terminal_view.rs | 17 +- crates/theme/src/theme.rs | 10 +- crates/workspace/src/searchable.rs | 18 +- styles/src/style_tree/search.ts | 79 ++++-- 11 files changed, 471 insertions(+), 96 deletions(-) create mode 100644 assets/icons/select-all.svg diff --git a/assets/icons/select-all.svg b/assets/icons/select-all.svg new file mode 100644 index 0000000000000000000000000000000000000000..45a10bba42648ee0f6f9011f4386630609515e0c --- /dev/null +++ b/assets/icons/select-all.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index d9998725922f5154e299bb3bd7a32b04ff18c2d2..b31c9dcd1b8698e2f92585bca563c4ba4ea1b1fc 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -16,7 +16,7 @@ use language::{ proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, OffsetRangeExt, Point, SelectionGoal, }; -use project::{FormatTrigger, Item as _, Project, ProjectPath}; +use project::{search::SearchQuery, FormatTrigger, Item as _, Project, ProjectPath}; use rpc::proto::{self, update_view}; use smallvec::SmallVec; use std::{ @@ -26,6 +26,7 @@ use std::{ iter, ops::Range, path::{Path, PathBuf}, + sync::Arc, }; use text::Selection; use util::{ @@ -978,7 +979,26 @@ impl SearchableItem for Editor { } self.change_selections(None, cx, |s| s.select_ranges(ranges)); } + fn replace( + &mut self, + identifier: &Self::Match, + query: &SearchQuery, + cx: &mut ViewContext, + ) { + let text = self.buffer.read(cx); + let text = text.snapshot(cx); + let text = text.text_for_range(identifier.clone()).collect::>(); + let text: Cow<_> = if text.len() == 1 { + text.first().cloned().unwrap().into() + } else { + let joined_chunks = text.join(""); + joined_chunks.into() + }; + if let Some(replacement) = query.replacement(&text) { + self.edit([(identifier.clone(), Arc::from(&*replacement))], cx); + } + } fn match_index_for_direction( &mut self, matches: &Vec>, @@ -1030,7 +1050,7 @@ impl SearchableItem for Editor { fn find_matches( &mut self, - query: project::search::SearchQuery, + query: Arc, cx: &mut ViewContext, ) -> Task>> { let buffer = self.buffer().read(cx).snapshot(cx); diff --git a/crates/feedback/src/feedback_editor.rs b/crates/feedback/src/feedback_editor.rs index a717223f6d7d3a5e3eb4dd2dd50f81154bee3072..0b8a29e1146333e9c08062e7a9b55de3d9fa7bde 100644 --- a/crates/feedback/src/feedback_editor.rs +++ b/crates/feedback/src/feedback_editor.rs @@ -13,7 +13,7 @@ use gpui::{ use isahc::Request; use language::Buffer; use postage::prelude::Stream; -use project::Project; +use project::{search::SearchQuery, Project}; use regex::Regex; use serde::Serialize; use smallvec::SmallVec; @@ -418,10 +418,13 @@ impl SearchableItem for FeedbackEditor { self.editor .update(cx, |e, cx| e.select_matches(matches, cx)) } - + fn replace(&mut self, matches: &Self::Match, query: &SearchQuery, cx: &mut ViewContext) { + self.editor + .update(cx, |e, cx| e.replace(matches, query, cx)); + } fn find_matches( &mut self, - query: project::search::SearchQuery, + query: Arc, cx: &mut ViewContext, ) -> Task> { self.editor diff --git a/crates/language_tools/src/lsp_log.rs b/crates/language_tools/src/lsp_log.rs index a918e3d151aaa8e6f53bde31350e554550cf783f..587e6ed25aba2c5603ca700cf8e61e8391926705 100644 --- a/crates/language_tools/src/lsp_log.rs +++ b/crates/language_tools/src/lsp_log.rs @@ -13,7 +13,7 @@ use gpui::{ }; use language::{Buffer, LanguageServerId, LanguageServerName}; use lsp::IoKind; -use project::{Project, Worktree}; +use project::{search::SearchQuery, Project, Worktree}; use std::{borrow::Cow, sync::Arc}; use theme::{ui, Theme}; use workspace::{ @@ -524,12 +524,24 @@ impl SearchableItem for LspLogView { fn find_matches( &mut self, - query: project::search::SearchQuery, + query: Arc, cx: &mut ViewContext, ) -> gpui::Task> { self.editor.update(cx, |e, cx| e.find_matches(query, cx)) } + fn replace(&mut self, _: &Self::Match, _: &SearchQuery, _: &mut ViewContext) { + // Since LSP Log is read-only, it doesn't make sense to support replace operation. + } + fn supported_options() -> workspace::searchable::SearchOptions { + workspace::searchable::SearchOptions { + case: true, + word: true, + regex: true, + // LSP log is read-only. + replacement: false, + } + } fn active_match_index( &mut self, matches: Vec, diff --git a/crates/project/src/search.rs b/crates/project/src/search.rs index 6c53d2e934e12c177d322da9ca73022683c54007..bf81158701310f61b77db9307064f7f526e99187 100644 --- a/crates/project/src/search.rs +++ b/crates/project/src/search.rs @@ -7,6 +7,7 @@ use language::{char_kind, BufferSnapshot}; use regex::{Regex, RegexBuilder}; use smol::future::yield_now; use std::{ + borrow::Cow, io::{BufRead, BufReader, Read}, ops::Range, path::{Path, PathBuf}, @@ -35,6 +36,7 @@ impl SearchInputs { pub enum SearchQuery { Text { search: Arc>, + replacement: Option, whole_word: bool, case_sensitive: bool, inner: SearchInputs, @@ -42,7 +44,7 @@ pub enum SearchQuery { Regex { regex: Regex, - + replacement: Option, multiline: bool, whole_word: bool, case_sensitive: bool, @@ -95,6 +97,7 @@ impl SearchQuery { }; Self::Text { search: Arc::new(search), + replacement: None, whole_word, case_sensitive, inner, @@ -130,6 +133,7 @@ impl SearchQuery { }; Ok(Self::Regex { regex, + replacement: None, multiline, whole_word, case_sensitive, @@ -156,7 +160,21 @@ impl SearchQuery { )) } } - + pub fn with_replacement(mut self, new_replacement: Option) -> Self { + match self { + Self::Text { + ref mut replacement, + .. + } + | Self::Regex { + ref mut replacement, + .. + } => { + *replacement = new_replacement; + self + } + } + } pub fn to_proto(&self, project_id: u64) -> proto::SearchProject { proto::SearchProject { project_id, @@ -214,7 +232,20 @@ impl SearchQuery { } } } - + pub fn replacement<'a>(&self, text: &'a str) -> Option> { + match self { + SearchQuery::Text { replacement, .. } => replacement.clone().map(Cow::from), + SearchQuery::Regex { + regex, replacement, .. + } => { + if let Some(replacement) = replacement { + Some(regex.replace(text, replacement)) + } else { + None + } + } + } + } pub async fn search( &self, buffer: &BufferSnapshot, diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 78729df936c15140d034ce29a6c4ccb108c46deb..6a227812d17ef08bfe9f1153e378e34df333b4c7 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -2,19 +2,16 @@ use crate::{ history::SearchHistory, mode::{next_mode, SearchMode, Side}, search_bar::{render_nav_button, render_search_mode_button}, - CycleMode, NextHistoryQuery, PreviousHistoryQuery, SearchOptions, SelectAllMatches, - SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleWholeWord, + CycleMode, NextHistoryQuery, PreviousHistoryQuery, ReplaceAll, ReplaceNext, SearchOptions, + SelectAllMatches, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleReplace, + ToggleWholeWord, }; use collections::HashMap; use editor::Editor; use futures::channel::oneshot; use gpui::{ - actions, - elements::*, - impl_actions, - platform::{CursorStyle, MouseButton}, - Action, AnyViewHandle, AppContext, Entity, Subscription, Task, View, ViewContext, ViewHandle, - WindowContext, + actions, elements::*, impl_actions, Action, AnyViewHandle, AppContext, Entity, Subscription, + Task, View, ViewContext, ViewHandle, WindowContext, }; use project::search::SearchQuery; use serde::Deserialize; @@ -54,6 +51,11 @@ pub fn init(cx: &mut AppContext) { cx.add_action(BufferSearchBar::previous_history_query); cx.add_action(BufferSearchBar::cycle_mode); cx.add_action(BufferSearchBar::cycle_mode_on_pane); + cx.add_action(BufferSearchBar::replace_all); + cx.add_action(BufferSearchBar::replace_next); + cx.add_action(BufferSearchBar::replace_all_on_pane); + cx.add_action(BufferSearchBar::replace_next_on_pane); + cx.add_action(BufferSearchBar::toggle_replace); add_toggle_option_action::(SearchOptions::CASE_SENSITIVE, cx); add_toggle_option_action::(SearchOptions::WHOLE_WORD, cx); } @@ -73,9 +75,11 @@ fn add_toggle_option_action(option: SearchOptions, cx: &mut AppContex pub struct BufferSearchBar { query_editor: ViewHandle, + replacement_editor: ViewHandle, active_searchable_item: Option>, active_match_index: Option, active_searchable_item_subscription: Option, + active_search: Option>, searchable_items_with_matches: HashMap, Vec>>, pending_search: Option>, @@ -85,6 +89,7 @@ pub struct BufferSearchBar { dismissed: bool, search_history: SearchHistory, current_mode: SearchMode, + replace_is_active: bool, } impl Entity for BufferSearchBar { @@ -156,6 +161,9 @@ impl View for BufferSearchBar { self.query_editor.update(cx, |editor, cx| { editor.set_placeholder_text(new_placeholder_text, cx); }); + self.replacement_editor.update(cx, |editor, cx| { + editor.set_placeholder_text("Replace with...", cx); + }); let search_button_for_mode = |mode, side, cx: &mut ViewContext| { let is_active = self.current_mode == mode; @@ -212,7 +220,6 @@ impl View for BufferSearchBar { cx, ) }; - let query_column = Flex::row() .with_child( Svg::for_style(theme.search.editor_icon.clone().icon) @@ -243,7 +250,57 @@ impl View for BufferSearchBar { .with_max_width(theme.search.editor.max_width) .with_height(theme.search.search_bar_row_height) .flex(1., false); + let should_show_replace_input = self.replace_is_active && supported_options.replacement; + let replacement = should_show_replace_input.then(|| { + Flex::row() + .with_child( + Svg::for_style(theme.search.replace_icon.clone().icon) + .contained() + .with_style(theme.search.replace_icon.clone().container), + ) + .with_child(ChildView::new(&self.replacement_editor, cx).flex(1., true)) + .align_children_center() + .flex(1., true) + .contained() + .with_style(query_container_style) + .constrained() + .with_min_width(theme.search.editor.min_width) + .with_max_width(theme.search.editor.max_width) + .with_height(theme.search.search_bar_row_height) + .flex(1., false) + }); + let replace_all = should_show_replace_input.then(|| { + super::replace_action( + ReplaceAll, + "Replace all", + "icons/replace_all.svg", + theme.tooltip.clone(), + theme.search.action_button.clone(), + ) + }); + let replace_next = should_show_replace_input.then(|| { + super::replace_action( + ReplaceNext, + "Replace next", + "icons/replace_next.svg", + theme.tooltip.clone(), + theme.search.action_button.clone(), + ) + }); + let switches_column = supported_options.replacement.then(|| { + Flex::row() + .align_children_center() + .with_child(super::toggle_replace_button( + self.replace_is_active, + theme.tooltip.clone(), + theme.search.option_button_component.clone(), + )) + .constrained() + .with_height(theme.search.search_bar_row_height) + .contained() + .with_style(theme.search.option_button_group) + }); let mode_column = Flex::row() .with_child(search_button_for_mode( SearchMode::Text, @@ -261,7 +318,10 @@ impl View for BufferSearchBar { .with_height(theme.search.search_bar_row_height); let nav_column = Flex::row() - .with_child(self.render_action_button("all", cx)) + .align_children_center() + .with_children(replace_next) + .with_children(replace_all) + .with_child(self.render_action_button("icons/select-all.svg", cx)) .with_child(Flex::row().with_children(match_count)) .with_child(nav_button_for_direction("<", Direction::Prev, cx)) .with_child(nav_button_for_direction(">", Direction::Next, cx)) @@ -271,6 +331,8 @@ impl View for BufferSearchBar { Flex::row() .with_child(query_column) + .with_children(switches_column) + .with_children(replacement) .with_child(mode_column) .with_child(nav_column) .contained() @@ -345,9 +407,18 @@ impl BufferSearchBar { }); cx.subscribe(&query_editor, Self::on_query_editor_event) .detach(); - + let replacement_editor = cx.add_view(|cx| { + Editor::auto_height( + 2, + Some(Arc::new(|theme| theme.search.editor.input.clone())), + cx, + ) + }); + // cx.subscribe(&replacement_editor, Self::on_query_editor_event) + // .detach(); Self { query_editor, + replacement_editor, active_searchable_item: None, active_searchable_item_subscription: None, active_match_index: None, @@ -359,6 +430,8 @@ impl BufferSearchBar { dismissed: true, search_history: SearchHistory::default(), current_mode: SearchMode::default(), + active_search: None, + replace_is_active: false, } } @@ -441,7 +514,9 @@ impl BufferSearchBar { pub fn query(&self, cx: &WindowContext) -> String { self.query_editor.read(cx).text(cx) } - + pub fn replacement(&self, cx: &WindowContext) -> String { + self.replacement_editor.read(cx).text(cx) + } pub fn query_suggestion(&mut self, cx: &mut ViewContext) -> Option { self.active_searchable_item .as_ref() @@ -477,37 +552,16 @@ impl BufferSearchBar { ) -> AnyElement { let tooltip = "Select All Matches"; let tooltip_style = theme::current(cx).tooltip.clone(); - let action_type_id = 0_usize; - let has_matches = self.active_match_index.is_some(); - let cursor_style = if has_matches { - CursorStyle::PointingHand - } else { - CursorStyle::default() - }; - enum ActionButton {} - MouseEventHandler::new::(action_type_id, cx, |state, cx| { - let theme = theme::current(cx); - let style = theme - .search - .action_button - .in_state(has_matches) - .style_for(state); - Label::new(icon, style.text.clone()) - .aligned() - .contained() - .with_style(style.container) - }) - .on_click(MouseButton::Left, move |_, this, cx| { - this.select_all_matches(&SelectAllMatches, cx) - }) - .with_cursor_style(cursor_style) - .with_tooltip::( - action_type_id, - tooltip.to_string(), - Some(Box::new(SelectAllMatches)), - tooltip_style, - cx, - ) + + let theme = theme::current(cx); + let style = theme.search.action_button.clone(); + + gpui::elements::Component::element(SafeStylable::with_style( + theme::components::action_button::Button::action(SelectAllMatches) + .with_tooltip(tooltip, tooltip_style) + .with_contents(theme::components::svg::Svg::new(icon)), + style, + )) .into_any() } @@ -688,6 +742,7 @@ impl BufferSearchBar { let (done_tx, done_rx) = oneshot::channel(); let query = self.query(cx); self.pending_search.take(); + if let Some(active_searchable_item) = self.active_searchable_item.as_ref() { if query.is_empty() { self.active_match_index.take(); @@ -695,7 +750,7 @@ impl BufferSearchBar { let _ = done_tx.send(()); cx.notify(); } else { - let query = if self.current_mode == SearchMode::Regex { + let query: Arc<_> = if self.current_mode == SearchMode::Regex { match SearchQuery::regex( query, self.search_options.contains(SearchOptions::WHOLE_WORD), @@ -703,7 +758,8 @@ impl BufferSearchBar { Vec::new(), Vec::new(), ) { - Ok(query) => query, + Ok(query) => query + .with_replacement(Some(self.replacement(cx)).filter(|s| !s.is_empty())), Err(_) => { self.query_contains_error = true; cx.notify(); @@ -718,8 +774,10 @@ impl BufferSearchBar { Vec::new(), Vec::new(), ) - }; - + .with_replacement(Some(self.replacement(cx)).filter(|s| !s.is_empty())) + } + .into(); + self.active_search = Some(query.clone()); let query_text = query.as_str().to_string(); let matches = active_searchable_item.find_matches(query, cx); @@ -810,6 +868,63 @@ impl BufferSearchBar { cx.propagate_action(); } } + fn toggle_replace(&mut self, _: &ToggleReplace, _: &mut ViewContext) { + if let Some(_) = &self.active_searchable_item { + self.replace_is_active = !self.replace_is_active; + } + } + fn replace_next(&mut self, _: &ReplaceNext, cx: &mut ViewContext) { + if !self.dismissed && self.active_search.is_some() { + if let Some(searchable_item) = self.active_searchable_item.as_ref() { + if let Some(query) = self.active_search.as_ref() { + if let Some(matches) = self + .searchable_items_with_matches + .get(&searchable_item.downgrade()) + { + if let Some(active_index) = self.active_match_index { + let query = query.as_ref().clone().with_replacement( + Some(self.replacement(cx)).filter(|rep| !rep.is_empty()), + ); + searchable_item.replace(&matches[active_index], &query, cx); + } + + self.focus_editor(&FocusEditor, cx); + } + } + } + } + } + fn replace_all(&mut self, _: &ReplaceAll, cx: &mut ViewContext) { + if !self.dismissed && self.active_search.is_some() { + if let Some(searchable_item) = self.active_searchable_item.as_ref() { + if let Some(query) = self.active_search.as_ref() { + if let Some(matches) = self + .searchable_items_with_matches + .get(&searchable_item.downgrade()) + { + let query = query.as_ref().clone().with_replacement( + Some(self.replacement(cx)).filter(|rep| !rep.is_empty()), + ); + for m in matches { + searchable_item.replace(m, &query, cx); + } + + self.focus_editor(&FocusEditor, cx); + } + } + } + } + } + fn replace_next_on_pane(pane: &mut Pane, action: &ReplaceNext, cx: &mut ViewContext) { + if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { + search_bar.update(cx, |bar, cx| bar.replace_next(action, cx)); + } + } + fn replace_all_on_pane(pane: &mut Pane, action: &ReplaceAll, cx: &mut ViewContext) { + if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { + search_bar.update(cx, |bar, cx| bar.replace_all(action, cx)); + } + } } #[cfg(test)] @@ -1539,4 +1654,109 @@ mod tests { assert_eq!(search_bar.search_options, SearchOptions::NONE); }); } + #[gpui::test] + async fn test_replace_simple(cx: &mut TestAppContext) { + let (editor, search_bar) = init_test(cx); + + search_bar + .update(cx, |search_bar, cx| { + search_bar.search("expression", None, cx) + }) + .await + .unwrap(); + + search_bar.update(cx, |search_bar, cx| { + search_bar.replacement_editor.update(cx, |editor, cx| { + // We use $1 here as initially we should be in Text mode, where `$1` should be treated literally. + editor.set_text("expr$1", cx); + }); + search_bar.replace_all(&ReplaceAll, cx) + }); + assert_eq!( + editor.read_with(cx, |this, cx| { this.text(cx) }), + r#" + A regular expr$1 (shortened as regex or regexp;[1] also referred to as + rational expr$1[2][3]) is a sequence of characters that specifies a search + pattern in text. Usually such patterns are used by string-searching algorithms + for "find" or "find and replace" operations on strings, or for input validation. + "# + .unindent() + ); + + // Search for word boundaries and replace just a single one. + search_bar + .update(cx, |search_bar, cx| { + search_bar.search("or", Some(SearchOptions::WHOLE_WORD), cx) + }) + .await + .unwrap(); + + search_bar.update(cx, |search_bar, cx| { + search_bar.replacement_editor.update(cx, |editor, cx| { + editor.set_text("banana", cx); + }); + search_bar.replace_next(&ReplaceNext, cx) + }); + // Notice how the first or in the text (shORtened) is not replaced. Neither are the remaining hits of `or` in the text. + assert_eq!( + editor.read_with(cx, |this, cx| { this.text(cx) }), + r#" + A regular expr$1 (shortened as regex banana regexp;[1] also referred to as + rational expr$1[2][3]) is a sequence of characters that specifies a search + pattern in text. Usually such patterns are used by string-searching algorithms + for "find" or "find and replace" operations on strings, or for input validation. + "# + .unindent() + ); + // Let's turn on regex mode. + search_bar + .update(cx, |search_bar, cx| { + search_bar.activate_search_mode(SearchMode::Regex, cx); + search_bar.search("\\[([^\\]]+)\\]", None, cx) + }) + .await + .unwrap(); + search_bar.update(cx, |search_bar, cx| { + search_bar.replacement_editor.update(cx, |editor, cx| { + editor.set_text("${1}number", cx); + }); + search_bar.replace_all(&ReplaceAll, cx) + }); + assert_eq!( + editor.read_with(cx, |this, cx| { this.text(cx) }), + r#" + A regular expr$1 (shortened as regex banana regexp;1number also referred to as + rational expr$12number3number) is a sequence of characters that specifies a search + pattern in text. Usually such patterns are used by string-searching algorithms + for "find" or "find and replace" operations on strings, or for input validation. + "# + .unindent() + ); + // Now with a whole-word twist. + search_bar + .update(cx, |search_bar, cx| { + search_bar.activate_search_mode(SearchMode::Regex, cx); + search_bar.search("a\\w+s", Some(SearchOptions::WHOLE_WORD), cx) + }) + .await + .unwrap(); + search_bar.update(cx, |search_bar, cx| { + search_bar.replacement_editor.update(cx, |editor, cx| { + editor.set_text("things", cx); + }); + search_bar.replace_all(&ReplaceAll, cx) + }); + // The only word affected by this edit should be `algorithms`, even though there's a bunch + // of words in this text that would match this regex if not for WHOLE_WORD. + assert_eq!( + editor.read_with(cx, |this, cx| { this.text(cx) }), + r#" + A regular expr$1 (shortened as regex banana regexp;1number also referred to as + rational expr$12number3number) is a sequence of characters that specifies a search + pattern in text. Usually such patterns are used by string-searching things + for "find" or "find and replace" operations on strings, or for input validation. + "# + .unindent() + ); + } } diff --git a/crates/search/src/search.rs b/crates/search/src/search.rs index 47f7f485c486a86d3ff3725b5e05aedb52305b36..0135ed4eed69a6b0b1d5ad2f73a7521120c4cc8b 100644 --- a/crates/search/src/search.rs +++ b/crates/search/src/search.rs @@ -8,7 +8,9 @@ use gpui::{ pub use mode::SearchMode; use project::search::SearchQuery; pub use project_search::{ProjectSearchBar, ProjectSearchView}; -use theme::components::{action_button::Button, svg::Svg, ComponentExt, ToggleIconButtonStyle}; +use theme::components::{ + action_button::Button, svg::Svg, ComponentExt, IconButtonStyle, ToggleIconButtonStyle, +}; pub mod buffer_search; mod history; @@ -27,6 +29,7 @@ actions!( CycleMode, ToggleWholeWord, ToggleCaseSensitive, + ToggleReplace, SelectNextMatch, SelectPrevMatch, SelectAllMatches, @@ -34,7 +37,9 @@ actions!( PreviousHistoryQuery, ActivateTextMode, ActivateSemanticMode, - ActivateRegexMode + ActivateRegexMode, + ReplaceAll, + ReplaceNext ] ); @@ -98,3 +103,32 @@ impl SearchOptions { .into_any() } } + +fn toggle_replace_button( + active: bool, + tooltip_style: TooltipStyle, + button_style: ToggleIconButtonStyle, +) -> AnyElement { + Button::dynamic_action(Box::new(ToggleReplace)) + .with_tooltip("Toggle replace", tooltip_style) + .with_contents(theme::components::svg::Svg::new("icons/replace.svg")) + .toggleable(active) + .with_style(button_style) + .element() + .into_any() +} + +fn replace_action( + action: impl Action, + name: &'static str, + icon_path: &'static str, + tooltip_style: TooltipStyle, + button_style: IconButtonStyle, +) -> AnyElement { + Button::dynamic_action(Box::new(action)) + .with_tooltip(name, tooltip_style) + .with_contents(theme::components::svg::Svg::new(icon_path)) + .with_style(button_style) + .element() + .into_any() +} diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index a12f9d3c3c0447b82fde8db18b28d022f1d0cf4c..b79f655f815a71b3985eb26450b2b5edf9837c26 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -18,7 +18,7 @@ use gpui::{ ViewHandle, WeakViewHandle, }; use language::Bias; -use project::{LocalWorktree, Project}; +use project::{search::SearchQuery, LocalWorktree, Project}; use serde::Deserialize; use smallvec::{smallvec, SmallVec}; use smol::Timer; @@ -26,6 +26,7 @@ use std::{ borrow::Cow, ops::RangeInclusive, path::{Path, PathBuf}, + sync::Arc, time::Duration, }; use terminal::{ @@ -380,10 +381,10 @@ impl TerminalView { pub fn find_matches( &mut self, - query: project::search::SearchQuery, + query: Arc, cx: &mut ViewContext, ) -> Task>> { - let searcher = regex_search_for_query(query); + let searcher = regex_search_for_query(&query); if let Some(searcher) = searcher { self.terminal @@ -486,7 +487,7 @@ fn possible_open_targets( .collect() } -pub fn regex_search_for_query(query: project::search::SearchQuery) -> Option { +pub fn regex_search_for_query(query: &project::search::SearchQuery) -> Option { let query = query.as_str(); let searcher = RegexSearch::new(&query); searcher.ok() @@ -798,6 +799,7 @@ impl SearchableItem for TerminalView { case: false, word: false, regex: false, + replacement: false, } } @@ -851,10 +853,10 @@ impl SearchableItem for TerminalView { /// Get all of the matches for this query, should be done on the background fn find_matches( &mut self, - query: project::search::SearchQuery, + query: Arc, cx: &mut ViewContext, ) -> Task> { - if let Some(searcher) = regex_search_for_query(query) { + if let Some(searcher) = regex_search_for_query(&query) { self.terminal() .update(cx, |term, cx| term.find_matches(searcher, cx)) } else { @@ -898,6 +900,9 @@ impl SearchableItem for TerminalView { res } + fn replace(&mut self, _: &Self::Match, _: &SearchQuery, _: &mut ViewContext) { + // Replacement is not supported in terminal view, so this is a no-op. + } } ///Get's the working directory for the given workspace, respecting the user's settings. diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index cd983322db004b0ce5616146b0057b318032647e..cc90d96420068465908feb643fa304f01bd43e91 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -3,7 +3,9 @@ mod theme_registry; mod theme_settings; pub mod ui; -use components::{action_button::ButtonStyle, disclosure::DisclosureStyle, ToggleIconButtonStyle}; +use components::{ + action_button::ButtonStyle, disclosure::DisclosureStyle, IconButtonStyle, ToggleIconButtonStyle, +}; use gpui::{ color::Color, elements::{Border, ContainerStyle, ImageStyle, LabelStyle, Shadow, SvgStyle, TooltipStyle}, @@ -439,9 +441,7 @@ pub struct Search { pub include_exclude_editor: FindEditor, pub invalid_include_exclude_editor: ContainerStyle, pub include_exclude_inputs: ContainedText, - pub option_button: Toggleable>, pub option_button_component: ToggleIconButtonStyle, - pub action_button: Toggleable>, pub match_background: Color, pub match_index: ContainedText, pub major_results_status: TextStyle, @@ -453,6 +453,10 @@ pub struct Search { pub search_row_spacing: f32, pub option_button_height: f32, pub modes_container: ContainerStyle, + pub replace_icon: IconStyle, + // Used for filters and replace + pub option_button: Toggleable>, + pub action_button: IconButtonStyle, } #[derive(Clone, Deserialize, Default, JsonSchema)] diff --git a/crates/workspace/src/searchable.rs b/crates/workspace/src/searchable.rs index 7a470db7c926dd8d2a2aee171fd5d269b6eebab3..ddde5c35541bce265105d78b4d8125545bc38ff8 100644 --- a/crates/workspace/src/searchable.rs +++ b/crates/workspace/src/searchable.rs @@ -1,4 +1,4 @@ -use std::any::Any; +use std::{any::Any, sync::Arc}; use gpui::{ AnyViewHandle, AnyWeakViewHandle, AppContext, Subscription, Task, ViewContext, ViewHandle, @@ -25,6 +25,8 @@ pub struct SearchOptions { pub case: bool, pub word: bool, pub regex: bool, + /// Specifies whether the item supports search & replace. + pub replacement: bool, } pub trait SearchableItem: Item { @@ -35,6 +37,7 @@ pub trait SearchableItem: Item { case: true, word: true, regex: true, + replacement: true, } } fn to_search_event( @@ -52,6 +55,7 @@ pub trait SearchableItem: Item { cx: &mut ViewContext, ); fn select_matches(&mut self, matches: Vec, cx: &mut ViewContext); + fn replace(&mut self, _: &Self::Match, _: &SearchQuery, _: &mut ViewContext); fn match_index_for_direction( &mut self, matches: &Vec, @@ -74,7 +78,7 @@ pub trait SearchableItem: Item { } fn find_matches( &mut self, - query: SearchQuery, + query: Arc, cx: &mut ViewContext, ) -> Task>; fn active_match_index( @@ -103,6 +107,7 @@ pub trait SearchableItemHandle: ItemHandle { cx: &mut WindowContext, ); fn select_matches(&self, matches: &Vec>, cx: &mut WindowContext); + fn replace(&self, _: &Box, _: &SearchQuery, _: &mut WindowContext); fn match_index_for_direction( &self, matches: &Vec>, @@ -113,7 +118,7 @@ pub trait SearchableItemHandle: ItemHandle { ) -> usize; fn find_matches( &self, - query: SearchQuery, + query: Arc, cx: &mut WindowContext, ) -> Task>>; fn active_match_index( @@ -189,7 +194,7 @@ impl SearchableItemHandle for ViewHandle { } fn find_matches( &self, - query: SearchQuery, + query: Arc, cx: &mut WindowContext, ) -> Task>> { let matches = self.update(cx, |this, cx| this.find_matches(query, cx)); @@ -209,6 +214,11 @@ impl SearchableItemHandle for ViewHandle { let matches = downcast_matches(matches); self.update(cx, |this, cx| this.active_match_index(matches, cx)) } + + fn replace(&self, matches: &Box, query: &SearchQuery, cx: &mut WindowContext) { + let matches = matches.downcast_ref().unwrap(); + self.update(cx, |this, cx| this.replace(matches, query, cx)) + } } fn downcast_matches(matches: &Vec>) -> Vec { diff --git a/styles/src/style_tree/search.ts b/styles/src/style_tree/search.ts index 8174690fde913787845912defe44616d0a49ac21..bc95b91819e875d757a5736dbd6fda6b8011aa49 100644 --- a/styles/src/style_tree/search.ts +++ b/styles/src/style_tree/search.ts @@ -30,9 +30,6 @@ export default function search(): any { selection: theme.players[0], text: text(theme.highest, "mono", "default"), border: border(theme.highest), - margin: { - right: SEARCH_ROW_SPACING, - }, padding: { top: 4, bottom: 4, @@ -125,7 +122,7 @@ export default function search(): any { button_width: 32, background: background(theme.highest, "on"), - corner_radius: 2, + corner_radius: 6, margin: { right: 2 }, border: { width: 1, @@ -185,26 +182,6 @@ export default function search(): any { }, }, }), - // Search tool buttons - // HACK: This is not how disabled elements should be created - // Disabled elements should use a disabled state of an interactive element, not a toggleable element with the inactive state being disabled - action_button: toggleable({ - state: { - inactive: text_button({ - variant: "ghost", - layer: theme.highest, - disabled: true, - margin: { right: SEARCH_ROW_SPACING }, - text_properties: { size: "sm" }, - }), - active: text_button({ - variant: "ghost", - layer: theme.highest, - margin: { right: SEARCH_ROW_SPACING }, - text_properties: { size: "sm" }, - }), - }, - }), editor, invalid_editor: { ...editor, @@ -218,6 +195,7 @@ export default function search(): any { match_index: { ...text(theme.highest, "mono", { size: "sm" }), padding: { + left: SEARCH_ROW_SPACING, right: SEARCH_ROW_SPACING, }, }, @@ -398,6 +376,59 @@ export default function search(): any { search_row_spacing: 8, option_button_height: 22, modes_container: {}, + replace_icon: { + icon: { + color: foreground(theme.highest, "disabled"), + asset: "icons/replace.svg", + dimensions: { + width: 14, + height: 14, + }, + }, + container: { + margin: { right: 4 }, + padding: { left: 1, right: 1 }, + }, + }, + action_button: interactive({ + base: { + icon_size: 14, + color: foreground(theme.highest, "variant"), + + button_width: 32, + background: background(theme.highest, "on"), + corner_radius: 6, + margin: { right: 2 }, + border: { + width: 1, + color: background(theme.highest, "on"), + }, + padding: { + left: 4, + right: 4, + top: 4, + bottom: 4, + }, + }, + state: { + hovered: { + ...text(theme.highest, "mono", "variant", "hovered"), + background: background(theme.highest, "on", "hovered"), + border: { + width: 1, + color: background(theme.highest, "on", "hovered"), + }, + }, + clicked: { + ...text(theme.highest, "mono", "variant", "pressed"), + background: background(theme.highest, "on", "pressed"), + border: { + width: 1, + color: background(theme.highest, "on", "pressed"), + }, + }, + }, + }), ...search_results(), } } From b0facf8e1e84ec65efcb5e67fe0e9f9c1ef797e5 Mon Sep 17 00:00:00 2001 From: Julia Date: Tue, 12 Sep 2023 08:35:58 -0400 Subject: [PATCH 033/404] Use unbounded channel(s) for LSP binary status messaging Co-Authored-By: Antonio Scandurra --- crates/language/src/language.rs | 79 ++++++++++++++++++--------------- 1 file changed, 42 insertions(+), 37 deletions(-) diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 2193b5c07ed276f4b4f2030a94ffa95d70cca6a8..07bea434e0dffd8f93146720966212a176e87d19 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -13,7 +13,7 @@ use anyhow::{anyhow, Context, Result}; use async_trait::async_trait; use collections::{HashMap, HashSet}; use futures::{ - channel::oneshot, + channel::{mpsc, oneshot}, future::{BoxFuture, Shared}, FutureExt, TryFutureExt as _, }; @@ -48,9 +48,6 @@ use unicase::UniCase; use util::{http::HttpClient, paths::PathExt}; use util::{post_inc, ResultExt, TryFutureExt as _, UnwrapFuture}; -#[cfg(any(test, feature = "test-support"))] -use futures::channel::mpsc; - pub use buffer::Operation; pub use buffer::*; pub use diagnostic_set::DiagnosticEntry; @@ -64,6 +61,27 @@ pub fn init(cx: &mut AppContext) { language_settings::init(cx); } +#[derive(Clone, Default)] +struct LspBinaryStatusSender { + txs: Arc, LanguageServerBinaryStatus)>>>>, +} + +impl LspBinaryStatusSender { + fn subscribe(&self) -> mpsc::UnboundedReceiver<(Arc, LanguageServerBinaryStatus)> { + let (tx, rx) = mpsc::unbounded(); + self.txs.lock().push(tx); + rx + } + + fn send(&self, language: Arc, status: LanguageServerBinaryStatus) { + let mut txs = self.txs.lock(); + txs.retain(|tx| { + tx.unbounded_send((language.clone(), status.clone())) + .is_ok() + }); + } +} + thread_local! { static PARSER: RefCell = RefCell::new(Parser::new()); } @@ -594,14 +612,13 @@ struct AvailableLanguage { pub struct LanguageRegistry { state: RwLock, language_server_download_dir: Option>, - lsp_binary_statuses_tx: async_broadcast::Sender<(Arc, LanguageServerBinaryStatus)>, - lsp_binary_statuses_rx: async_broadcast::Receiver<(Arc, LanguageServerBinaryStatus)>, login_shell_env_loaded: Shared>, #[allow(clippy::type_complexity)] lsp_binary_paths: Mutex< HashMap>>>>, >, executor: Option>, + lsp_binary_status_tx: LspBinaryStatusSender, } struct LanguageRegistryState { @@ -624,7 +641,6 @@ pub struct PendingLanguageServer { impl LanguageRegistry { pub fn new(login_shell_env_loaded: Task<()>) -> Self { - let (lsp_binary_statuses_tx, lsp_binary_statuses_rx) = async_broadcast::broadcast(16); Self { state: RwLock::new(LanguageRegistryState { next_language_server_id: 0, @@ -638,11 +654,10 @@ impl LanguageRegistry { reload_count: 0, }), language_server_download_dir: None, - lsp_binary_statuses_tx, - lsp_binary_statuses_rx, login_shell_env_loaded: login_shell_env_loaded.shared(), lsp_binary_paths: Default::default(), executor: None, + lsp_binary_status_tx: Default::default(), } } @@ -918,8 +933,8 @@ impl LanguageRegistry { let container_dir: Arc = Arc::from(download_dir.join(adapter.name.0.as_ref())); let root_path = root_path.clone(); let adapter = adapter.clone(); - let lsp_binary_statuses = self.lsp_binary_statuses_tx.clone(); let login_shell_env_loaded = self.login_shell_env_loaded.clone(); + let lsp_binary_statuses = self.lsp_binary_status_tx.clone(); let task = { let container_dir = container_dir.clone(); @@ -976,8 +991,8 @@ impl LanguageRegistry { pub fn language_server_binary_statuses( &self, - ) -> async_broadcast::Receiver<(Arc, LanguageServerBinaryStatus)> { - self.lsp_binary_statuses_rx.clone() + ) -> mpsc::UnboundedReceiver<(Arc, LanguageServerBinaryStatus)> { + self.lsp_binary_status_tx.subscribe() } pub fn delete_server_container( @@ -1054,7 +1069,7 @@ async fn get_binary( language: Arc, delegate: Arc, container_dir: Arc, - statuses: async_broadcast::Sender<(Arc, LanguageServerBinaryStatus)>, + statuses: LspBinaryStatusSender, mut cx: AsyncAppContext, ) -> Result { if !container_dir.exists() { @@ -1081,19 +1096,15 @@ async fn get_binary( .cached_server_binary(container_dir.to_path_buf(), delegate.as_ref()) .await { - statuses - .broadcast((language.clone(), LanguageServerBinaryStatus::Cached)) - .await?; + statuses.send(language.clone(), LanguageServerBinaryStatus::Cached); return Ok(binary); } else { - statuses - .broadcast(( - language.clone(), - LanguageServerBinaryStatus::Failed { - error: format!("{:?}", error), - }, - )) - .await?; + statuses.send( + language.clone(), + LanguageServerBinaryStatus::Failed { + error: format!("{:?}", error), + }, + ); } } @@ -1105,27 +1116,21 @@ async fn fetch_latest_binary( language: Arc, delegate: &dyn LspAdapterDelegate, container_dir: &Path, - lsp_binary_statuses_tx: async_broadcast::Sender<(Arc, LanguageServerBinaryStatus)>, + lsp_binary_statuses_tx: LspBinaryStatusSender, ) -> Result { let container_dir: Arc = container_dir.into(); - lsp_binary_statuses_tx - .broadcast(( - language.clone(), - LanguageServerBinaryStatus::CheckingForUpdate, - )) - .await?; + lsp_binary_statuses_tx.send( + language.clone(), + LanguageServerBinaryStatus::CheckingForUpdate, + ); let version_info = adapter.fetch_latest_server_version(delegate).await?; - lsp_binary_statuses_tx - .broadcast((language.clone(), LanguageServerBinaryStatus::Downloading)) - .await?; + lsp_binary_statuses_tx.send(language.clone(), LanguageServerBinaryStatus::Downloading); let binary = adapter .fetch_server_binary(version_info, container_dir.to_path_buf(), delegate) .await?; - lsp_binary_statuses_tx - .broadcast((language.clone(), LanguageServerBinaryStatus::Downloaded)) - .await?; + lsp_binary_statuses_tx.send(language.clone(), LanguageServerBinaryStatus::Downloaded); Ok(binary) } From 0958def770cc592042bd8b66530ed4f55cd907e4 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 12 Sep 2023 11:24:48 -0600 Subject: [PATCH 034/404] Remove another supported exemption --- crates/vim/src/normal.rs | 24 ++++++++++--------- crates/vim/src/normal/delete.rs | 24 ++++++++++++------- .../src/test/neovim_backed_test_context.rs | 4 +--- 3 files changed, 30 insertions(+), 22 deletions(-) diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index 20344366944b382c42034ce39285f00fb78e6a31..c8d12f8ee33047c2148710b79360735d8ae9c114 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -356,7 +356,7 @@ mod test { use crate::{ state::Mode::{self}, - test::{ExemptionFeatures, NeovimBackedTestContext}, + test::NeovimBackedTestContext, }; #[gpui::test] @@ -762,20 +762,22 @@ mod test { #[gpui::test] async fn test_dd(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await.binding(["d", "d"]); - cx.assert("ˇ").await; - cx.assert("The ˇquick").await; - cx.assert_all(indoc! {" - The qˇuick - brown ˇfox - jumps ˇover"}) - .await; - cx.assert_exempted( + let mut cx = NeovimBackedTestContext::new(cx).await; + cx.assert_neovim_compatible("ˇ", ["d", "d"]).await; + cx.assert_neovim_compatible("The ˇquick", ["d", "d"]).await; + for marked_text in cx.each_marked_position(indoc! {" + The qˇuick + brown ˇfox + jumps ˇover"}) + { + cx.assert_neovim_compatible(&marked_text, ["d", "d"]).await; + } + cx.assert_neovim_compatible( indoc! {" The quick ˇ brown fox"}, - ExemptionFeatures::DeletionOnEmptyLine, + ["d", "d"], ) .await; } diff --git a/crates/vim/src/normal/delete.rs b/crates/vim/src/normal/delete.rs index 19ea6af875f6508ccac20447ad9d4d102a238882..848e9f725d815f2b7b327e3757eb1b9445e65636 100644 --- a/crates/vim/src/normal/delete.rs +++ b/crates/vim/src/normal/delete.rs @@ -322,24 +322,31 @@ mod test { let mut cx = NeovimBackedTestContext::new(cx) .await .binding(["d", "g", "g"]); - cx.assert_neovim_compatible(indoc! {" + cx.assert_neovim_compatible( + indoc! {" The quick brownˇ fox jumps over - the lazy"}, ["d", "g", "g"]) - .await; - cx.assert_neovim_compatible(indoc! {" + the lazy"}, + ["d", "g", "g"], + ) + .await; + cx.assert_neovim_compatible( + indoc! {" The quick brown fox jumps over - the lˇazy"}, ["d", "g", "g"]) - .await; + the lˇazy"}, + ["d", "g", "g"], + ) + .await; cx.assert_neovim_compatible( indoc! {" The qˇuick brown fox jumps over - the lazy"},["d", "g", "g"] + the lazy"}, + ["d", "g", "g"], ) .await; cx.assert_neovim_compatible( @@ -347,7 +354,8 @@ mod test { ˇ brown fox jumps over - the lazy"},["d", "g", "g"] + the lazy"}, + ["d", "g", "g"], ) .await; } diff --git a/crates/vim/src/test/neovim_backed_test_context.rs b/crates/vim/src/test/neovim_backed_test_context.rs index 0df5c32136b2818bba631f8d2c64c104a274944f..e58f805a026f32473ab18b4dccc9eb662ae6e541 100644 --- a/crates/vim/src/test/neovim_backed_test_context.rs +++ b/crates/vim/src/test/neovim_backed_test_context.rs @@ -13,15 +13,13 @@ use util::test::{generate_marked_text, marked_text_offsets}; use super::{neovim_connection::NeovimConnection, NeovimBackedBindingTestContext, VimTestContext}; use crate::state::Mode; -pub const SUPPORTED_FEATURES: &[ExemptionFeatures] = &[ExemptionFeatures::DeletionOnEmptyLine]; +pub const SUPPORTED_FEATURES: &[ExemptionFeatures] = &[]; /// Enum representing features we have tests for but which don't work, yet. Used /// to add exemptions and automatically #[derive(PartialEq, Eq)] pub enum ExemptionFeatures { // MOTIONS - // Deletions on empty lines miss some newlines - DeletionOnEmptyLine, // When an operator completes at the end of the file, an extra newline is left OperatorLastNewlineRemains, // Deleting a word on an empty line doesn't remove the newline From c6f293076efa72745de30ace1913279582696717 Mon Sep 17 00:00:00 2001 From: Julia Date: Tue, 12 Sep 2023 15:14:49 -0400 Subject: [PATCH 035/404] Avoid keeping stale LSP progress indicator state when server is removed --- crates/diagnostics/src/items.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/diagnostics/src/items.rs b/crates/diagnostics/src/items.rs index 89b4469d42d0f795f27db338cb2e84eb224431d8..c3733018b67e0142115c3baac2d8a068d5f6e328 100644 --- a/crates/diagnostics/src/items.rs +++ b/crates/diagnostics/src/items.rs @@ -32,7 +32,8 @@ impl DiagnosticIndicator { this.in_progress_checks.insert(*language_server_id); cx.notify(); } - project::Event::DiskBasedDiagnosticsFinished { language_server_id } => { + project::Event::DiskBasedDiagnosticsFinished { language_server_id } + | project::Event::LanguageServerRemoved(language_server_id) => { this.summary = project.read(cx).diagnostic_summary(cx); this.in_progress_checks.remove(language_server_id); cx.notify(); From bbc4673f178a2adc4759c077b328049aa38380ff Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 12 Sep 2023 15:18:13 -0400 Subject: [PATCH 036/404] Checkpoint --- crates/storybook/src/modules/title_bar.rs | 2 +- crates/storybook/src/prelude.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/storybook/src/modules/title_bar.rs b/crates/storybook/src/modules/title_bar.rs index dfd4e4490eb7028c82d7b3aaed68cc13790ed005..9e1e0b35332fb7869f3b917bec5f5bfb4d1dacd6 100644 --- a/crates/storybook/src/modules/title_bar.rs +++ b/crates/storybook/src/modules/title_bar.rs @@ -147,7 +147,7 @@ impl TitleBar { ) .child(div().px_2().flex().items_center().child(avatar( "https://avatars.githubusercontent.com/u/1714999?v=4", - Shape::Squircle, + Shape::RoundedRectangle, ))), ) } diff --git a/crates/storybook/src/prelude.rs b/crates/storybook/src/prelude.rs index 442567424ac6dc203b3287711a35ec6b1a0336a2..c925c64620e0d299aed6bfe0bfda881ebeeea925 100644 --- a/crates/storybook/src/prelude.rs +++ b/crates/storybook/src/prelude.rs @@ -7,7 +7,7 @@ pub enum ButtonVariant { #[derive(PartialEq)] pub enum Shape { Circle, - Squircle, + RoundedRectangle, } #[derive(PartialEq)] From a63b78d5a0b25497646dbfd0144eeb068e49e025 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 12 Sep 2023 22:08:39 +0200 Subject: [PATCH 037/404] Replace in buffer adjustments (#2960) This PR addresses feedback from @maxbrunsfeld on new replace in buffer. It fixes: - missing padding surrounding replace input. - missing padding around replace buttons. - missing `.notify` call which made the replace fields not show up immediately sometimes. Release Notes: - N/A --------- Co-authored-by: Max --- assets/keymaps/default.json | 9 ++++++++- crates/search/src/buffer_search.rs | 18 +++++++++++++++++- styles/src/style_tree/search.ts | 8 ++++++-- 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index fa62a74f3f7e27485cb0f36abd4c5ab5ab82ea38..dd5544f03483bb8f1551df9d3d06862dbf4c8edf 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -231,7 +231,14 @@ } }, { - "context": "BufferSearchBar > Editor", + "context": "BufferSearchBar && in_replace", + "bindings": { + "enter": "search::ReplaceNext", + "cmd-enter": "search::ReplaceAll" + } + }, + { + "context": "BufferSearchBar && !in_replace > Editor", "bindings": { "up": "search::PreviousHistoryQuery", "down": "search::NextHistoryQuery" diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 6a227812d17ef08bfe9f1153e378e34df333b4c7..9ae2b20f7af49626732697f7fdf418a9b4764fa7 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -101,6 +101,21 @@ impl View for BufferSearchBar { "BufferSearchBar" } + fn update_keymap_context( + &self, + keymap: &mut gpui::keymap_matcher::KeymapContext, + cx: &AppContext, + ) { + Self::reset_to_default_keymap_context(keymap); + let in_replace = self + .replacement_editor + .read_with(cx, |_, cx| cx.is_self_focused()) + .unwrap_or(false); + if in_replace { + keymap.add_identifier("in_replace"); + } + } + fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { if cx.is_self_focused() { cx.focus(&self.query_editor); @@ -868,9 +883,10 @@ impl BufferSearchBar { cx.propagate_action(); } } - fn toggle_replace(&mut self, _: &ToggleReplace, _: &mut ViewContext) { + fn toggle_replace(&mut self, _: &ToggleReplace, cx: &mut ViewContext) { if let Some(_) = &self.active_searchable_item { self.replace_is_active = !self.replace_is_active; + cx.notify(); } } fn replace_next(&mut self, _: &ReplaceNext, cx: &mut ViewContext) { diff --git a/styles/src/style_tree/search.ts b/styles/src/style_tree/search.ts index bc95b91819e875d757a5736dbd6fda6b8011aa49..b0ac023c09c90b35804ad6544df83195c83498f1 100644 --- a/styles/src/style_tree/search.ts +++ b/styles/src/style_tree/search.ts @@ -36,6 +36,7 @@ export default function search(): any { left: 10, right: 4, }, + margin: { right: SEARCH_ROW_SPACING } } const include_exclude_editor = { @@ -201,7 +202,6 @@ export default function search(): any { }, option_button_group: { padding: { - left: SEARCH_ROW_SPACING, right: SEARCH_ROW_SPACING, }, }, @@ -375,7 +375,11 @@ export default function search(): any { search_bar_row_height: 34, search_row_spacing: 8, option_button_height: 22, - modes_container: {}, + modes_container: { + padding: { + right: SEARCH_ROW_SPACING, + } + }, replace_icon: { icon: { color: foreground(theme.highest, "disabled"), From 66c967da8837b572b6f07c1965d020a0779f439f Mon Sep 17 00:00:00 2001 From: KCaverly Date: Tue, 12 Sep 2023 16:25:31 -0400 Subject: [PATCH 038/404] start work on eval script for semantic_index --- Cargo.lock | 19 ++++ crates/semantic_index/Cargo.toml | 4 + crates/semantic_index/eval/tree-sitter.json | 10 +++ crates/semantic_index/examples/eval.rs | 97 +++++++++++++++++++++ script/evaluate_semantic_index | 3 + 5 files changed, 133 insertions(+) create mode 100644 crates/semantic_index/eval/tree-sitter.json create mode 100644 crates/semantic_index/examples/eval.rs create mode 100755 script/evaluate_semantic_index diff --git a/Cargo.lock b/Cargo.lock index 775e1d2b8e3139b32e9b712f813977e93ebebefd..a66391ed077a41bebd67854aa79b33bed5ae7104 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3061,6 +3061,8 @@ dependencies = [ "libc", "libgit2-sys", "log", + "openssl-probe", + "openssl-sys", "url", ] @@ -4015,7 +4017,9 @@ checksum = "7f3d95f6b51075fe9810a7ae22c7095f12b98005ab364d8544797a825ce946a4" dependencies = [ "cc", "libc", + "libssh2-sys", "libz-sys", + "openssl-sys", "pkg-config", ] @@ -4056,6 +4060,20 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "libssh2-sys" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b094a36eb4b8b8c8a7b4b8ae43b2944502be3e59cd87687595cf6b0a71b3f4ca" +dependencies = [ + "cc", + "libc", + "libz-sys", + "openssl-sys", + "pkg-config", + "vcpkg", +] + [[package]] name = "libz-sys" version = "1.1.12" @@ -6731,6 +6749,7 @@ dependencies = [ "editor", "env_logger 0.9.3", "futures 0.3.28", + "git2", "globset", "gpui", "isahc", diff --git a/crates/semantic_index/Cargo.toml b/crates/semantic_index/Cargo.toml index 72a36efd508eecc5e28223e3593ad883a6084e2d..b5537dd2fa20464743185603c06b471509f52bd4 100644 --- a/crates/semantic_index/Cargo.toml +++ b/crates/semantic_index/Cargo.toml @@ -50,6 +50,7 @@ project = { path = "../project", features = ["test-support"] } rpc = { path = "../rpc", features = ["test-support"] } workspace = { path = "../workspace", features = ["test-support"] } settings = { path = "../settings", features = ["test-support"]} +git2 = { version = "0.15"} pretty_assertions.workspace = true rand.workspace = true @@ -67,3 +68,6 @@ tree-sitter-elixir.workspace = true tree-sitter-lua.workspace = true tree-sitter-ruby.workspace = true tree-sitter-php.workspace = true + +[[example]] +name = "eval" diff --git a/crates/semantic_index/eval/tree-sitter.json b/crates/semantic_index/eval/tree-sitter.json new file mode 100644 index 0000000000000000000000000000000000000000..a469543cf4db769223c27947fe8c7737543a6891 --- /dev/null +++ b/crates/semantic_index/eval/tree-sitter.json @@ -0,0 +1,10 @@ +{ + "repo": "https://github.com/tree-sitter/tree-sitter.git", + "commit": "46af27796a76c72d8466627d499f2bca4af958ee", + "assertions": [ + { + "query": "", + "matches": [] + } + ] +} diff --git a/crates/semantic_index/examples/eval.rs b/crates/semantic_index/examples/eval.rs new file mode 100644 index 0000000000000000000000000000000000000000..c3950757ce4372a7e1662dd8bd2184433a91eebe --- /dev/null +++ b/crates/semantic_index/examples/eval.rs @@ -0,0 +1,97 @@ +use git2::{Object, Oid, Repository}; +use serde::Deserialize; +use std::path::{Path, PathBuf}; +use std::{env, fs}; + +#[derive(Deserialize, Clone)] +struct QueryMatches { + query: String, + matches: Vec, +} + +#[derive(Deserialize, Clone)] +struct RepoEval { + repo: String, + commit: String, + assertions: Vec, +} + +const TMP_REPO_PATH: &str = "./target/eval_repos"; + +fn parse_eval() -> anyhow::Result> { + let eval_folder = env::current_dir()? + .as_path() + .parent() + .unwrap() + .join("crates/semantic_index/eval"); + + let mut repo_evals: Vec = Vec::new(); + for entry in fs::read_dir(eval_folder)? { + let file_path = entry.unwrap().path(); + if let Some(extension) = file_path.extension() { + if extension == "json" { + if let Ok(file) = fs::read_to_string(file_path) { + let repo_eval = serde_json::from_str(file.as_str()); + + match repo_eval { + Ok(repo_eval) => { + repo_evals.push(repo_eval); + } + Err(err) => { + println!("Err: {:?}", err); + } + } + } + } + } + } + + Ok(repo_evals) +} + +fn clone_repo(repo_eval: RepoEval) -> anyhow::Result { + let repo_name = Path::new(repo_eval.repo.as_str()) + .file_name() + .unwrap() + .to_str() + .unwrap() + .to_owned() + .replace(".git", ""); + let clone_path = Path::new(TMP_REPO_PATH).join(&repo_name).to_path_buf(); + + // Delete Clone Path if already exists + let _ = fs::remove_dir_all(&clone_path); + + // Clone in Repo + git2::build::RepoBuilder::new() + // .branch(repo_eval.sha.as_str()) + .clone(repo_eval.repo.as_str(), clone_path.as_path())?; + + let repo: Repository = Repository::open(clone_path.clone())?; + let obj: Object = repo + .find_commit(Oid::from_str(repo_eval.commit.as_str())?)? + .into_object(); + repo.checkout_tree(&obj, None)?; + repo.set_head_detached(obj.id())?; + + Ok(clone_path) +} + +fn main() { + if let Ok(repo_evals) = parse_eval() { + for repo in repo_evals { + let cloned = clone_repo(repo.clone()); + match cloned { + Ok(clone_path) => { + println!( + "Cloned {:?} @ {:?} into {:?}", + repo.repo, repo.commit, clone_path + ); + } + Err(err) => { + println!("Error Cloning: {:?}", err); + } + } + } + } +} diff --git a/script/evaluate_semantic_index b/script/evaluate_semantic_index new file mode 100755 index 0000000000000000000000000000000000000000..e9a96a02b40d46c09dbf588361bfc98990321377 --- /dev/null +++ b/script/evaluate_semantic_index @@ -0,0 +1,3 @@ +#!/bin/bash + +cargo run -p semantic_index --example eval From 0d14bbbf5b14ae4045cea65a68e3d6341f48f79c Mon Sep 17 00:00:00 2001 From: KCaverly Date: Tue, 12 Sep 2023 20:36:06 -0400 Subject: [PATCH 039/404] add eval values for tree-sitter --- crates/semantic_index/README.md | 39 +++----- crates/semantic_index/eval/tree-sitter.json | 98 ++++++++++++++++++++- 2 files changed, 110 insertions(+), 27 deletions(-) diff --git a/crates/semantic_index/README.md b/crates/semantic_index/README.md index 86e68dc414f7e655546094f9d188c1e8d7148b0f..85f83af121ed96a51ac84165c19cda3cd8aff7d4 100644 --- a/crates/semantic_index/README.md +++ b/crates/semantic_index/README.md @@ -1,31 +1,20 @@ -WIP: Sample SQL Queries -/* +# Semantic Index -create table "files" ( -"id" INTEGER PRIMARY KEY, -"path" VARCHAR, -"sha1" VARCHAR, -); +## Evaluation -create table symbols ( -"file_id" INTEGER REFERENCES("files", "id") ON CASCADE DELETE, -"offset" INTEGER, -"embedding" VECTOR, -); +### Metrics -insert into "files" ("path", "sha1") values ("src/main.rs", "sha1") return id; -insert into symbols ( -"file_id", -"start", -"end", -"embedding" -) values ( -(id,), -(id,), -(id,), -(id,), -) +nDCG@k: +- "The value of NDCG is determined by comparing the relevance of the items returned by the search engine to the relevance of the item that a hypothetical "ideal" search engine would return. +- "The relevance of result is represented by a score (also known as a 'grade') that is assigned to the search query. The scores of these results are then discounted based on their position in the search results -- did they get recommended first or last?" +MRR@k: +- "Mean reciprocal rank quantifies the rank of the first relevant item found in teh recommendation list." -*/ +MAP@k: +- "Mean average precision averages the precision@k metric at each relevant item position in the recommendation list. + +Resources: +- [Evaluating recommendation metrics](https://www.shaped.ai/blog/evaluating-recommendation-systems-map-mmr-ndcg) +- [Math Walkthrough](https://towardsdatascience.com/demystifying-ndcg-bee3be58cfe0) diff --git a/crates/semantic_index/eval/tree-sitter.json b/crates/semantic_index/eval/tree-sitter.json index a469543cf4db769223c27947fe8c7737543a6891..4f2edfb063345b59260e52582ea38674aaf8f2f0 100644 --- a/crates/semantic_index/eval/tree-sitter.json +++ b/crates/semantic_index/eval/tree-sitter.json @@ -3,8 +3,102 @@ "commit": "46af27796a76c72d8466627d499f2bca4af958ee", "assertions": [ { - "query": "", - "matches": [] + "query": "What attributes are available for the tags configuration struct?", + "matches": [ + "tags/src/lib.rs:24" + ] + }, + { + "query": "create a new tag configuration", + "matches": [ + "tags/src/lib.rs:119" + ] + }, + { + "query": "generate tags based on config", + "matches": [ + "tags/src/lib.rs:261", + ] + }, + { + "query": "match on ts quantifier in rust", + "matches": [ + "lib/binding_rust/lib.rs:139" + ] + }, + { + "query": "cli command to generate tags", + "matches": [ + "cli/src/tags.rs:10" + ] + }, + { + "query": "what version of the tree-sitter-tags package is active?", + "matches": [ + "tags/Cargo.toml:4" + ] + }, + { + "query": "Insert a new parse state", + "matches": [ + "cli/src/generate/build_tables/build_parse_table.rs:153" + ] + }, + { + "query": "Handle conflict when numerous actions occur on the same symbol", + "matches": [ + "cli/src/generate/build_tables/build_parse_table.rs:363", + "cli/src/generate/build_tables/build_parse_table.rs:442", + ] + }, + { + "query": "Match based on associativity of actions", + "matches": [ + "cri/src/generate/build_tables/build_parse_table.rs:542", + ] + }, + { + "query": "Format token set display", + "matches": [ + "cli/src/generate/build_tables/item.rs:246", + ] + }, + { + "query": "extract choices from rule", + "matches": [ + "cli/src/generate/prepare_grammar/flatten_grammar.rs:124" + ] + }, + { + "query": "How do we identify if a symbol is being used?", + "matches": [ + "cli/src/generate/prepare_grammar/flatten_grammar.rs:175" + ] + }, + { + "query": "How do we launch the playground?", + "matches": [ + "cli/src/playground.rs:46" + ] + }, + { + "query": "How do we test treesitter query matches in rust?", + "matches": [ + "cli/src/query_testing.rs:152", + "cli/src/tests/query_test.rs:781", + "cli/src/tests/query_test.rs:2163", + "cli/src/tests/query_test.rs:3781", + "cli/src/tests/query_test.rs:887" + ] + }, + { + "query": "What does the CLI do?", + "matches": [ + "cli/README.md:10", + "cli/loader/README.md:3", + "docs/section-5-implementation.md:14", + "docs/section-5-implementation.md:18" + ] } ] } From d4fbe990520fd079dc99e8120a0d08ff1076ef69 Mon Sep 17 00:00:00 2001 From: KCaverly Date: Tue, 12 Sep 2023 21:27:35 -0400 Subject: [PATCH 040/404] add eval for gpt-engineer --- crates/semantic_index/eval/gpt-engineer.json | 114 +++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 crates/semantic_index/eval/gpt-engineer.json diff --git a/crates/semantic_index/eval/gpt-engineer.json b/crates/semantic_index/eval/gpt-engineer.json new file mode 100644 index 0000000000000000000000000000000000000000..d7c08cd505bda5f70bc6bdb05c967bc005522b97 --- /dev/null +++ b/crates/semantic_index/eval/gpt-engineer.json @@ -0,0 +1,114 @@ +{ + "repo": "https://github.com/AntonOsika/gpt-engineer.git", + "commit": "7735a6445bae3611c62f521e6464c67c957f87c2", + "assertions": [ + { + "query": "How do I contribute to this project?", + "matches": [ + ".github/CONTRIBUTING.md:1", + "ROADMAP.md:48" + ] + }, + { + "query": "What version of the openai package is active?", + "matches": [ + "pyproject.toml:14" + ] + }, + { + "query": "Ask user for clarification", + "matches": [ + "gpt-engineer/steps.py:69" + ] + }, + { + "query": "generate tests for python code", + "matches": [ + "gpt-engineer/steps.py:153" + ] + }, + { + "query": "get item from database based on key", + "matches": [ + "gpt-engineer/db.py:42", + "gpt-engineer/db.py:68" + ] + }, + { + "query": "prompt user to select files", + "matches": [ + "gpt-engineer/file_selector.py:171", + "gpt-engineer/file_selector.py:306", + "gpt-engineer/file_selector.py:289", + "gpt-engineer/file_selector.py:234" + ] + }, + { + "query": "send to rudderstack", + "matches": [ + "gpt-engineer/collect.py:11", + "gpt-engineer/collect.py:38" + ] + }, + { + "query": "parse code blocks from chat messages", + "matches": [ + "gpt-engineer/chat_to_files.py:10", + "docs/intro/chat_parsing.md:1" + ] + }, + { + "query": "how do I use the docker cli?", + "matches": [ + "docker/README.md:1" + ] + }, + { + "query": "ask the user if the code ran successfully?", + "matches": [ + "gpt-engineer/learning.py:54" + ] + }, + { + "query": "how is consent granted by the user?", + "matches": [ + "gpt-engineer/learning.py:107", + "gpt-engineer/learning.py:130", + "gpt-engineer/learning.py:152" + ] + }, + { + "query": "what are all the different steps the agent can take?", + "matches": [ + "docs/intro/steps_module.md:1", + "gpt-engineer/steps.py:391" + ] + }, + { + "query": "ask the user for clarification?", + "matches": [ + "gpt-engineer/steps.py:69" + ] + }, + { + "query": "what models are available?", + "matches": [ + "gpt-engineer/ai.py:315", + "gpt-engineer/ai.py:341", + "docs/open-models.md:1" + ] + }, + { + "query": "what is the current focus of the project?", + "matches": [ + "ROADMAP.md:11" + ] + }, + { + "query": "does the agent know how to fix code?", + "matches": [ + "gpt-engineer/steps.py:367" + ] + } + ] +} From 54838664ae59391c6a18fbb4c5d8e87d761b1767 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 12 Sep 2023 21:04:59 -0700 Subject: [PATCH 041/404] Retrieve load balancer certificate id from DigitalOcean on each deploy Co-authored-by: Mikayla --- crates/collab/k8s/manifest.template.yml | 3 ++- script/deploy | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/collab/k8s/manifest.template.yml b/crates/collab/k8s/manifest.template.yml index 79dd2b885104bac14afcdfbb3bb40e9d65d40b8c..8f6915019bc8ceb4e1743188afb536ec6671b6f5 100644 --- a/crates/collab/k8s/manifest.template.yml +++ b/crates/collab/k8s/manifest.template.yml @@ -3,6 +3,7 @@ apiVersion: v1 kind: Namespace metadata: name: ${ZED_KUBE_NAMESPACE} + --- kind: Service apiVersion: v1 @@ -11,7 +12,7 @@ metadata: name: collab annotations: service.beta.kubernetes.io/do-loadbalancer-tls-ports: "443" - service.beta.kubernetes.io/do-loadbalancer-certificate-id: "08d9d8ce-761f-4ab3-bc78-4923ab5b0e33" + service.beta.kubernetes.io/do-loadbalancer-certificate-id: ${ZED_DO_CERTIFICATE_ID} spec: type: LoadBalancer selector: diff --git a/script/deploy b/script/deploy index f675da6a99ecb7441f7cccff0242077b4b023da1..efccb18506de28beb41c2517453259ee627ca8ea 100755 --- a/script/deploy +++ b/script/deploy @@ -13,6 +13,7 @@ version=$2 export_vars_for_environment ${environment} image_id=$(image_id_for_version ${version}) +export ZED_DO_CERTIFICATE_ID=$(doctl compute certificate list --format ID --no-header) export ZED_KUBE_NAMESPACE=${environment} export ZED_IMAGE_ID=${image_id} From 94db0be3ec5dd821782841a010b17a7e43cc2bb2 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 12 Sep 2023 21:06:43 -0700 Subject: [PATCH 042/404] Start work on deploying pgAdmin to k8s cluster Co-authored-by: Mikayla --- crates/collab/k8s/manifest.template.yml | 115 ++++++++++++++++++++++++ script/deploy | 2 +- 2 files changed, 116 insertions(+), 1 deletion(-) diff --git a/crates/collab/k8s/manifest.template.yml b/crates/collab/k8s/manifest.template.yml index 8f6915019bc8ceb4e1743188afb536ec6671b6f5..5af1aad450f9cc111fbb4f4130625293721efcec 100644 --- a/crates/collab/k8s/manifest.template.yml +++ b/crates/collab/k8s/manifest.template.yml @@ -22,6 +22,26 @@ spec: protocol: TCP port: 443 targetPort: 8080 + +--- +kind: Service +apiVersion: v1 +metadata: + namespace: ${ZED_KUBE_NAMESPACE} + name: pgadmin + annotations: + service.beta.kubernetes.io/do-loadbalancer-tls-ports: "443" + service.beta.kubernetes.io/do-loadbalancer-certificate-id: ${ZED_DO_CERTIFICATE_ID} +spec: + type: LoadBalancer + selector: + app: pgadmin + ports: + - name: web + protocol: TCP + port: 443 + targetPort: 8080 + --- apiVersion: apps/v1 kind: Deployment @@ -118,3 +138,98 @@ spec: # FIXME - Switch to the more restrictive `PERFMON` capability. # This capability isn't yet available in a stable version of Debian. add: ["SYS_ADMIN"] + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + namespace: ${ZED_KUBE_NAMESPACE} + name: pgadmin + +spec: + replicas: 1 + selector: + matchLabels: + app: pgadmin + template: + metadata: + labels: + app: pgadmin + spec: + securityContext: + runAsUser: 0 + containers: + - name: pgadmin + image: "dpage/pgadmin4" + ports: + - containerPort: 8080 + protocol: TCP + livenessProbe: + httpGet: + path: /misc/ping + port: 8080 + initialDelaySeconds: 30 + periodSeconds: 5 + timeoutSeconds: 5 + readinessProbe: + httpGet: + path: /misc/ping + port: 8080 + initialDelaySeconds: 1 + periodSeconds: 1 + command: ['/bin/sh', '-c'] + args: + - | + set -e + + python3 - < Date: Mon, 11 Sep 2023 16:33:25 +0200 Subject: [PATCH 043/404] Never use the indentation that comes from OpenAI --- crates/ai/src/assistant.rs | 51 +++--- crates/ai/src/codegen.rs | 338 +++++++++++++++++++++++++------------ 2 files changed, 257 insertions(+), 132 deletions(-) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 1d56a6308cfeab744a092968f5f9bc2d5470efb6..6d4fce2f6d09de085f2e78e8f10aeaad8c0bd16c 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -1,6 +1,6 @@ use crate::{ assistant_settings::{AssistantDockPosition, AssistantSettings, OpenAIModel}, - codegen::{self, Codegen, OpenAICompletionProvider}, + codegen::{self, Codegen, CodegenKind, OpenAICompletionProvider}, stream_completion, MessageId, MessageMetadata, MessageStatus, OpenAIRequest, RequestMessage, Role, SavedConversation, SavedConversationMetadata, SavedMessage, OPENAI_API_URL, }; @@ -270,24 +270,28 @@ impl AssistantPanel { let inline_assist_id = post_inc(&mut self.next_inline_assist_id); let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx); - let selection = editor.read(cx).selections.newest_anchor().clone(); - let range = selection.start.bias_left(&snapshot)..selection.end.bias_right(&snapshot); let provider = Arc::new(OpenAICompletionProvider::new( api_key, cx.background().clone(), )); - let codegen = - cx.add_model(|cx| Codegen::new(editor.read(cx).buffer().clone(), range, provider, cx)); - let assist_kind = if editor.read(cx).selections.newest::(cx).is_empty() { - InlineAssistKind::Generate + let selection = editor.read(cx).selections.newest_anchor().clone(); + let codegen_kind = if editor.read(cx).selections.newest::(cx).is_empty() { + CodegenKind::Generate { + position: selection.start, + } } else { - InlineAssistKind::Transform + CodegenKind::Transform { + range: selection.start..selection.end, + } }; + let codegen = cx.add_model(|cx| { + Codegen::new(editor.read(cx).buffer().clone(), codegen_kind, provider, cx) + }); + let measurements = Rc::new(Cell::new(BlockMeasurements::default())); let inline_assistant = cx.add_view(|cx| { let assistant = InlineAssistant::new( inline_assist_id, - assist_kind, measurements.clone(), self.include_conversation_in_next_inline_assist, self.inline_prompt_history.clone(), @@ -330,7 +334,6 @@ impl AssistantPanel { self.pending_inline_assists.insert( inline_assist_id, PendingInlineAssist { - kind: assist_kind, editor: editor.downgrade(), inline_assistant: Some((block_id, inline_assistant.clone())), codegen: codegen.clone(), @@ -348,6 +351,14 @@ impl AssistantPanel { } } }), + cx.observe(&codegen, { + let editor = editor.downgrade(); + move |this, _, cx| { + if let Some(editor) = editor.upgrade(cx) { + this.update_highlights_for_editor(&editor, cx); + } + } + }), cx.subscribe(&codegen, move |this, codegen, event, cx| match event { codegen::Event::Undone => { this.finish_inline_assist(inline_assist_id, false, cx) @@ -542,8 +553,8 @@ impl AssistantPanel { if let Some(language_name) = language_name { writeln!(prompt, "You're an expert {language_name} engineer.").unwrap(); } - match pending_assist.kind { - InlineAssistKind::Transform => { + match pending_assist.codegen.read(cx).kind() { + CodegenKind::Transform { .. } => { writeln!( prompt, "You're currently working inside an editor on this file:" @@ -583,7 +594,7 @@ impl AssistantPanel { ) .unwrap(); } - InlineAssistKind::Generate => { + CodegenKind::Generate { .. } => { writeln!( prompt, "You're currently working inside an editor on this file:" @@ -2649,12 +2660,6 @@ enum InlineAssistantEvent { }, } -#[derive(Copy, Clone)] -enum InlineAssistKind { - Transform, - Generate, -} - struct InlineAssistant { id: usize, prompt_editor: ViewHandle, @@ -2769,7 +2774,6 @@ impl View for InlineAssistant { impl InlineAssistant { fn new( id: usize, - kind: InlineAssistKind, measurements: Rc>, include_conversation: bool, prompt_history: VecDeque, @@ -2781,9 +2785,9 @@ impl InlineAssistant { Some(Arc::new(|theme| theme.assistant.inline.editor.clone())), cx, ); - let placeholder = match kind { - InlineAssistKind::Transform => "Enter transformation prompt…", - InlineAssistKind::Generate => "Enter generation prompt…", + let placeholder = match codegen.read(cx).kind() { + CodegenKind::Transform { .. } => "Enter transformation prompt…", + CodegenKind::Generate { .. } => "Enter generation prompt…", }; editor.set_placeholder_text(placeholder, cx); editor @@ -2929,7 +2933,6 @@ struct BlockMeasurements { } struct PendingInlineAssist { - kind: InlineAssistKind, editor: WeakViewHandle, inline_assistant: Option<(BlockId, ViewHandle)>, codegen: ModelHandle, diff --git a/crates/ai/src/codegen.rs b/crates/ai/src/codegen.rs index 9657d9a4926c17eadb5ae7d78a020ee6342deacf..bc13e294fa8ca9a6e49c811a789ce663d4e1e37e 100644 --- a/crates/ai/src/codegen.rs +++ b/crates/ai/src/codegen.rs @@ -4,12 +4,14 @@ use crate::{ OpenAIRequest, }; use anyhow::Result; -use editor::{multi_buffer, Anchor, MultiBuffer, ToOffset, ToPoint}; +use editor::{ + multi_buffer, Anchor, AnchorRangeExt, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint, +}; use futures::{ channel::mpsc, future::BoxFuture, stream::BoxStream, FutureExt, SinkExt, Stream, StreamExt, }; use gpui::{executor::Background, Entity, ModelContext, ModelHandle, Task}; -use language::{IndentSize, Point, Rope, TransactionId}; +use language::{Rope, TransactionId}; use std::{cmp, future, ops::Range, sync::Arc}; pub trait CompletionProvider { @@ -57,10 +59,17 @@ pub enum Event { Undone, } +#[derive(Clone)] +pub enum CodegenKind { + Transform { range: Range }, + Generate { position: Anchor }, +} + pub struct Codegen { provider: Arc, buffer: ModelHandle, - range: Range, + snapshot: MultiBufferSnapshot, + kind: CodegenKind, last_equal_ranges: Vec>, transaction_id: Option, error: Option, @@ -76,14 +85,31 @@ impl Entity for Codegen { impl Codegen { pub fn new( buffer: ModelHandle, - range: Range, + mut kind: CodegenKind, provider: Arc, cx: &mut ModelContext, ) -> Self { + let snapshot = buffer.read(cx).snapshot(cx); + match &mut kind { + CodegenKind::Transform { range } => { + let mut point_range = range.to_point(&snapshot); + point_range.start.column = 0; + if point_range.end.column > 0 || point_range.start.row == point_range.end.row { + point_range.end.column = snapshot.line_len(point_range.end.row); + } + range.start = snapshot.anchor_before(point_range.start); + range.end = snapshot.anchor_after(point_range.end); + } + CodegenKind::Generate { position } => { + *position = position.bias_right(&snapshot); + } + } + Self { provider, buffer: buffer.clone(), - range, + snapshot, + kind, last_equal_ranges: Default::default(), transaction_id: Default::default(), error: Default::default(), @@ -109,7 +135,14 @@ impl Codegen { } pub fn range(&self) -> Range { - self.range.clone() + match &self.kind { + CodegenKind::Transform { range } => range.clone(), + CodegenKind::Generate { position } => position.bias_left(&self.snapshot)..*position, + } + } + + pub fn kind(&self) -> &CodegenKind { + &self.kind } pub fn last_equal_ranges(&self) -> &[Range] { @@ -125,56 +158,18 @@ impl Codegen { } pub fn start(&mut self, prompt: OpenAIRequest, cx: &mut ModelContext) { - let range = self.range.clone(); - let snapshot = self.buffer.read(cx).snapshot(cx); + let range = self.range(); + let snapshot = self.snapshot.clone(); let selected_text = snapshot .text_for_range(range.start..range.end) .collect::(); let selection_start = range.start.to_point(&snapshot); - let selection_end = range.end.to_point(&snapshot); - - let mut base_indent: Option = None; - let mut start_row = selection_start.row; - if snapshot.is_line_blank(start_row) { - if let Some(prev_non_blank_row) = snapshot.prev_non_blank_row(start_row) { - start_row = prev_non_blank_row; - } - } - for row in start_row..=selection_end.row { - if snapshot.is_line_blank(row) { - continue; - } - - let line_indent = snapshot.indent_size_for_line(row); - if let Some(base_indent) = base_indent.as_mut() { - if line_indent.len < base_indent.len { - *base_indent = line_indent; - } - } else { - base_indent = Some(line_indent); - } - } - - let mut normalized_selected_text = selected_text.clone(); - if let Some(base_indent) = base_indent { - for row in selection_start.row..=selection_end.row { - let selection_row = row - selection_start.row; - let line_start = - normalized_selected_text.point_to_offset(Point::new(selection_row, 0)); - let indent_len = if row == selection_start.row { - base_indent.len.saturating_sub(selection_start.column) - } else { - let line_len = normalized_selected_text.line_len(selection_row); - cmp::min(line_len, base_indent.len) - }; - let indent_end = cmp::min( - line_start + indent_len as usize, - normalized_selected_text.len(), - ); - normalized_selected_text.replace(line_start..indent_end, ""); - } - } + let suggested_line_indent = snapshot + .suggested_indents(selection_start.row..selection_start.row + 1, cx) + .into_values() + .next() + .unwrap_or_else(|| snapshot.indent_size_for_line(selection_start.row)); let response = self.provider.complete(prompt); self.generation = cx.spawn_weak(|this, mut cx| { @@ -188,66 +183,58 @@ impl Codegen { futures::pin_mut!(chunks); let mut diff = StreamingDiff::new(selected_text.to_string()); - let mut indent_len; - let indent_text; - if let Some(base_indent) = base_indent { - indent_len = base_indent.len; - indent_text = match base_indent.kind { - language::IndentKind::Space => " ", - language::IndentKind::Tab => "\t", - }; - } else { - indent_len = 0; - indent_text = ""; - }; - - let mut first_line_len = 0; - let mut first_line_non_whitespace_char_ix = None; - let mut first_line = true; let mut new_text = String::new(); + let mut base_indent = None; + let mut line_indent = None; + let mut first_line = true; while let Some(chunk) = chunks.next().await { let chunk = chunk?; - let mut lines = chunk.split('\n'); - if let Some(mut line) = lines.next() { - if first_line { - if first_line_non_whitespace_char_ix.is_none() { - if let Some(mut char_ix) = - line.find(|ch: char| !ch.is_whitespace()) - { - line = &line[char_ix..]; - char_ix += first_line_len; - first_line_non_whitespace_char_ix = Some(char_ix); - let first_line_indent = char_ix - .saturating_sub(selection_start.column as usize) - as usize; - new_text - .push_str(&indent_text.repeat(first_line_indent)); - indent_len = indent_len.saturating_sub(char_ix as u32); + let mut lines = chunk.split('\n').peekable(); + while let Some(line) = lines.next() { + new_text.push_str(line); + if line_indent.is_none() { + if let Some(non_whitespace_ch_ix) = + new_text.find(|ch: char| !ch.is_whitespace()) + { + line_indent = Some(non_whitespace_ch_ix); + base_indent = base_indent.or(line_indent); + + let line_indent = line_indent.unwrap(); + let base_indent = base_indent.unwrap(); + let indent_delta = line_indent as i32 - base_indent as i32; + let mut corrected_indent_len = cmp::max( + 0, + suggested_line_indent.len as i32 + indent_delta, + ) + as usize; + if first_line { + corrected_indent_len = corrected_indent_len + .saturating_sub(selection_start.column as usize); } - } - first_line_len += line.len(); - } - if first_line_non_whitespace_char_ix.is_some() { - new_text.push_str(line); + let indent_char = suggested_line_indent.char(); + let mut indent_buffer = [0; 4]; + let indent_str = + indent_char.encode_utf8(&mut indent_buffer); + new_text.replace_range( + ..line_indent, + &indent_str.repeat(corrected_indent_len), + ); + } } - } - for line in lines { - first_line = false; - new_text.push('\n'); - if !line.is_empty() { - new_text.push_str(&indent_text.repeat(indent_len as usize)); + if lines.peek().is_some() { + hunks_tx.send(diff.push_new(&new_text)).await?; + hunks_tx.send(diff.push_new("\n")).await?; + new_text.clear(); + line_indent = None; + first_line = false; } - new_text.push_str(line); } - - let hunks = diff.push_new(&new_text); - hunks_tx.send(hunks).await?; - new_text.clear(); } + hunks_tx.send(diff.push_new(&new_text)).await?; hunks_tx.send(diff.finish()).await?; anyhow::Ok(()) @@ -285,7 +272,7 @@ impl Codegen { let edit_end = edit_start + len; let edit_range = snapshot.anchor_after(edit_start) ..snapshot.anchor_before(edit_end); - edit_start += len; + edit_start = edit_end; this.last_equal_ranges.push(edit_range); None } @@ -410,16 +397,20 @@ mod tests { use futures::stream; use gpui::{executor::Deterministic, TestAppContext}; use indoc::indoc; - use language::{tree_sitter_rust, Buffer, Language, LanguageConfig}; + use language::{language_settings, tree_sitter_rust, Buffer, Language, LanguageConfig, Point}; use parking_lot::Mutex; use rand::prelude::*; + use settings::SettingsStore; #[gpui::test(iterations = 10)] - async fn test_autoindent( + async fn test_transform_autoindent( cx: &mut TestAppContext, mut rng: StdRng, deterministic: Arc, ) { + cx.set_global(cx.read(SettingsStore::test)); + cx.update(language_settings::init); + let text = indoc! {" fn main() { let x = 0; @@ -436,15 +427,146 @@ mod tests { snapshot.anchor_before(Point::new(1, 4))..snapshot.anchor_after(Point::new(4, 4)) }); let provider = Arc::new(TestCompletionProvider::new()); - let codegen = cx.add_model(|cx| Codegen::new(buffer.clone(), range, provider.clone(), cx)); + let codegen = cx.add_model(|cx| { + Codegen::new( + buffer.clone(), + CodegenKind::Transform { range }, + provider.clone(), + cx, + ) + }); codegen.update(cx, |codegen, cx| codegen.start(Default::default(), cx)); - let mut new_text = indoc! {" - let mut x = 0; - while x < 10 { - x += 1; - } + let mut new_text = concat!( + " let mut x = 0;\n", + " while x < 10 {\n", + " x += 1;\n", + " }", + ); + while !new_text.is_empty() { + let max_len = cmp::min(new_text.len(), 10); + let len = rng.gen_range(1..=max_len); + let (chunk, suffix) = new_text.split_at(len); + provider.send_completion(chunk); + new_text = suffix; + deterministic.run_until_parked(); + } + provider.finish_completion(); + deterministic.run_until_parked(); + + assert_eq!( + buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx).text()), + indoc! {" + fn main() { + let mut x = 0; + while x < 10 { + x += 1; + } + } + "} + ); + } + + #[gpui::test(iterations = 10)] + async fn test_autoindent_when_generating_past_indentation( + cx: &mut TestAppContext, + mut rng: StdRng, + deterministic: Arc, + ) { + cx.set_global(cx.read(SettingsStore::test)); + cx.update(language_settings::init); + + let text = indoc! {" + fn main() { + le + } "}; + let buffer = + cx.add_model(|cx| Buffer::new(0, 0, text).with_language(Arc::new(rust_lang()), cx)); + let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); + let position = buffer.read_with(cx, |buffer, cx| { + let snapshot = buffer.snapshot(cx); + snapshot.anchor_before(Point::new(1, 6)) + }); + let provider = Arc::new(TestCompletionProvider::new()); + let codegen = cx.add_model(|cx| { + Codegen::new( + buffer.clone(), + CodegenKind::Generate { position }, + provider.clone(), + cx, + ) + }); + codegen.update(cx, |codegen, cx| codegen.start(Default::default(), cx)); + + let mut new_text = concat!( + "t mut x = 0;\n", + "while x < 10 {\n", + " x += 1;\n", + "}", // + ); + while !new_text.is_empty() { + let max_len = cmp::min(new_text.len(), 10); + let len = rng.gen_range(1..=max_len); + let (chunk, suffix) = new_text.split_at(len); + provider.send_completion(chunk); + new_text = suffix; + deterministic.run_until_parked(); + } + provider.finish_completion(); + deterministic.run_until_parked(); + + assert_eq!( + buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx).text()), + indoc! {" + fn main() { + let mut x = 0; + while x < 10 { + x += 1; + } + } + "} + ); + } + + #[gpui::test(iterations = 10)] + async fn test_autoindent_when_generating_before_indentation( + cx: &mut TestAppContext, + mut rng: StdRng, + deterministic: Arc, + ) { + cx.set_global(cx.read(SettingsStore::test)); + cx.update(language_settings::init); + + let text = concat!( + "fn main() {\n", + " \n", + "}\n" // + ); + let buffer = + cx.add_model(|cx| Buffer::new(0, 0, text).with_language(Arc::new(rust_lang()), cx)); + let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); + let position = buffer.read_with(cx, |buffer, cx| { + let snapshot = buffer.snapshot(cx); + snapshot.anchor_before(Point::new(1, 2)) + }); + let provider = Arc::new(TestCompletionProvider::new()); + let codegen = cx.add_model(|cx| { + Codegen::new( + buffer.clone(), + CodegenKind::Generate { position }, + provider.clone(), + cx, + ) + }); + codegen.update(cx, |codegen, cx| codegen.start(Default::default(), cx)); + + let mut new_text = concat!( + "let mut x = 0;\n", + "while x < 10 {\n", + " x += 1;\n", + "}", // + ); while !new_text.is_empty() { let max_len = cmp::min(new_text.len(), 10); let len = rng.gen_range(1..=max_len); From 127d03516fb40a16f1cb362d9512653b57fffe67 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 13 Sep 2023 11:57:10 +0200 Subject: [PATCH 044/404] Diff lines one chunk at a time after discovering indentation --- crates/ai/src/codegen.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/crates/ai/src/codegen.rs b/crates/ai/src/codegen.rs index bc13e294fa8ca9a6e49c811a789ce663d4e1e37e..e7da46cdf95f50927f0f9350f45dfb361bdfbd2e 100644 --- a/crates/ai/src/codegen.rs +++ b/crates/ai/src/codegen.rs @@ -225,10 +225,13 @@ impl Codegen { } } - if lines.peek().is_some() { + if line_indent.is_some() { hunks_tx.send(diff.push_new(&new_text)).await?; - hunks_tx.send(diff.push_new("\n")).await?; new_text.clear(); + } + + if lines.peek().is_some() { + hunks_tx.send(diff.push_new("\n")).await?; line_indent = None; first_line = false; } From 6f29582fb064e709056236ceb732335d63bbbfe4 Mon Sep 17 00:00:00 2001 From: KCaverly Date: Wed, 13 Sep 2023 10:32:36 -0400 Subject: [PATCH 045/404] progress on eval --- crates/semantic_index/eval/gpt-engineer.json | 2 +- crates/semantic_index/eval/tree-sitter.json | 2 +- crates/semantic_index/examples/eval.rs | 80 +++++++++++++++++++- 3 files changed, 79 insertions(+), 5 deletions(-) diff --git a/crates/semantic_index/eval/gpt-engineer.json b/crates/semantic_index/eval/gpt-engineer.json index d7c08cd505bda5f70bc6bdb05c967bc005522b97..64322e8384cec0f615dc93d29bc8c03448d51c7f 100644 --- a/crates/semantic_index/eval/gpt-engineer.json +++ b/crates/semantic_index/eval/gpt-engineer.json @@ -12,7 +12,7 @@ { "query": "What version of the openai package is active?", "matches": [ - "pyproject.toml:14" + "pyprojet.toml:14" ] }, { diff --git a/crates/semantic_index/eval/tree-sitter.json b/crates/semantic_index/eval/tree-sitter.json index 4f2edfb063345b59260e52582ea38674aaf8f2f0..52d1e9df16ba586de27f3be9b4c755ca05038a01 100644 --- a/crates/semantic_index/eval/tree-sitter.json +++ b/crates/semantic_index/eval/tree-sitter.json @@ -48,7 +48,7 @@ "query": "Handle conflict when numerous actions occur on the same symbol", "matches": [ "cli/src/generate/build_tables/build_parse_table.rs:363", - "cli/src/generate/build_tables/build_parse_table.rs:442", + "cli/src/generate/build_tables/build_parse_table.rs:442" ] }, { diff --git a/crates/semantic_index/examples/eval.rs b/crates/semantic_index/examples/eval.rs index c3950757ce4372a7e1662dd8bd2184433a91eebe..f666f5c281d93898d086812a96cf5f77d783074f 100644 --- a/crates/semantic_index/examples/eval.rs +++ b/crates/semantic_index/examples/eval.rs @@ -1,19 +1,36 @@ use git2::{Object, Oid, Repository}; +use semantic_index::SearchResult; use serde::Deserialize; use std::path::{Path, PathBuf}; use std::{env, fs}; #[derive(Deserialize, Clone)] -struct QueryMatches { +struct EvaluationQuery { query: String, matches: Vec, } +impl EvaluationQuery { + fn match_pairs(&self) -> Vec<(PathBuf, usize)> { + let mut pairs = Vec::new(); + for match_identifier in self.matches { + let match_parts = match_identifier.split(":"); + + if let Some(file_path) = match_parts.next() { + if let Some(row_number) = match_parts.next() { + pairs.push((PathBuf::from(file_path), from_str::(row_number))); + } + } + + pairs + } +} + #[derive(Deserialize, Clone)] struct RepoEval { repo: String, commit: String, - assertions: Vec, + assertions: Vec, } const TMP_REPO_PATH: &str = "./target/eval_repos"; @@ -77,7 +94,60 @@ fn clone_repo(repo_eval: RepoEval) -> anyhow::Result { Ok(clone_path) } +fn dcg(hits: Vec) -> f32 { + let mut result = 0.0; + for (idx, hit) in hits.iter().enumerate() { + result += *hit as f32 / (2.0 + idx as f32).log2(); + } + + println!("DCG: {:?}", result); + result +} + +fn evaluate_ndcg(eval_query: EvaluationQuery, search_results: Vec, k: usize) -> f32 { + + // NDCG or Normalized Discounted Cumulative Gain, is determined by comparing the relevance of + // items returned by the search engine relative to the hypothetical ideal. + // Relevance is represented as a series of booleans, in which each search result returned + // is identified as being inside the test set of matches (1) or not (0). + + // For example, if result 1, 3 and 5 match the 3 relevant results provided + // actual dcg is calculated against a vector of [1, 0, 1, 0, 1] + // whereas ideal dcg is calculated against a vector of [1, 1, 1, 0, 0] + // as this ideal vector assumes the 3 relevant results provided were returned first + // normalized dcg is then calculated as actual dcg / ideal dcg. + + // NDCG ranges from 0 to 1, which higher values indicating better performance + // Commonly NDCG is expressed as NDCG@k, in which k represents the metric calculated + // including only the top k values returned. + // The @k metrics can help you identify, at what point does the relevant results start to fall off. + // Ie. a NDCG@1 of 0.9 and a NDCG@3 of 0.5 may indicate that the first result returned in usually + // very high quality, whereas rank results quickly drop off after the first result. + + let ideal = vec![1; cmp::min(eval_query.matches.len(), k)]; + + return dcg(hits) / dcg(ideal); +} + +fn evaluate_map(eval_query: EvaluationQuery, search_results: Vec, k: usize) -> f32 { + +} + +fn evaluate_repo(repo_eval: RepoEval, clone_path: PathBuf) { + + // Launch new repo as a new Zed workspace/project + // Index the project + // Search each eval_query + // Calculate Statistics + +} + fn main() { + + // zed/main.rs + // creating an app and running it, gives you the context. + // create a project, find_or_create_local_worktree. + if let Ok(repo_evals) = parse_eval() { for repo in repo_evals { let cloned = clone_repo(repo.clone()); @@ -85,8 +155,12 @@ fn main() { Ok(clone_path) => { println!( "Cloned {:?} @ {:?} into {:?}", - repo.repo, repo.commit, clone_path + repo.repo, repo.commit, &clone_path ); + + // Evaluate Repo + evaluate_repo(repo, clone_path); + } Err(err) => { println!("Error Cloning: {:?}", err); From a6cb5f99f338d278287dea11bfe7bd117a09291b Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 13 Sep 2023 12:22:33 -0400 Subject: [PATCH 046/404] v0.105.x dev --- Cargo.lock | 2 +- crates/zed/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index eb7543491dbec35c627d90151ea52fbcdda21dd8..5d713352301339878445414f55a6412856da8a0f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9794,7 +9794,7 @@ dependencies = [ [[package]] name = "zed" -version = "0.104.0" +version = "0.105.0" dependencies = [ "activity_indicator", "ai", diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 1d014197e1e14262180763151c2649dd04158686..b2339f998f292161eedeaf4a5ef5f2365d9bee3f 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -3,7 +3,7 @@ authors = ["Nathan Sobo "] description = "The fast, collaborative code editor." edition = "2021" name = "zed" -version = "0.104.0" +version = "0.105.0" publish = false [lib] From f54f2c52e9c7a3a4e93f22019daac845c55fec55 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Wed, 13 Sep 2023 12:40:28 -0400 Subject: [PATCH 047/404] Checkpoint --- .../storybook/src/components/icon_button.rs | 28 +++++++----- crates/storybook/src/modules/chat_panel.rs | 14 ++---- crates/storybook/src/modules/tab_bar.rs | 37 +++++++--------- crates/storybook/src/modules/title_bar.rs | 33 +++----------- crates/storybook/src/prelude.rs | 34 +++++++++++---- docs/ui/states.md | 43 +++++++++++++++++++ 6 files changed, 110 insertions(+), 79 deletions(-) create mode 100644 docs/ui/states.md diff --git a/crates/storybook/src/components/icon_button.rs b/crates/storybook/src/components/icon_button.rs index cecb6ccfc3570d05716d79e16c6294e32b4187f9..32ef1d0ce2ae4fda2f17ccc4ea0ff1b12d5d2acc 100644 --- a/crates/storybook/src/components/icon_button.rs +++ b/crates/storybook/src/components/icon_button.rs @@ -1,4 +1,4 @@ -use crate::prelude::{ButtonVariant, UIState}; +use crate::prelude::{ButtonVariant, InteractionState}; use crate::theme::theme; use gpui2::elements::svg; use gpui2::style::{StyleHelpers, Styleable}; @@ -6,31 +6,37 @@ use gpui2::{elements::div, IntoElement}; use gpui2::{Element, ParentElement, ViewContext}; #[derive(Element)] -pub(crate) struct IconButton { +pub struct IconButton { path: &'static str, variant: ButtonVariant, - state: UIState, + state: InteractionState, } -pub fn icon_button( - path: &'static str, - variant: ButtonVariant, - state: UIState, -) -> impl Element { +pub fn icon_button(path: &'static str) -> IconButton { IconButton { path, - variant, - state, + variant: ButtonVariant::default(), + state: InteractionState::default(), } } impl IconButton { + pub fn variant(mut self, variant: ButtonVariant) -> Self { + self.variant = variant; + self + } + + pub fn state(mut self, state: InteractionState) -> Self { + self.state = state; + self + } + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { let theme = theme(cx); let icon_color; - if self.state == UIState::Disabled { + if self.state == InteractionState::Disabled { icon_color = theme.highest.base.disabled.foreground; } else { icon_color = theme.highest.base.default.foreground; diff --git a/crates/storybook/src/modules/chat_panel.rs b/crates/storybook/src/modules/chat_panel.rs index 772bb908e0b39a16cb020fd93441e31262f4343c..25bd9debe3c77bb121c5c8dc6d115342e7c453c9 100644 --- a/crates/storybook/src/modules/chat_panel.rs +++ b/crates/storybook/src/modules/chat_panel.rs @@ -1,12 +1,12 @@ use std::marker::PhantomData; use crate::components::icon_button; -use crate::prelude::{ButtonVariant, UIState}; use crate::theme::theme; use gpui2::elements::div::ScrollState; use gpui2::style::StyleHelpers; use gpui2::{elements::div, IntoElement}; use gpui2::{Element, ParentElement, ViewContext}; +use theme::IconButton; #[derive(Element)] pub struct ChatPanel { @@ -58,16 +58,8 @@ impl ChatPanel { .flex() .items_center() .gap_px() - .child(icon_button( - "icons/plus.svg", - ButtonVariant::Ghost, - UIState::Default, - )) - .child(icon_button( - "icons/split.svg", - ButtonVariant::Ghost, - UIState::Default, - )), + .child(icon_button::("icons/plus.svg")) + .child(icon_button::("icons/split.svg")), ), ) } diff --git a/crates/storybook/src/modules/tab_bar.rs b/crates/storybook/src/modules/tab_bar.rs index 5a3358588ef282bbcde1dde31f5487d75271d375..8cc7ab9433cd1cb6e15682b8798db08f0e06f727 100644 --- a/crates/storybook/src/modules/tab_bar.rs +++ b/crates/storybook/src/modules/tab_bar.rs @@ -1,12 +1,13 @@ use std::marker::PhantomData; use crate::components::{icon_button, tab}; -use crate::prelude::{ButtonVariant, UIState}; +use crate::prelude::InteractionState; use crate::theme::theme; use gpui2::elements::div::ScrollState; use gpui2::style::StyleHelpers; use gpui2::{elements::div, IntoElement}; use gpui2::{Element, ParentElement, ViewContext}; +use theme::IconButton; #[derive(Element)] pub struct TabBar { @@ -24,7 +25,8 @@ pub fn tab_bar(scroll_state: ScrollState) -> TabBar { impl TabBar { fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { let theme = theme(cx); - + let can_navigate_back = true; + let can_navigate_forward = false; div() .w_full() .flex() @@ -41,16 +43,15 @@ impl TabBar { .flex() .items_center() .gap_px() - .child(icon_button( - "icons/arrow_left.svg", - ButtonVariant::Ghost, - UIState::Default, - )) - .child(icon_button( - "icons/arrow_right.svg", - ButtonVariant::Ghost, - UIState::Disabled, - )), + .child( + icon_button::("icons/arrow_left.svg") + .state(InteractionState::Enabled.if_enabled(can_navigate_back)), + ) + .child( + icon_button::("icons/arrow_right.svg").state( + InteractionState::Enabled.if_enabled(can_navigate_forward), + ), + ), ), ) .child( @@ -83,16 +84,8 @@ impl TabBar { .flex() .items_center() .gap_px() - .child(icon_button( - "icons/plus.svg", - ButtonVariant::Ghost, - UIState::Default, - )) - .child(icon_button( - "icons/split.svg", - ButtonVariant::Ghost, - UIState::Default, - )), + .child(icon_button::("icons/plus.svg")) + .child(icon_button::("icons/split.svg")), ), ) } diff --git a/crates/storybook/src/modules/title_bar.rs b/crates/storybook/src/modules/title_bar.rs index 9e1e0b35332fb7869f3b917bec5f5bfb4d1dacd6..ead95c20092edd2f695fb2ec6191f6ed95c3a141 100644 --- a/crates/storybook/src/modules/title_bar.rs +++ b/crates/storybook/src/modules/title_bar.rs @@ -1,11 +1,12 @@ use std::marker::PhantomData; use crate::components::{avatar, icon_button, tool_divider}; -use crate::prelude::{ButtonVariant, Shape, UIState}; +use crate::prelude::Shape; use crate::theme::theme; use gpui2::style::{StyleHelpers, Styleable}; use gpui2::{elements::div, IntoElement}; use gpui2::{Element, ParentElement, ViewContext}; +use theme::IconButton; #[derive(Element)] pub struct TitleBar { @@ -111,16 +112,8 @@ impl TitleBar { .flex() .items_center() .gap_1() - .child(icon_button( - "icons/stop_sharing.svg", - ButtonVariant::Ghost, - UIState::Default, - )) - .child(icon_button( - "icons/exit.svg", - ButtonVariant::Ghost, - UIState::Default, - )), + .child(icon_button::("icons/stop_sharing.svg")) + .child(icon_button::("icons/exit.svg")), ) .child(tool_divider()) .child( @@ -129,21 +122,9 @@ impl TitleBar { .flex() .items_center() .gap_1() - .child(icon_button( - "icons/radix/mic.svg", - ButtonVariant::Ghost, - UIState::Default, - )) - .child(icon_button( - "icons/radix/speaker-loud.svg", - ButtonVariant::Ghost, - UIState::Default, - )) - .child(icon_button( - "icons/radix/desktop.svg", - ButtonVariant::Ghost, - UIState::Default, - )), + .child(icon_button::("icons/radix/mic.svg")) + .child(icon_button::("icons/radix/speaker-loud.svg")) + .child(icon_button::("icons/radix/desktop.svg")), ) .child(div().px_2().flex().items_center().child(avatar( "https://avatars.githubusercontent.com/u/1714999?v=4", diff --git a/crates/storybook/src/prelude.rs b/crates/storybook/src/prelude.rs index c925c64620e0d299aed6bfe0bfda881ebeeea925..50ab042cfe80820835cbc530eaa16bf020ad7971 100644 --- a/crates/storybook/src/prelude.rs +++ b/crates/storybook/src/prelude.rs @@ -1,26 +1,42 @@ -#[derive(PartialEq)] +#[derive(Default, PartialEq)] pub enum ButtonVariant { + #[default] Ghost, Filled, } -#[derive(PartialEq)] +#[derive(Default, PartialEq)] pub enum Shape { + #[default] Circle, RoundedRectangle, } -#[derive(PartialEq)] -pub enum UIState { - Default, +#[derive(Default, PartialEq, Clone, Copy)] +pub enum InteractionState { + #[default] + Enabled, Hovered, Active, Focused, + Dragged, Disabled, } -#[derive(PartialEq)] -pub enum UIToggleState { - Default, - Enabled, +impl InteractionState { + pub fn if_enabled(&self, enabled: bool) -> Self { + if enabled { + *self + } else { + InteractionState::Disabled + } + } +} + +#[derive(Default, PartialEq)] +pub enum SelectedState { + #[default] + Unselected, + PartiallySelected, + Selected, } diff --git a/docs/ui/states.md b/docs/ui/states.md new file mode 100644 index 0000000000000000000000000000000000000000..7dc3110ceda07386fbede131e155aeb7f8a87c01 --- /dev/null +++ b/docs/ui/states.md @@ -0,0 +1,43 @@ +## Interaction State + +**Enabled** + +An enabled state communicates an interactive component or element. + +**Disabled** + +A disabled state communicates a inoperable component or element. + +**Hover** + +A hover state communicates when a user has placed a cursor above an interactive element. + +**Focused** + +A focused state communicates when a user has highlighted an element, using an input method such as a keyboard or voice. + +**Activated** + +An activated state communicates a highlighted destination, whether initiated by the user or by default. + +**Pressed** + +A pressed state communicates a user tap. + +**Dragged** + +A dragged state communicates when a user presses and moves an element. + +## Selected State + +**Unselected** + +dfa + +**Partially Selected** + +daf + +**Selected** + +dfa From a316e2503435b19b6cc3b5ea3a12b383e5a45d2e Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Wed, 13 Sep 2023 12:50:01 -0400 Subject: [PATCH 048/404] Checkpoint --- crates/storybook/src/components.rs | 12 +- crates/storybook/src/components/avatar.rs | 11 +- crates/storybook/src/components/icon_button2 | 123 ++++++++++++++++++ crates/storybook/src/components/tab.rs | 2 +- .../storybook/src/components/tool_divider.rs | 2 +- crates/storybook/src/modules/chat_panel.rs | 3 +- crates/storybook/src/modules/tab_bar.rs | 3 +- crates/storybook/src/modules/title_bar.rs | 13 +- 8 files changed, 150 insertions(+), 19 deletions(-) create mode 100644 crates/storybook/src/components/icon_button2 diff --git a/crates/storybook/src/components.rs b/crates/storybook/src/components.rs index e0ba73b866a6e6cd40006f3e0eb214a05b8b2768..36ac1575f553231a09c6602da274eb821fa79c88 100644 --- a/crates/storybook/src/components.rs +++ b/crates/storybook/src/components.rs @@ -9,10 +9,14 @@ mod icon_button; mod tab; mod tool_divider; -pub(crate) use avatar::avatar; -pub(crate) use icon_button::icon_button; -pub(crate) use tab::tab; -pub(crate) use tool_divider::tool_divider; +pub use avatar::avatar; +pub use avatar::Avatar; +pub use icon_button::icon_button; +pub use icon_button::IconButton; +pub use tab::tab; +pub use tab::Tab; +pub use tool_divider::tool_divider; +pub use tool_divider::ToolDivider; struct ButtonHandlers { click: Option)>>, diff --git a/crates/storybook/src/components/avatar.rs b/crates/storybook/src/components/avatar.rs index 8eff055d75c0406d73c0ac0cda512c6bcbadd0b2..6e136977c86a0c37f8a5202e00ab98832c635d09 100644 --- a/crates/storybook/src/components/avatar.rs +++ b/crates/storybook/src/components/avatar.rs @@ -8,19 +8,24 @@ use gpui2::{Element, ViewContext}; pub type UnknownString = ArcCow<'static, str>; #[derive(Element)] -pub(crate) struct Avatar { +pub struct Avatar { src: ArcCow<'static, str>, shape: Shape, } -pub fn avatar(src: impl Into>, shape: Shape) -> impl Element { +pub fn avatar(src: impl Into>) -> Avatar { Avatar { src: src.into(), - shape, + shape: Shape::Circle, } } impl Avatar { + pub fn shape(mut self, shape: Shape) -> Self { + self.shape = shape; + self + } + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { let theme = theme(cx); diff --git a/crates/storybook/src/components/icon_button2 b/crates/storybook/src/components/icon_button2 new file mode 100644 index 0000000000000000000000000000000000000000..9fe738d5ac73c539c3c2183f1d7340dd14cc272c --- /dev/null +++ b/crates/storybook/src/components/icon_button2 @@ -0,0 +1,123 @@ +// use crate::prelude::{ButtonVariant, UIState}; +// use crate::theme::theme; +// use gpui2::elements::svg; +// use gpui2::style::{StyleHelpers, Styleable}; +// use gpui2::{elements::div, IntoElement}; +// use gpui2::{Element, ParentElement, ViewContext}; + +// #[derive(Element)] +// pub(crate) struct IconButton { +// path: &'static str, +// variant: ButtonVariant, +// state: UIState, +// } + +// pub fn icon_button(path: &'static str) -> IconButton { +// IconButton { +// path, +// variant: ButtonVariant::Filled, +// state: UIState::Default, +// } +// } + +// impl IconButton { +// fn variant(mut self, variant: ButtonVariant) -> Self { +// self.variant = variant; + +// // Example of more interesting setter behavior +// // FilledButtons must be disabled +// if self.variant == ButtonVariant::Filled { +// self.state = UIState::Disabled; +// } + +// self +// } + +// fn state(mut self, state: UIState) -> Self { +// // Example of more interesting setter behavior: +// // GhostButtons Cannot be disabled +// // Debug asserts are compiled out when we make a new release. +// // Useful for making sure developers develop correctly without breaking +// // everything +// debug_assert!(self.variant != ButtonVariant::Ghost && state != UIState::Disabled); + +// self.state = state; +// self +// } + +// // const state = { +// // foo: "foo", +// // bar: "bar" +// // } as const +// // +// // type State = typeof state[keyof typeof something] +// // +// // type Button { +// // style: State +// // } +// // +// //