Detailed changes
@@ -12,7 +12,7 @@ use chrono::{DateTime, Local};
use collections::HashMap;
use fs::Fs;
use futures::StreamExt;
-use gpui::{actions, AppContext};
+use gpui::{actions, AppContext, SharedString};
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::{cmp::Reverse, ffi::OsStr, path::PathBuf, sync::Arc};
@@ -47,7 +47,7 @@ struct MessageMetadata {
enum MessageStatus {
Pending,
Done,
- Error(Arc<str>),
+ Error(SharedString),
}
#[derive(Serialize, Deserialize)]
@@ -1628,8 +1628,9 @@ impl Conversation {
metadata.status = MessageStatus::Done;
}
Err(error) => {
- metadata.status =
- MessageStatus::Error(error.to_string().trim().into());
+ metadata.status = MessageStatus::Error(SharedString::from(
+ error.to_string().trim().to_string(),
+ ));
}
}
cx.notify();
@@ -2273,7 +2274,7 @@ impl ConversationEditor {
Some(
div()
.id("error")
- .tooltip(move |cx| Tooltip::text(&error, cx))
+ .tooltip(move |cx| Tooltip::text(error.clone(), cx))
.child(IconElement::new(Icon::XCircle)),
)
} else {
@@ -8,7 +8,8 @@ use collections::{hash_map, HashMap, HashSet};
use db::RELEASE_CHANNEL;
use futures::{channel::mpsc, future::Shared, Future, FutureExt, StreamExt};
use gpui::{
- AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Task, WeakModel,
+ AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, SharedString, Task,
+ WeakModel,
};
use rpc::{
proto::{self, ChannelVisibility},
@@ -46,7 +47,7 @@ pub struct ChannelStore {
#[derive(Clone, Debug, PartialEq)]
pub struct Channel {
pub id: ChannelId,
- pub name: String,
+ pub name: SharedString,
pub visibility: proto::ChannelVisibility,
pub role: proto::ChannelRole,
pub unseen_note_version: Option<(u64, clock::Global)>,
@@ -895,14 +896,16 @@ impl ChannelStore {
.channel_invitations
.binary_search_by_key(&channel.id, |c| c.id)
{
- Ok(ix) => Arc::make_mut(&mut self.channel_invitations[ix]).name = channel.name,
+ Ok(ix) => {
+ Arc::make_mut(&mut self.channel_invitations[ix]).name = channel.name.into()
+ }
Err(ix) => self.channel_invitations.insert(
ix,
Arc::new(Channel {
id: channel.id,
visibility: channel.visibility(),
role: channel.role(),
- name: channel.name,
+ name: channel.name.into(),
unseen_note_version: None,
unseen_message_id: None,
parent_path: channel.parent_path,
@@ -104,7 +104,7 @@ impl<'a> ChannelPathsInsertGuard<'a> {
existing_channel.visibility = channel_proto.visibility();
existing_channel.role = channel_proto.role();
- existing_channel.name = channel_proto.name;
+ existing_channel.name = channel_proto.name.into();
} else {
self.channels_by_id.insert(
channel_proto.id,
@@ -112,7 +112,7 @@ impl<'a> ChannelPathsInsertGuard<'a> {
id: channel_proto.id,
visibility: channel_proto.visibility(),
role: channel_proto.role(),
- name: channel_proto.name,
+ name: channel_proto.name.into(),
unseen_note_version: None,
unseen_message_id: None,
parent_path: channel_proto.parent_path,
@@ -146,11 +146,11 @@ fn channel_path_sorting_key<'a>(
let (parent_path, name) = channels_by_id
.get(&id)
.map_or((&[] as &[_], None), |channel| {
- (channel.parent_path.as_slice(), Some(channel.name.as_str()))
+ (channel.parent_path.as_slice(), Some(channel.name.as_ref()))
});
parent_path
.iter()
- .filter_map(|id| Some(channels_by_id.get(id)?.name.as_str()))
+ .filter_map(|id| Some(channels_by_id.get(id)?.name.as_ref()))
.chain(name)
}
@@ -7,7 +7,7 @@ use call::ActiveCall;
use channel::{ChannelId, ChannelMembership, ChannelStore};
use client::User;
use futures::future::try_join_all;
-use gpui::{BackgroundExecutor, Model, TestAppContext};
+use gpui::{BackgroundExecutor, Model, SharedString, TestAppContext};
use rpc::{
proto::{self, ChannelRole},
RECEIVE_TIMEOUT,
@@ -46,13 +46,13 @@ async fn test_core_channels(
&[
ExpectedChannel {
id: channel_a_id,
- name: "channel-a".to_string(),
+ name: "channel-a".into(),
depth: 0,
role: ChannelRole::Admin,
},
ExpectedChannel {
id: channel_b_id,
- name: "channel-b".to_string(),
+ name: "channel-b".into(),
depth: 1,
role: ChannelRole::Admin,
},
@@ -92,7 +92,7 @@ async fn test_core_channels(
cx_b,
&[ExpectedChannel {
id: channel_a_id,
- name: "channel-a".to_string(),
+ name: "channel-a".into(),
depth: 0,
role: ChannelRole::Member,
}],
@@ -140,13 +140,13 @@ async fn test_core_channels(
&[
ExpectedChannel {
id: channel_a_id,
- name: "channel-a".to_string(),
+ name: "channel-a".into(),
role: ChannelRole::Member,
depth: 0,
},
ExpectedChannel {
id: channel_b_id,
- name: "channel-b".to_string(),
+ name: "channel-b".into(),
role: ChannelRole::Member,
depth: 1,
},
@@ -168,19 +168,19 @@ async fn test_core_channels(
&[
ExpectedChannel {
id: channel_a_id,
- name: "channel-a".to_string(),
+ name: "channel-a".into(),
role: ChannelRole::Member,
depth: 0,
},
ExpectedChannel {
id: channel_b_id,
- name: "channel-b".to_string(),
+ name: "channel-b".into(),
role: ChannelRole::Member,
depth: 1,
},
ExpectedChannel {
id: channel_c_id,
- name: "channel-c".to_string(),
+ name: "channel-c".into(),
role: ChannelRole::Member,
depth: 2,
},
@@ -211,19 +211,19 @@ async fn test_core_channels(
&[
ExpectedChannel {
id: channel_a_id,
- name: "channel-a".to_string(),
+ name: "channel-a".into(),
depth: 0,
role: ChannelRole::Admin,
},
ExpectedChannel {
id: channel_b_id,
- name: "channel-b".to_string(),
+ name: "channel-b".into(),
depth: 1,
role: ChannelRole::Admin,
},
ExpectedChannel {
id: channel_c_id,
- name: "channel-c".to_string(),
+ name: "channel-c".into(),
depth: 2,
role: ChannelRole::Admin,
},
@@ -245,7 +245,7 @@ async fn test_core_channels(
cx_a,
&[ExpectedChannel {
id: channel_a_id,
- name: "channel-a".to_string(),
+ name: "channel-a".into(),
depth: 0,
role: ChannelRole::Admin,
}],
@@ -255,7 +255,7 @@ async fn test_core_channels(
cx_b,
&[ExpectedChannel {
id: channel_a_id,
- name: "channel-a".to_string(),
+ name: "channel-a".into(),
depth: 0,
role: ChannelRole::Admin,
}],
@@ -278,7 +278,7 @@ async fn test_core_channels(
cx_a,
&[ExpectedChannel {
id: channel_a_id,
- name: "channel-a".to_string(),
+ name: "channel-a".into(),
depth: 0,
role: ChannelRole::Admin,
}],
@@ -309,7 +309,7 @@ async fn test_core_channels(
cx_a,
&[ExpectedChannel {
id: channel_a_id,
- name: "channel-a-renamed".to_string(),
+ name: "channel-a-renamed".into(),
depth: 0,
role: ChannelRole::Admin,
}],
@@ -418,7 +418,7 @@ async fn test_channel_room(
cx_b,
&[ExpectedChannel {
id: zed_id,
- name: "zed".to_string(),
+ name: "zed".into(),
depth: 0,
role: ChannelRole::Member,
}],
@@ -680,7 +680,7 @@ async fn test_permissions_update_while_invited(
&[ExpectedChannel {
depth: 0,
id: rust_id,
- name: "rust".to_string(),
+ name: "rust".into(),
role: ChannelRole::Member,
}],
);
@@ -708,7 +708,7 @@ async fn test_permissions_update_while_invited(
&[ExpectedChannel {
depth: 0,
id: rust_id,
- name: "rust".to_string(),
+ name: "rust".into(),
role: ChannelRole::Member,
}],
);
@@ -747,7 +747,7 @@ async fn test_channel_rename(
&[ExpectedChannel {
depth: 0,
id: rust_id,
- name: "rust-archive".to_string(),
+ name: "rust-archive".into(),
role: ChannelRole::Admin,
}],
);
@@ -759,7 +759,7 @@ async fn test_channel_rename(
&[ExpectedChannel {
depth: 0,
id: rust_id,
- name: "rust-archive".to_string(),
+ name: "rust-archive".into(),
role: ChannelRole::Member,
}],
);
@@ -888,7 +888,7 @@ async fn test_lost_channel_creation(
&[ExpectedChannel {
depth: 0,
id: channel_id,
- name: "x".to_string(),
+ name: "x".into(),
role: ChannelRole::Member,
}],
);
@@ -912,13 +912,13 @@ async fn test_lost_channel_creation(
ExpectedChannel {
depth: 0,
id: channel_id,
- name: "x".to_string(),
+ name: "x".into(),
role: ChannelRole::Admin,
},
ExpectedChannel {
depth: 1,
id: subchannel_id,
- name: "subchannel".to_string(),
+ name: "subchannel".into(),
role: ChannelRole::Admin,
},
],
@@ -943,13 +943,13 @@ async fn test_lost_channel_creation(
ExpectedChannel {
depth: 0,
id: channel_id,
- name: "x".to_string(),
+ name: "x".into(),
role: ChannelRole::Member,
},
ExpectedChannel {
depth: 1,
id: subchannel_id,
- name: "subchannel".to_string(),
+ name: "subchannel".into(),
role: ChannelRole::Member,
},
],
@@ -1221,13 +1221,13 @@ async fn test_channel_membership_notifications(
ExpectedChannel {
depth: 0,
id: zed_channel,
- name: "zed".to_string(),
+ name: "zed".into(),
role: ChannelRole::Guest,
},
ExpectedChannel {
depth: 1,
id: vim_channel,
- name: "vim".to_string(),
+ name: "vim".into(),
role: ChannelRole::Member,
},
],
@@ -1250,13 +1250,13 @@ async fn test_channel_membership_notifications(
ExpectedChannel {
depth: 0,
id: zed_channel,
- name: "zed".to_string(),
+ name: "zed".into(),
role: ChannelRole::Guest,
},
ExpectedChannel {
depth: 1,
id: vim_channel,
- name: "vim".to_string(),
+ name: "vim".into(),
role: ChannelRole::Guest,
},
],
@@ -1476,7 +1476,7 @@ async fn test_channel_moving(
struct ExpectedChannel {
depth: usize,
id: ChannelId,
- name: String,
+ name: SharedString,
role: ChannelRole,
}
@@ -1515,7 +1515,7 @@ fn assert_channels(
.ordered_channels()
.map(|(depth, channel)| ExpectedChannel {
depth,
- name: channel.name.clone(),
+ name: channel.name.clone().into(),
id: channel.id,
role: channel.role,
})
@@ -3,7 +3,7 @@ use crate::db::ChannelRole;
use super::{run_randomized_test, RandomizedTest, TestClient, TestError, TestServer, UserTestPlan};
use anyhow::Result;
use async_trait::async_trait;
-use gpui::{BackgroundExecutor, TestAppContext};
+use gpui::{BackgroundExecutor, SharedString, TestAppContext};
use rand::prelude::*;
use serde_derive::{Deserialize, Serialize};
use std::{
@@ -30,13 +30,13 @@ struct RandomChannelBufferTest;
#[derive(Clone, Serialize, Deserialize)]
enum ChannelBufferOperation {
JoinChannelNotes {
- channel_name: String,
+ channel_name: SharedString,
},
LeaveChannelNotes {
- channel_name: String,
+ channel_name: SharedString,
},
EditChannelNotes {
- channel_name: String,
+ channel_name: SharedString,
edits: Vec<(Range<usize>, Arc<str>)>,
},
Noop,
@@ -1,983 +1,694 @@
-// use crate::{
-// channel_view::ChannelView, is_channels_feature_enabled, render_avatar, ChatPanelSettings,
-// };
-// use anyhow::Result;
-// use call::ActiveCall;
-// use channel::{ChannelChat, ChannelChatEvent, ChannelMessageId, ChannelStore};
-// use client::Client;
-// use collections::HashMap;
-// use db::kvp::KEY_VALUE_STORE;
-// use editor::Editor;
-// use gpui::{
-// actions,
-// elements::*,
-// platform::{CursorStyle, MouseButton},
-// serde_json,
-// views::{ItemType, Select, SelectStyle},
-// AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelHandle, Subscription, Task, View,
-// ViewContext, ViewHandle, WeakViewHandle,
-// };
-// use language::LanguageRegistry;
-// use menu::Confirm;
-// use message_editor::MessageEditor;
-// use project::Fs;
-// use rich_text::RichText;
-// use serde::{Deserialize, Serialize};
-// use settings::SettingsStore;
-// use std::sync::Arc;
-// use theme::{IconButton, Theme};
-// use time::{OffsetDateTime, UtcOffset};
-// use util::{ResultExt, TryFutureExt};
-// use workspace::{
-// dock::{DockPosition, Panel},
-// Workspace,
-// };
-
-// mod message_editor;
-
-// const MESSAGE_LOADING_THRESHOLD: usize = 50;
-// const CHAT_PANEL_KEY: &'static str = "ChatPanel";
-
-// pub struct ChatPanel {
-// client: Arc<Client>,
-// channel_store: ModelHandle<ChannelStore>,
-// languages: Arc<LanguageRegistry>,
-// active_chat: Option<(ModelHandle<ChannelChat>, Subscription)>,
-// message_list: ListState<ChatPanel>,
-// input_editor: ViewHandle<MessageEditor>,
-// channel_select: ViewHandle<Select>,
-// local_timezone: UtcOffset,
-// fs: Arc<dyn Fs>,
-// width: Option<f32>,
-// active: bool,
-// pending_serialization: Task<Option<()>>,
-// subscriptions: Vec<gpui::Subscription>,
-// workspace: WeakViewHandle<Workspace>,
-// is_scrolled_to_bottom: bool,
-// has_focus: bool,
-// markdown_data: HashMap<ChannelMessageId, RichText>,
-// }
-
-// #[derive(Serialize, Deserialize)]
-// struct SerializedChatPanel {
-// width: Option<f32>,
-// }
-
-// #[derive(Debug)]
-// pub enum Event {
-// DockPositionChanged,
-// Focus,
-// Dismissed,
-// }
-
-// actions!(
-// chat_panel,
-// [LoadMoreMessages, ToggleFocus, OpenChannelNotes, JoinCall]
-// );
-
-// pub fn init(cx: &mut AppContext) {
-// cx.add_action(ChatPanel::send);
-// cx.add_action(ChatPanel::load_more_messages);
-// cx.add_action(ChatPanel::open_notes);
-// cx.add_action(ChatPanel::join_call);
-// }
-
-// impl ChatPanel {
-// pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> ViewHandle<Self> {
-// let fs = workspace.app_state().fs.clone();
-// let client = workspace.app_state().client.clone();
-// let channel_store = ChannelStore::global(cx);
-// let languages = workspace.app_state().languages.clone();
-
-// let input_editor = cx.add_view(|cx| {
-// MessageEditor::new(
-// languages.clone(),
-// channel_store.clone(),
-// cx.add_view(|cx| {
-// Editor::auto_height(
-// 4,
-// Some(Arc::new(|theme| theme.chat_panel.input_editor.clone())),
-// cx,
-// )
-// }),
-// cx,
-// )
-// });
-
-// let workspace_handle = workspace.weak_handle();
-
-// let channel_select = cx.add_view(|cx| {
-// let channel_store = channel_store.clone();
-// let workspace = workspace_handle.clone();
-// Select::new(0, cx, {
-// move |ix, item_type, is_hovered, cx| {
-// Self::render_channel_name(
-// &channel_store,
-// ix,
-// item_type,
-// is_hovered,
-// workspace,
-// cx,
-// )
-// }
-// })
-// .with_style(move |cx| {
-// let style = &theme::current(cx).chat_panel.channel_select;
-// SelectStyle {
-// header: Default::default(),
-// menu: style.menu,
-// }
-// })
-// });
-
-// let mut message_list =
-// ListState::<Self>::new(0, Orientation::Bottom, 10., move |this, ix, cx| {
-// this.render_message(ix, cx)
-// });
-// message_list.set_scroll_handler(|visible_range, count, this, cx| {
-// if visible_range.start < MESSAGE_LOADING_THRESHOLD {
-// this.load_more_messages(&LoadMoreMessages, cx);
-// }
-// this.is_scrolled_to_bottom = visible_range.end == count;
-// });
-
-// cx.add_view(|cx| {
-// let mut this = Self {
-// fs,
-// client,
-// channel_store,
-// languages,
-// active_chat: Default::default(),
-// pending_serialization: Task::ready(None),
-// message_list,
-// input_editor,
-// channel_select,
-// local_timezone: cx.platform().local_timezone(),
-// has_focus: false,
-// subscriptions: Vec::new(),
-// workspace: workspace_handle,
-// is_scrolled_to_bottom: true,
-// active: false,
-// width: None,
-// markdown_data: Default::default(),
-// };
-
-// let mut old_dock_position = this.position(cx);
-// this.subscriptions
-// .push(
-// cx.observe_global::<SettingsStore, _>(move |this: &mut Self, cx| {
-// let new_dock_position = this.position(cx);
-// if new_dock_position != old_dock_position {
-// old_dock_position = new_dock_position;
-// cx.emit(Event::DockPositionChanged);
-// }
-// cx.notify();
-// }),
-// );
-
-// this.update_channel_count(cx);
-// cx.observe(&this.channel_store, |this, _, cx| {
-// this.update_channel_count(cx)
-// })
-// .detach();
-
-// 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)
-// .channel_at(selected_ix)
-// .map(|e| e.id);
-// if let Some(selected_channel_id) = selected_channel_id {
-// this.select_channel(selected_channel_id, None, cx)
-// .detach_and_log_err(cx);
-// }
-// })
-// .detach();
-
-// this
-// })
-// }
-
-// pub fn is_scrolled_to_bottom(&self) -> bool {
-// self.is_scrolled_to_bottom
-// }
-
-// pub fn active_chat(&self) -> Option<ModelHandle<ChannelChat>> {
-// self.active_chat.as_ref().map(|(chat, _)| chat.clone())
-// }
-
-// pub fn load(
-// workspace: WeakViewHandle<Workspace>,
-// cx: AsyncAppContext,
-// ) -> Task<Result<ViewHandle<Self>>> {
-// cx.spawn(|mut cx| async move {
-// let serialized_panel = if let Some(panel) = cx
-// .background()
-// .spawn(async move { KEY_VALUE_STORE.read_kvp(CHAT_PANEL_KEY) })
-// .await
-// .log_err()
-// .flatten()
-// {
-// Some(serde_json::from_str::<SerializedChatPanel>(&panel)?)
-// } else {
-// None
-// };
-
-// workspace.update(&mut cx, |workspace, cx| {
-// let panel = Self::new(workspace, cx);
-// if let Some(serialized_panel) = serialized_panel {
-// panel.update(cx, |panel, cx| {
-// panel.width = serialized_panel.width;
-// cx.notify();
-// });
-// }
-// panel
-// })
-// })
-// }
-
-// fn serialize(&mut self, cx: &mut ViewContext<Self>) {
-// let width = self.width;
-// self.pending_serialization = cx.background().spawn(
-// async move {
-// KEY_VALUE_STORE
-// .write_kvp(
-// CHAT_PANEL_KEY.into(),
-// serde_json::to_string(&SerializedChatPanel { width })?,
-// )
-// .await?;
-// anyhow::Ok(())
-// }
-// .log_err(),
-// );
-// }
-
-// fn update_channel_count(&mut self, cx: &mut ViewContext<Self>) {
-// let channel_count = self.channel_store.read(cx).channel_count();
-// self.channel_select.update(cx, |select, cx| {
-// select.set_item_count(channel_count, cx);
-// });
-// }
-
-// fn set_active_chat(&mut self, chat: ModelHandle<ChannelChat>, cx: &mut ViewContext<Self>) {
-// if self.active_chat.as_ref().map(|e| &e.0) != Some(&chat) {
-// let channel_id = chat.read(cx).channel_id;
-// {
-// self.markdown_data.clear();
-// let chat = chat.read(cx);
-// self.message_list.reset(chat.message_count());
-
-// let channel_name = chat.channel(cx).map(|channel| channel.name.clone());
-// self.input_editor.update(cx, |editor, cx| {
-// editor.set_channel(channel_id, channel_name, cx);
-// });
-// };
-// let subscription = cx.subscribe(&chat, Self::channel_did_change);
-// self.active_chat = Some((chat, subscription));
-// self.acknowledge_last_message(cx);
-// self.channel_select.update(cx, |select, cx| {
-// if let Some(ix) = self.channel_store.read(cx).index_of_channel(channel_id) {
-// select.set_selected_index(ix, cx);
-// }
-// });
-// cx.notify();
-// }
-// }
-
-// fn channel_did_change(
-// &mut self,
-// _: ModelHandle<ChannelChat>,
-// event: &ChannelChatEvent,
-// cx: &mut ViewContext<Self>,
-// ) {
-// match event {
-// ChannelChatEvent::MessagesUpdated {
-// old_range,
-// new_count,
-// } => {
-// self.message_list.splice(old_range.clone(), *new_count);
-// if self.active {
-// self.acknowledge_last_message(cx);
-// }
-// }
-// ChannelChatEvent::NewMessage {
-// channel_id,
-// message_id,
-// } => {
-// if !self.active {
-// self.channel_store.update(cx, |store, cx| {
-// store.new_message(*channel_id, *message_id, cx)
-// })
-// }
-// }
-// }
-// cx.notify();
-// }
-
-// fn acknowledge_last_message(&mut self, cx: &mut ViewContext<'_, '_, ChatPanel>) {
-// if self.active && self.is_scrolled_to_bottom {
-// if let Some((chat, _)) = &self.active_chat {
-// chat.update(cx, |chat, cx| {
-// chat.acknowledge_last_message(cx);
-// });
-// }
-// }
-// }
-
-// fn render_channel(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-// let theme = theme::current(cx);
-// Flex::column()
-// .with_child(
-// ChildView::new(&self.channel_select, cx)
-// .contained()
-// .with_style(theme.chat_panel.channel_select.container),
-// )
-// .with_child(self.render_active_channel_messages(&theme))
-// .with_child(self.render_input_box(&theme, cx))
-// .into_any()
-// }
-
-// fn render_active_channel_messages(&self, theme: &Arc<Theme>) -> AnyElement<Self> {
-// let messages = if self.active_chat.is_some() {
-// List::new(self.message_list.clone())
-// .contained()
-// .with_style(theme.chat_panel.list)
-// .into_any()
-// } else {
-// Empty::new().into_any()
-// };
-
-// messages.flex(1., true).into_any()
-// }
-
-// fn render_message(&mut self, ix: usize, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-// let (message, is_continuation, is_last, is_admin) = self
-// .active_chat
-// .as_ref()
-// .unwrap()
-// .0
-// .update(cx, |active_chat, cx| {
-// let is_admin = self
-// .channel_store
-// .read(cx)
-// .is_channel_admin(active_chat.channel_id);
-
-// let last_message = active_chat.message(ix.saturating_sub(1));
-// let this_message = active_chat.message(ix).clone();
-// let is_continuation = last_message.id != this_message.id
-// && this_message.sender.id == last_message.sender.id;
-
-// if let ChannelMessageId::Saved(id) = this_message.id {
-// if this_message
-// .mentions
-// .iter()
-// .any(|(_, user_id)| Some(*user_id) == self.client.user_id())
-// {
-// active_chat.acknowledge_message(id);
-// }
-// }
-
-// (
-// this_message,
-// is_continuation,
-// active_chat.message_count() == ix + 1,
-// is_admin,
-// )
-// });
-
-// let is_pending = message.is_pending();
-// let theme = theme::current(cx);
-// let text = self.markdown_data.entry(message.id).or_insert_with(|| {
-// Self::render_markdown_with_mentions(&self.languages, self.client.id(), &message)
-// });
-
-// let now = OffsetDateTime::now_utc();
-
-// let style = if is_pending {
-// &theme.chat_panel.pending_message
-// } else if is_continuation {
-// &theme.chat_panel.continuation_message
-// } else {
-// &theme.chat_panel.message
-// };
-
-// let belongs_to_user = Some(message.sender.id) == self.client.user_id();
-// let message_id_to_remove = if let (ChannelMessageId::Saved(id), true) =
-// (message.id, belongs_to_user || is_admin)
-// {
-// Some(id)
-// } else {
-// None
-// };
-
-// enum MessageBackgroundHighlight {}
-// MouseEventHandler::new::<MessageBackgroundHighlight, _>(ix, cx, |state, cx| {
-// let container = style.style_for(state);
-// if is_continuation {
-// Flex::row()
-// .with_child(
-// text.element(
-// theme.editor.syntax.clone(),
-// theme.chat_panel.rich_text.clone(),
-// cx,
-// )
-// .flex(1., true),
-// )
-// .with_child(render_remove(message_id_to_remove, cx, &theme))
-// .contained()
-// .with_style(*container)
-// .with_margin_bottom(if is_last {
-// theme.chat_panel.last_message_bottom_spacing
-// } else {
-// 0.
-// })
-// .into_any()
-// } else {
-// Flex::column()
-// .with_child(
-// Flex::row()
-// .with_child(
-// Flex::row()
-// .with_child(render_avatar(
-// message.sender.avatar.clone(),
-// &theme.chat_panel.avatar,
-// theme.chat_panel.avatar_container,
-// ))
-// .with_child(
-// Label::new(
-// message.sender.github_login.clone(),
-// theme.chat_panel.message_sender.text.clone(),
-// )
-// .contained()
-// .with_style(theme.chat_panel.message_sender.container),
-// )
-// .with_child(
-// Label::new(
-// format_timestamp(
-// message.timestamp,
-// now,
-// self.local_timezone,
-// ),
-// theme.chat_panel.message_timestamp.text.clone(),
-// )
-// .contained()
-// .with_style(theme.chat_panel.message_timestamp.container),
-// )
-// .align_children_center()
-// .flex(1., true),
-// )
-// .with_child(render_remove(message_id_to_remove, cx, &theme))
-// .align_children_center(),
-// )
-// .with_child(
-// Flex::row()
-// .with_child(
-// text.element(
-// theme.editor.syntax.clone(),
-// theme.chat_panel.rich_text.clone(),
-// cx,
-// )
-// .flex(1., true),
-// )
-// // Add a spacer to make everything line up
-// .with_child(render_remove(None, cx, &theme)),
-// )
-// .contained()
-// .with_style(*container)
-// .with_margin_bottom(if is_last {
-// theme.chat_panel.last_message_bottom_spacing
-// } else {
-// 0.
-// })
-// .into_any()
-// }
-// })
-// .into_any()
-// }
-
-// fn render_markdown_with_mentions(
-// language_registry: &Arc<LanguageRegistry>,
-// current_user_id: u64,
-// message: &channel::ChannelMessage,
-// ) -> RichText {
-// let mentions = message
-// .mentions
-// .iter()
-// .map(|(range, user_id)| rich_text::Mention {
-// range: range.clone(),
-// is_self_mention: *user_id == current_user_id,
-// })
-// .collect::<Vec<_>>();
-
-// rich_text::render_markdown(message.body.clone(), &mentions, language_registry, None)
-// }
-
-// fn render_input_box(&self, theme: &Arc<Theme>, cx: &AppContext) -> AnyElement<Self> {
-// ChildView::new(&self.input_editor, cx)
-// .contained()
-// .with_style(theme.chat_panel.input_editor.container)
-// .into_any()
-// }
-
-// fn render_channel_name(
-// channel_store: &ModelHandle<ChannelStore>,
-// ix: usize,
-// item_type: ItemType,
-// is_hovered: bool,
-// workspace: WeakViewHandle<Workspace>,
-// cx: &mut ViewContext<Select>,
-// ) -> AnyElement<Select> {
-// let theme = theme::current(cx);
-// let tooltip_style = &theme.tooltip;
-// let theme = &theme.chat_panel;
-// let style = match (&item_type, is_hovered) {
-// (ItemType::Header, _) => &theme.channel_select.header,
-// (ItemType::Selected, _) => &theme.channel_select.active_item,
-// (ItemType::Unselected, false) => &theme.channel_select.item,
-// (ItemType::Unselected, true) => &theme.channel_select.hovered_item,
-// };
-
-// let channel = &channel_store.read(cx).channel_at(ix).unwrap();
-// let channel_id = channel.id;
-
-// let mut row = Flex::row()
-// .with_child(
-// Label::new("#".to_string(), style.hash.text.clone())
-// .contained()
-// .with_style(style.hash.container),
-// )
-// .with_child(Label::new(channel.name.clone(), style.name.clone()));
-
-// if matches!(item_type, ItemType::Header) {
-// row.add_children([
-// MouseEventHandler::new::<OpenChannelNotes, _>(0, cx, |mouse_state, _| {
-// render_icon_button(theme.icon_button.style_for(mouse_state), "icons/file.svg")
-// })
-// .on_click(MouseButton::Left, move |_, _, cx| {
-// if let Some(workspace) = workspace.upgrade(cx) {
-// ChannelView::open(channel_id, workspace, cx).detach();
-// }
-// })
-// .with_tooltip::<OpenChannelNotes>(
-// channel_id as usize,
-// "Open Notes",
-// Some(Box::new(OpenChannelNotes)),
-// tooltip_style.clone(),
-// cx,
-// )
-// .flex_float(),
-// MouseEventHandler::new::<ActiveCall, _>(0, cx, |mouse_state, _| {
-// render_icon_button(
-// theme.icon_button.style_for(mouse_state),
-// "icons/speaker-loud.svg",
-// )
-// })
-// .on_click(MouseButton::Left, move |_, _, cx| {
-// ActiveCall::global(cx)
-// .update(cx, |call, cx| call.join_channel(channel_id, cx))
-// .detach_and_log_err(cx);
-// })
-// .with_tooltip::<ActiveCall>(
-// channel_id as usize,
-// "Join Call",
-// Some(Box::new(JoinCall)),
-// tooltip_style.clone(),
-// cx,
-// )
-// .flex_float(),
-// ]);
-// }
-
-// row.align_children_center()
-// .contained()
-// .with_style(style.container)
-// .into_any()
-// }
-
-// fn render_sign_in_prompt(
-// &self,
-// theme: &Arc<Theme>,
-// cx: &mut ViewContext<Self>,
-// ) -> AnyElement<Self> {
-// enum SignInPromptLabel {}
-
-// MouseEventHandler::new::<SignInPromptLabel, _>(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<Self>) {
-// if let Some((chat, _)) = self.active_chat.as_ref() {
-// let message = self
-// .input_editor
-// .update(cx, |editor, cx| editor.take_message(cx));
-
-// if let Some(task) = chat
-// .update(cx, |chat, cx| chat.send_message(message, cx))
-// .log_err()
-// {
-// task.detach();
-// }
-// }
-// }
-
-// fn remove_message(&mut self, id: u64, cx: &mut ViewContext<Self>) {
-// if let Some((chat, _)) = self.active_chat.as_ref() {
-// chat.update(cx, |chat, cx| chat.remove_message(id, cx).detach())
-// }
-// }
-
-// fn load_more_messages(&mut self, _: &LoadMoreMessages, cx: &mut ViewContext<Self>) {
-// if let Some((chat, _)) = self.active_chat.as_ref() {
-// chat.update(cx, |channel, cx| {
-// if let Some(task) = channel.load_more_messages(cx) {
-// task.detach();
-// }
-// })
-// }
-// }
-
-// pub fn select_channel(
-// &mut self,
-// selected_channel_id: u64,
-// scroll_to_message_id: Option<u64>,
-// cx: &mut ViewContext<ChatPanel>,
-// ) -> Task<Result<()>> {
-// let open_chat = self
-// .active_chat
-// .as_ref()
-// .and_then(|(chat, _)| {
-// (chat.read(cx).channel_id == selected_channel_id)
-// .then(|| Task::ready(anyhow::Ok(chat.clone())))
-// })
-// .unwrap_or_else(|| {
-// 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_chat(chat.clone(), cx);
-// })?;
-
-// if let Some(message_id) = scroll_to_message_id {
-// if let Some(item_ix) =
-// ChannelChat::load_history_since_message(chat.clone(), message_id, cx.clone())
-// .await
-// {
-// this.update(&mut cx, |this, cx| {
-// if this.active_chat.as_ref().map_or(false, |(c, _)| *c == chat) {
-// this.message_list.scroll_to(ListOffset {
-// item_ix,
-// offset_in_item: 0.,
-// });
-// cx.notify();
-// }
-// })?;
-// }
-// }
-
-// Ok(())
-// })
-// }
-
-// fn open_notes(&mut self, _: &OpenChannelNotes, cx: &mut ViewContext<Self>) {
-// if let Some((chat, _)) = &self.active_chat {
-// let channel_id = chat.read(cx).channel_id;
-// if let Some(workspace) = self.workspace.upgrade(cx) {
-// ChannelView::open(channel_id, workspace, cx).detach();
-// }
-// }
-// }
-
-// fn join_call(&mut self, _: &JoinCall, cx: &mut ViewContext<Self>) {
-// if let Some((chat, _)) = &self.active_chat {
-// let channel_id = chat.read(cx).channel_id;
-// ActiveCall::global(cx)
-// .update(cx, |call, cx| call.join_channel(channel_id, cx))
-// .detach_and_log_err(cx);
-// }
-// }
-// }
-
-// fn render_remove(
-// message_id_to_remove: Option<u64>,
-// cx: &mut ViewContext<'_, '_, ChatPanel>,
-// theme: &Arc<Theme>,
-// ) -> AnyElement<ChatPanel> {
-// enum DeleteMessage {}
-
-// message_id_to_remove
-// .map(|id| {
-// MouseEventHandler::new::<DeleteMessage, _>(id as usize, cx, |mouse_state, _| {
-// let button_style = theme.chat_panel.icon_button.style_for(mouse_state);
-// render_icon_button(button_style, "icons/x.svg")
-// .aligned()
-// .into_any()
-// })
-// .with_padding(Padding::uniform(2.))
-// .with_cursor_style(CursorStyle::PointingHand)
-// .on_click(MouseButton::Left, move |_, this, cx| {
-// this.remove_message(id, cx);
-// })
-// .flex_float()
-// .into_any()
-// })
-// .unwrap_or_else(|| {
-// let style = theme.chat_panel.icon_button.default;
-
-// Empty::new()
-// .constrained()
-// .with_width(style.icon_width)
-// .aligned()
-// .constrained()
-// .with_width(style.button_width)
-// .with_height(style.button_width)
-// .contained()
-// .with_uniform_padding(2.)
-// .flex_float()
-// .into_any()
-// })
-// }
-
-// impl Entity for ChatPanel {
-// type Event = Event;
-// }
-
-// impl View for ChatPanel {
-// fn ui_name() -> &'static str {
-// "ChatPanel"
-// }
-
-// fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-// 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<Self>) {
-// self.has_focus = true;
-// if matches!(
-// *self.client.status().borrow(),
-// client::Status::Connected { .. }
-// ) {
-// let editor = self.input_editor.read(cx).editor.clone();
-// cx.focus(&editor);
-// }
-// }
-
-// fn focus_out(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {
-// self.has_focus = false;
-// }
-// }
-
-// impl Panel for ChatPanel {
-// fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
-// settings::get::<ChatPanelSettings>(cx).dock
-// }
-
-// fn position_is_valid(&self, position: DockPosition) -> bool {
-// matches!(position, DockPosition::Left | DockPosition::Right)
-// }
-
-// fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
-// settings::update_settings_file::<ChatPanelSettings>(self.fs.clone(), cx, move |settings| {
-// settings.dock = Some(position)
-// });
-// }
-
-// fn size(&self, cx: &gpui::WindowContext) -> f32 {
-// self.width
-// .unwrap_or_else(|| settings::get::<ChatPanelSettings>(cx).default_width)
-// }
-
-// fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>) {
-// self.width = size;
-// self.serialize(cx);
-// cx.notify();
-// }
-
-// fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
-// self.active = active;
-// if active {
-// self.acknowledge_last_message(cx);
-// if !is_channels_feature_enabled(cx) {
-// cx.emit(Event::Dismissed);
-// }
-// }
-// }
-
-// fn icon_path(&self, cx: &gpui::WindowContext) -> Option<&'static str> {
-// (settings::get::<ChatPanelSettings>(cx).button && is_channels_feature_enabled(cx))
-// .then(|| "icons/conversations.svg")
-// }
-
-// fn icon_tooltip(&self) -> (String, Option<Box<dyn gpui::Action>>) {
-// ("Chat Panel".to_string(), Some(Box::new(ToggleFocus)))
-// }
-
-// fn should_change_position_on_event(event: &Self::Event) -> bool {
-// matches!(event, Event::DockPositionChanged)
-// }
-
-// fn should_close_on_event(event: &Self::Event) -> bool {
-// matches!(event, Event::Dismissed)
-// }
-
-// fn has_focus(&self, _cx: &gpui::WindowContext) -> bool {
-// self.has_focus
-// }
-
-// fn is_focus_event(event: &Self::Event) -> bool {
-// matches!(event, Event::Focus)
-// }
-// }
-
-// 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())
-// }
-// }
-
-// fn render_icon_button<V: View>(style: &IconButton, svg_path: &'static str) -> impl Element<V> {
-// Svg::new(svg_path)
-// .with_color(style.color)
-// .constrained()
-// .with_width(style.icon_width)
-// .aligned()
-// .constrained()
-// .with_width(style.button_width)
-// .with_height(style.button_width)
-// .contained()
-// .with_style(style.container)
-// }
-
-// #[cfg(test)]
-// mod tests {
-// use super::*;
-// use gpui::fonts::HighlightStyle;
-// use pretty_assertions::assert_eq;
-// use rich_text::{BackgroundKind, Highlight, RenderedRegion};
-// use util::test::marked_text_ranges;
-
-// #[gpui::test]
-// fn test_render_markdown_with_mentions() {
-// let language_registry = Arc::new(LanguageRegistry::test());
-// let (body, ranges) = marked_text_ranges("*hi*, ยซ@abcยป, let's **call** ยซ@fghยป", false);
-// let message = channel::ChannelMessage {
-// id: ChannelMessageId::Saved(0),
-// body,
-// timestamp: OffsetDateTime::now_utc(),
-// sender: Arc::new(client::User {
-// github_login: "fgh".into(),
-// avatar: None,
-// id: 103,
-// }),
-// nonce: 5,
-// mentions: vec![(ranges[0].clone(), 101), (ranges[1].clone(), 102)],
-// };
-
-// let message = ChatPanel::render_markdown_with_mentions(&language_registry, 102, &message);
-
-// // Note that the "'" was replaced with โ due to smart punctuation.
-// let (body, ranges) = marked_text_ranges("ยซhiยป, ยซ@abcยป, letโs ยซcallยป ยซ@fghยป", false);
-// assert_eq!(message.text, body);
-// assert_eq!(
-// message.highlights,
-// vec![
-// (
-// ranges[0].clone(),
-// HighlightStyle {
-// italic: Some(true),
-// ..Default::default()
-// }
-// .into()
-// ),
-// (ranges[1].clone(), Highlight::Mention),
-// (
-// ranges[2].clone(),
-// HighlightStyle {
-// weight: Some(gpui::fonts::Weight::BOLD),
-// ..Default::default()
-// }
-// .into()
-// ),
-// (ranges[3].clone(), Highlight::SelfMention)
-// ]
-// );
-// assert_eq!(
-// message.regions,
-// vec![
-// RenderedRegion {
-// background_kind: Some(BackgroundKind::Mention),
-// link_url: None
-// },
-// RenderedRegion {
-// background_kind: Some(BackgroundKind::SelfMention),
-// link_url: None
-// },
-// ]
-// );
-// }
-// }
+use crate::{channel_view::ChannelView, is_channels_feature_enabled, ChatPanelSettings};
+use anyhow::Result;
+use call::ActiveCall;
+use channel::{ChannelChat, ChannelChatEvent, ChannelMessageId, ChannelStore};
+use client::Client;
+use collections::HashMap;
+use db::kvp::KEY_VALUE_STORE;
+use editor::Editor;
+use gpui::{
+ actions, div, list, prelude::*, px, serde_json, AnyElement, AppContext, AsyncWindowContext,
+ ClickEvent, Div, EventEmitter, FocusableView, ListOffset, ListScrollEvent, ListState, Model,
+ Render, SharedString, Subscription, Task, View, ViewContext, VisualContext, WeakView,
+};
+use language::LanguageRegistry;
+use menu::Confirm;
+use message_editor::MessageEditor;
+use project::Fs;
+use rich_text::RichText;
+use serde::{Deserialize, Serialize};
+use settings::{Settings, SettingsStore};
+use std::sync::Arc;
+use theme::ActiveTheme as _;
+use time::{OffsetDateTime, UtcOffset};
+use ui::{
+ h_stack, prelude::WindowContext, v_stack, Avatar, Button, ButtonCommon as _, Clickable, Icon,
+ IconButton, Label, Tooltip,
+};
+use util::{ResultExt, TryFutureExt};
+use workspace::{
+ dock::{DockPosition, Panel, PanelEvent},
+ Workspace,
+};
+
+mod message_editor;
+
+const MESSAGE_LOADING_THRESHOLD: usize = 50;
+const CHAT_PANEL_KEY: &'static str = "ChatPanel";
+
+pub struct ChatPanel {
+ client: Arc<Client>,
+ channel_store: Model<ChannelStore>,
+ languages: Arc<LanguageRegistry>,
+ message_list: ListState,
+ active_chat: Option<(Model<ChannelChat>, Subscription)>,
+ input_editor: View<MessageEditor>,
+ local_timezone: UtcOffset,
+ fs: Arc<dyn Fs>,
+ width: Option<f32>,
+ active: bool,
+ pending_serialization: Task<Option<()>>,
+ subscriptions: Vec<gpui::Subscription>,
+ workspace: WeakView<Workspace>,
+ is_scrolled_to_bottom: bool,
+ markdown_data: HashMap<ChannelMessageId, RichText>,
+}
+
+#[derive(Serialize, Deserialize)]
+struct SerializedChatPanel {
+ width: Option<f32>,
+}
+
+#[derive(Debug)]
+pub enum Event {
+ DockPositionChanged,
+ Focus,
+ Dismissed,
+}
+
+actions!(ToggleFocus);
+
+impl ChatPanel {
+ pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> View<Self> {
+ let fs = workspace.app_state().fs.clone();
+ let client = workspace.app_state().client.clone();
+ let channel_store = ChannelStore::global(cx);
+ let languages = workspace.app_state().languages.clone();
+
+ let input_editor = cx.build_view(|cx| {
+ MessageEditor::new(
+ languages.clone(),
+ channel_store.clone(),
+ cx.build_view(|cx| Editor::auto_height(4, cx)),
+ cx,
+ )
+ });
+
+ let workspace_handle = workspace.weak_handle();
+
+ cx.build_view(|cx| {
+ let view: View<ChatPanel> = cx.view().clone();
+ let message_list =
+ ListState::new(0, gpui::ListAlignment::Bottom, px(1000.), move |ix, cx| {
+ view.update(cx, |view, cx| view.render_message(ix, cx))
+ });
+
+ message_list.set_scroll_handler(cx.listener(|this, event: &ListScrollEvent, cx| {
+ if event.visible_range.start < MESSAGE_LOADING_THRESHOLD {
+ this.load_more_messages(cx);
+ }
+ this.is_scrolled_to_bottom = event.visible_range.end == event.count;
+ }));
+
+ let mut this = Self {
+ fs,
+ client,
+ channel_store,
+ languages,
+ message_list,
+ active_chat: Default::default(),
+ pending_serialization: Task::ready(None),
+ input_editor,
+ local_timezone: cx.local_timezone(),
+ subscriptions: Vec::new(),
+ workspace: workspace_handle,
+ is_scrolled_to_bottom: true,
+ active: false,
+ width: None,
+ markdown_data: Default::default(),
+ };
+
+ let mut old_dock_position = this.position(cx);
+ this.subscriptions.push(cx.observe_global::<SettingsStore>(
+ move |this: &mut Self, cx| {
+ let new_dock_position = this.position(cx);
+ if new_dock_position != old_dock_position {
+ old_dock_position = new_dock_position;
+ cx.emit(Event::DockPositionChanged);
+ }
+ cx.notify();
+ },
+ ));
+
+ this
+ })
+ }
+
+ pub fn is_scrolled_to_bottom(&self) -> bool {
+ self.is_scrolled_to_bottom
+ }
+
+ pub fn active_chat(&self) -> Option<Model<ChannelChat>> {
+ self.active_chat.as_ref().map(|(chat, _)| chat.clone())
+ }
+
+ pub fn load(
+ workspace: WeakView<Workspace>,
+ cx: AsyncWindowContext,
+ ) -> Task<Result<View<Self>>> {
+ cx.spawn(|mut cx| async move {
+ let serialized_panel = if let Some(panel) = cx
+ .background_executor()
+ .spawn(async move { KEY_VALUE_STORE.read_kvp(CHAT_PANEL_KEY) })
+ .await
+ .log_err()
+ .flatten()
+ {
+ Some(serde_json::from_str::<SerializedChatPanel>(&panel)?)
+ } else {
+ None
+ };
+
+ workspace.update(&mut cx, |workspace, cx| {
+ let panel = Self::new(workspace, cx);
+ if let Some(serialized_panel) = serialized_panel {
+ panel.update(cx, |panel, cx| {
+ panel.width = serialized_panel.width;
+ cx.notify();
+ });
+ }
+ panel
+ })
+ })
+ }
+
+ fn serialize(&mut self, cx: &mut ViewContext<Self>) {
+ let width = self.width;
+ self.pending_serialization = cx.background_executor().spawn(
+ async move {
+ KEY_VALUE_STORE
+ .write_kvp(
+ CHAT_PANEL_KEY.into(),
+ serde_json::to_string(&SerializedChatPanel { width })?,
+ )
+ .await?;
+ anyhow::Ok(())
+ }
+ .log_err(),
+ );
+ }
+
+ fn set_active_chat(&mut self, chat: Model<ChannelChat>, cx: &mut ViewContext<Self>) {
+ if self.active_chat.as_ref().map(|e| &e.0) != Some(&chat) {
+ let channel_id = chat.read(cx).channel_id;
+ {
+ self.markdown_data.clear();
+ let chat = chat.read(cx);
+ self.message_list.reset(chat.message_count());
+
+ let channel_name = chat.channel(cx).map(|channel| channel.name.clone());
+ self.input_editor.update(cx, |editor, cx| {
+ editor.set_channel(channel_id, channel_name, cx);
+ });
+ };
+ let subscription = cx.subscribe(&chat, Self::channel_did_change);
+ self.active_chat = Some((chat, subscription));
+ self.acknowledge_last_message(cx);
+ cx.notify();
+ }
+ }
+
+ fn channel_did_change(
+ &mut self,
+ _: Model<ChannelChat>,
+ event: &ChannelChatEvent,
+ cx: &mut ViewContext<Self>,
+ ) {
+ match event {
+ ChannelChatEvent::MessagesUpdated {
+ old_range,
+ new_count,
+ } => {
+ self.message_list.splice(old_range.clone(), *new_count);
+ if self.active {
+ self.acknowledge_last_message(cx);
+ }
+ }
+ ChannelChatEvent::NewMessage {
+ channel_id,
+ message_id,
+ } => {
+ if !self.active {
+ self.channel_store.update(cx, |store, cx| {
+ store.new_message(*channel_id, *message_id, cx)
+ })
+ }
+ }
+ }
+ cx.notify();
+ }
+
+ fn acknowledge_last_message(&mut self, cx: &mut ViewContext<Self>) {
+ if self.active && self.is_scrolled_to_bottom {
+ if let Some((chat, _)) = &self.active_chat {
+ chat.update(cx, |chat, cx| {
+ chat.acknowledge_last_message(cx);
+ });
+ }
+ }
+ }
+
+ fn render_channel(&self, cx: &mut ViewContext<Self>) -> AnyElement {
+ v_stack()
+ .full()
+ .on_action(cx.listener(Self::send))
+ .child(
+ h_stack()
+ .w_full()
+ .h_7()
+ .justify_between()
+ .z_index(1)
+ .bg(cx.theme().colors().background)
+ .border()
+ .border_color(gpui::red())
+ .child(Label::new(
+ self.active_chat
+ .as_ref()
+ .and_then(|c| Some(c.0.read(cx).channel(cx)?.name.clone()))
+ .unwrap_or_default(),
+ ))
+ .child(
+ h_stack()
+ .child(
+ IconButton::new("notes", Icon::File)
+ .on_click(cx.listener(Self::open_notes))
+ .tooltip(|cx| Tooltip::text("Open notes", cx)),
+ )
+ .child(
+ IconButton::new("call", Icon::AudioOn)
+ .on_click(cx.listener(Self::join_call))
+ .tooltip(|cx| Tooltip::text("Join call", cx)),
+ ),
+ ),
+ )
+ .child(div().grow().child(self.render_active_channel_messages(cx)))
+ .child(
+ div()
+ .z_index(1)
+ .p_2()
+ .bg(cx.theme().colors().background)
+ .child(self.input_editor.clone()),
+ )
+ .into_any()
+ }
+
+ fn render_active_channel_messages(&self, _cx: &mut ViewContext<Self>) -> AnyElement {
+ if self.active_chat.is_some() {
+ list(self.message_list.clone()).full().into_any_element()
+ } else {
+ div().into_any_element()
+ }
+ }
+
+ fn render_message(&mut self, ix: usize, cx: &mut ViewContext<Self>) -> AnyElement {
+ let active_chat = &self.active_chat.as_ref().unwrap().0;
+ let (message, is_continuation, is_admin) = active_chat.update(cx, |active_chat, cx| {
+ let is_admin = self
+ .channel_store
+ .read(cx)
+ .is_channel_admin(active_chat.channel_id);
+
+ let last_message = active_chat.message(ix.saturating_sub(1));
+ let this_message = active_chat.message(ix).clone();
+ let is_continuation = last_message.id != this_message.id
+ && this_message.sender.id == last_message.sender.id;
+
+ if let ChannelMessageId::Saved(id) = this_message.id {
+ if this_message
+ .mentions
+ .iter()
+ .any(|(_, user_id)| Some(*user_id) == self.client.user_id())
+ {
+ active_chat.acknowledge_message(id);
+ }
+ }
+
+ (this_message, is_continuation, is_admin)
+ });
+
+ let _is_pending = message.is_pending();
+ let text = self.markdown_data.entry(message.id).or_insert_with(|| {
+ Self::render_markdown_with_mentions(&self.languages, self.client.id(), &message)
+ });
+
+ let now = OffsetDateTime::now_utc();
+
+ let belongs_to_user = Some(message.sender.id) == self.client.user_id();
+ let message_id_to_remove = if let (ChannelMessageId::Saved(id), true) =
+ (message.id, belongs_to_user || is_admin)
+ {
+ Some(id)
+ } else {
+ None
+ };
+
+ // todo!("render the text with markdown formatting")
+ if is_continuation {
+ h_stack()
+ .child(SharedString::from(text.text.clone()))
+ .child(render_remove(message_id_to_remove, cx))
+ .mb_1()
+ .into_any()
+ } else {
+ v_stack()
+ .child(
+ h_stack()
+ .children(
+ message
+ .sender
+ .avatar
+ .clone()
+ .map(|avatar| Avatar::data(avatar)),
+ )
+ .child(Label::new(message.sender.github_login.clone()))
+ .child(Label::new(format_timestamp(
+ message.timestamp,
+ now,
+ self.local_timezone,
+ )))
+ .child(render_remove(message_id_to_remove, cx)),
+ )
+ .child(
+ h_stack()
+ .child(SharedString::from(text.text.clone()))
+ .child(render_remove(None, cx)),
+ )
+ .mb_1()
+ .into_any()
+ }
+ }
+
+ fn render_markdown_with_mentions(
+ language_registry: &Arc<LanguageRegistry>,
+ current_user_id: u64,
+ message: &channel::ChannelMessage,
+ ) -> RichText {
+ let mentions = message
+ .mentions
+ .iter()
+ .map(|(range, user_id)| rich_text::Mention {
+ range: range.clone(),
+ is_self_mention: *user_id == current_user_id,
+ })
+ .collect::<Vec<_>>();
+
+ rich_text::render_markdown(message.body.clone(), &mentions, language_registry, None)
+ }
+
+ fn render_sign_in_prompt(&self, cx: &mut ViewContext<Self>) -> AnyElement {
+ Button::new("sign-in", "Sign in to use chat")
+ .on_click(cx.listener(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, |_, cx| {
+ cx.focus_self();
+ })
+ .ok();
+ }
+ })
+ .detach();
+ }))
+ .into_any_element()
+ }
+
+ fn send(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
+ if let Some((chat, _)) = self.active_chat.as_ref() {
+ let message = self
+ .input_editor
+ .update(cx, |editor, cx| editor.take_message(cx));
+
+ if let Some(task) = chat
+ .update(cx, |chat, cx| chat.send_message(message, cx))
+ .log_err()
+ {
+ task.detach();
+ }
+ }
+ }
+
+ fn remove_message(&mut self, id: u64, cx: &mut ViewContext<Self>) {
+ if let Some((chat, _)) = self.active_chat.as_ref() {
+ chat.update(cx, |chat, cx| chat.remove_message(id, cx).detach())
+ }
+ }
+
+ fn load_more_messages(&mut self, cx: &mut ViewContext<Self>) {
+ if let Some((chat, _)) = self.active_chat.as_ref() {
+ chat.update(cx, |channel, cx| {
+ if let Some(task) = channel.load_more_messages(cx) {
+ task.detach();
+ }
+ })
+ }
+ }
+
+ pub fn select_channel(
+ &mut self,
+ selected_channel_id: u64,
+ scroll_to_message_id: Option<u64>,
+ cx: &mut ViewContext<ChatPanel>,
+ ) -> Task<Result<()>> {
+ let open_chat = self
+ .active_chat
+ .as_ref()
+ .and_then(|(chat, _)| {
+ (chat.read(cx).channel_id == selected_channel_id)
+ .then(|| Task::ready(anyhow::Ok(chat.clone())))
+ })
+ .unwrap_or_else(|| {
+ 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_chat(chat.clone(), cx);
+ })?;
+
+ if let Some(message_id) = scroll_to_message_id {
+ if let Some(item_ix) =
+ ChannelChat::load_history_since_message(chat.clone(), message_id, (*cx).clone())
+ .await
+ {
+ this.update(&mut cx, |this, cx| {
+ if this.active_chat.as_ref().map_or(false, |(c, _)| *c == chat) {
+ this.message_list.scroll_to(ListOffset {
+ item_ix,
+ offset_in_item: px(0.0),
+ });
+ cx.notify();
+ }
+ })?;
+ }
+ }
+
+ Ok(())
+ })
+ }
+
+ fn open_notes(&mut self, _: &ClickEvent, cx: &mut ViewContext<Self>) {
+ if let Some((chat, _)) = &self.active_chat {
+ let channel_id = chat.read(cx).channel_id;
+ if let Some(workspace) = self.workspace.upgrade() {
+ ChannelView::open(channel_id, workspace, cx).detach();
+ }
+ }
+ }
+
+ fn join_call(&mut self, _: &ClickEvent, cx: &mut ViewContext<Self>) {
+ if let Some((chat, _)) = &self.active_chat {
+ let channel_id = chat.read(cx).channel_id;
+ ActiveCall::global(cx)
+ .update(cx, |call, cx| call.join_channel(channel_id, cx))
+ .detach_and_log_err(cx);
+ }
+ }
+}
+
+fn render_remove(message_id_to_remove: Option<u64>, cx: &mut ViewContext<ChatPanel>) -> AnyElement {
+ if let Some(message_id) = message_id_to_remove {
+ IconButton::new(("remove", message_id), Icon::XCircle)
+ .on_click(cx.listener(move |this, _, cx| {
+ this.remove_message(message_id, cx);
+ }))
+ .into_any_element()
+ } else {
+ div().into_any_element()
+ }
+}
+
+impl EventEmitter<Event> for ChatPanel {}
+
+impl Render for ChatPanel {
+ type Element = Div;
+
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+ div()
+ .full()
+ .child(if self.client.user_id().is_some() {
+ self.render_channel(cx)
+ } else {
+ self.render_sign_in_prompt(cx)
+ })
+ .min_w(px(150.))
+ }
+}
+
+impl FocusableView for ChatPanel {
+ fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
+ self.input_editor.read(cx).focus_handle(cx)
+ }
+}
+
+impl Panel for ChatPanel {
+ fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
+ ChatPanelSettings::get_global(cx).dock
+ }
+
+ fn position_is_valid(&self, position: DockPosition) -> bool {
+ matches!(position, DockPosition::Left | DockPosition::Right)
+ }
+
+ fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
+ settings::update_settings_file::<ChatPanelSettings>(self.fs.clone(), cx, move |settings| {
+ settings.dock = Some(position)
+ });
+ }
+
+ fn size(&self, cx: &gpui::WindowContext) -> f32 {
+ self.width
+ .unwrap_or_else(|| ChatPanelSettings::get_global(cx).default_width)
+ }
+
+ fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>) {
+ self.width = size;
+ self.serialize(cx);
+ cx.notify();
+ }
+
+ fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
+ self.active = active;
+ if active {
+ self.acknowledge_last_message(cx);
+ if !is_channels_feature_enabled(cx) {
+ cx.emit(Event::Dismissed);
+ }
+ }
+ }
+
+ fn persistent_name() -> &'static str {
+ "ChatPanel"
+ }
+
+ fn icon(&self, _cx: &WindowContext) -> Option<ui::Icon> {
+ Some(ui::Icon::MessageBubbles)
+ }
+
+ fn toggle_action(&self) -> Box<dyn gpui::Action> {
+ Box::new(ToggleFocus)
+ }
+}
+
+impl EventEmitter<PanelEvent> for ChatPanel {}
+
+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())
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use gpui::HighlightStyle;
+ use pretty_assertions::assert_eq;
+ use rich_text::{BackgroundKind, Highlight, RenderedRegion};
+ use util::test::marked_text_ranges;
+
+ #[gpui::test]
+ fn test_render_markdown_with_mentions() {
+ let language_registry = Arc::new(LanguageRegistry::test());
+ let (body, ranges) = marked_text_ranges("*hi*, ยซ@abcยป, let's **call** ยซ@fghยป", false);
+ let message = channel::ChannelMessage {
+ id: ChannelMessageId::Saved(0),
+ body,
+ timestamp: OffsetDateTime::now_utc(),
+ sender: Arc::new(client::User {
+ github_login: "fgh".into(),
+ avatar: None,
+ id: 103,
+ }),
+ nonce: 5,
+ mentions: vec![(ranges[0].clone(), 101), (ranges[1].clone(), 102)],
+ };
+
+ let message = ChatPanel::render_markdown_with_mentions(&language_registry, 102, &message);
+
+ // Note that the "'" was replaced with โ due to smart punctuation.
+ let (body, ranges) = marked_text_ranges("ยซhiยป, ยซ@abcยป, letโs ยซcallยป ยซ@fghยป", false);
+ assert_eq!(message.text, body);
+ assert_eq!(
+ message.highlights,
+ vec![
+ (
+ ranges[0].clone(),
+ HighlightStyle {
+ font_style: Some(gpui::FontStyle::Italic),
+ ..Default::default()
+ }
+ .into()
+ ),
+ (ranges[1].clone(), Highlight::Mention),
+ (
+ ranges[2].clone(),
+ HighlightStyle {
+ font_weight: Some(gpui::FontWeight::BOLD),
+ ..Default::default()
+ }
+ .into()
+ ),
+ (ranges[3].clone(), Highlight::SelfMention)
+ ]
+ );
+ assert_eq!(
+ message.regions,
+ vec![
+ RenderedRegion {
+ background_kind: Some(BackgroundKind::Mention),
+ link_url: None
+ },
+ RenderedRegion {
+ background_kind: Some(BackgroundKind::SelfMention),
+ link_url: None
+ },
+ ]
+ );
+ }
+}
@@ -3,13 +3,14 @@ use client::UserId;
use collections::HashMap;
use editor::{AnchorRangeExt, Editor};
use gpui::{
- elements::ChildView, AnyElement, AsyncAppContext, Element, Entity, ModelHandle, Task, View,
- ViewContext, ViewHandle, WeakViewHandle,
+ AnyView, AsyncWindowContext, FocusableView, Model, Render, SharedString, Task, View,
+ ViewContext, WeakView,
};
use language::{language_settings::SoftWrap, Buffer, BufferSnapshot, LanguageRegistry};
use lazy_static::lazy_static;
use project::search::SearchQuery;
use std::{sync::Arc, time::Duration};
+use workspace::item::ItemHandle;
const MENTIONS_DEBOUNCE_INTERVAL: Duration = Duration::from_millis(50);
@@ -19,8 +20,8 @@ lazy_static! {
}
pub struct MessageEditor {
- pub editor: ViewHandle<Editor>,
- channel_store: ModelHandle<ChannelStore>,
+ pub editor: View<Editor>,
+ channel_store: Model<ChannelStore>,
users: HashMap<String, UserId>,
mentions: Vec<UserId>,
mentions_task: Option<Task<()>>,
@@ -30,8 +31,8 @@ pub struct MessageEditor {
impl MessageEditor {
pub fn new(
language_registry: Arc<LanguageRegistry>,
- channel_store: ModelHandle<ChannelStore>,
- editor: ViewHandle<Editor>,
+ channel_store: Model<ChannelStore>,
+ editor: View<Editor>,
cx: &mut ViewContext<Self>,
) -> Self {
editor.update(cx, |editor, cx| {
@@ -48,15 +49,13 @@ impl MessageEditor {
cx.subscribe(&buffer, Self::on_buffer_event).detach();
let markdown = language_registry.language_for_name("Markdown");
- cx.app_context()
- .spawn(|mut cx| async move {
- let markdown = markdown.await?;
- buffer.update(&mut cx, |buffer, cx| {
- buffer.set_language(Some(markdown), cx)
- });
- anyhow::Ok(())
+ cx.spawn(|_, mut cx| async move {
+ let markdown = markdown.await?;
+ buffer.update(&mut cx, |buffer, cx| {
+ buffer.set_language(Some(markdown), cx)
})
- .detach_and_log_err(cx);
+ })
+ .detach_and_log_err(cx);
Self {
editor,
@@ -71,7 +70,7 @@ impl MessageEditor {
pub fn set_channel(
&mut self,
channel_id: u64,
- channel_name: Option<String>,
+ channel_name: Option<SharedString>,
cx: &mut ViewContext<Self>,
) {
self.editor.update(cx, |editor, cx| {
@@ -132,26 +131,28 @@ impl MessageEditor {
fn on_buffer_event(
&mut self,
- buffer: ModelHandle<Buffer>,
+ buffer: Model<Buffer>,
event: &language::Event,
cx: &mut ViewContext<Self>,
) {
if let language::Event::Reparsed | language::Event::Edited = event {
let buffer = buffer.read(cx).snapshot();
self.mentions_task = Some(cx.spawn(|this, cx| async move {
- cx.background().timer(MENTIONS_DEBOUNCE_INTERVAL).await;
+ cx.background_executor()
+ .timer(MENTIONS_DEBOUNCE_INTERVAL)
+ .await;
Self::find_mentions(this, buffer, cx).await;
}));
}
}
async fn find_mentions(
- this: WeakViewHandle<MessageEditor>,
+ this: WeakView<MessageEditor>,
buffer: BufferSnapshot,
- mut cx: AsyncAppContext,
+ mut cx: AsyncWindowContext,
) {
let (buffer, ranges) = cx
- .background()
+ .background_executor()
.spawn(async move {
let ranges = MENTIONS_SEARCH.search(&buffer, None).await;
(buffer, ranges)
@@ -180,11 +181,7 @@ impl MessageEditor {
}
editor.clear_highlights::<Self>(cx);
- editor.highlight_text::<Self>(
- anchor_ranges,
- theme::current(cx).chat_panel.rich_text.mention_highlight,
- cx,
- )
+ editor.highlight_text::<Self>(anchor_ranges, gpui::red().into(), cx)
});
this.mentions = mentioned_user_ids;
@@ -192,116 +189,112 @@ impl MessageEditor {
})
.ok();
}
-}
-
-impl Entity for MessageEditor {
- type Event = ();
-}
-
-impl View for MessageEditor {
- fn render(&mut self, cx: &mut ViewContext<'_, '_, Self>) -> AnyElement<Self> {
- ChildView::new(&self.editor, cx).into_any()
- }
- fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
- if cx.is_self_focused() {
- cx.focus(&self.editor);
- }
+ pub(crate) fn focus_handle(&self, cx: &gpui::AppContext) -> gpui::FocusHandle {
+ self.editor.read(cx).focus_handle(cx)
}
}
-#[cfg(test)]
-mod tests {
- use super::*;
- use client::{Client, User, UserStore};
- use gpui::{TestAppContext, WindowHandle};
- use language::{Language, LanguageConfig};
- use rpc::proto;
- use settings::SettingsStore;
- use util::{http::FakeHttpClient, test::marked_text_ranges};
-
- #[gpui::test]
- async fn test_message_editor(cx: &mut TestAppContext) {
- let editor = init_test(cx);
- let editor = editor.root(cx);
+impl Render for MessageEditor {
+ type Element = AnyView;
- editor.update(cx, |editor, cx| {
- editor.set_members(
- vec![
- ChannelMembership {
- user: Arc::new(User {
- github_login: "a-b".into(),
- id: 101,
- avatar: None,
- }),
- kind: proto::channel_member::Kind::Member,
- role: proto::ChannelRole::Member,
- },
- ChannelMembership {
- user: Arc::new(User {
- github_login: "C_D".into(),
- id: 102,
- avatar: None,
- }),
- kind: proto::channel_member::Kind::Member,
- role: proto::ChannelRole::Member,
- },
- ],
- cx,
- );
-
- editor.editor.update(cx, |editor, cx| {
- editor.set_text("Hello, @a-b! Have you met @C_D?", cx)
- });
- });
-
- cx.foreground().advance_clock(MENTIONS_DEBOUNCE_INTERVAL);
-
- editor.update(cx, |editor, cx| {
- let (text, ranges) = marked_text_ranges("Hello, ยซ@a-bยป! Have you met ยซ@C_Dยป?", false);
- assert_eq!(
- editor.take_message(cx),
- MessageParams {
- text,
- mentions: vec![(ranges[0].clone(), 101), (ranges[1].clone(), 102)],
- }
- );
- });
- }
-
- fn init_test(cx: &mut TestAppContext) -> WindowHandle<MessageEditor> {
- cx.foreground().forbid_parking();
-
- cx.update(|cx| {
- 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.set_global(SettingsStore::test(cx));
- theme::init((), cx);
- language::init(cx);
- editor::init(cx);
- client::init(&client, cx);
- channel::init(&client, user_store, cx);
- });
-
- let language_registry = Arc::new(LanguageRegistry::test());
- language_registry.add(Arc::new(Language::new(
- LanguageConfig {
- name: "Markdown".into(),
- ..Default::default()
- },
- Some(tree_sitter_markdown::language()),
- )));
-
- let editor = cx.add_window(|cx| {
- MessageEditor::new(
- language_registry,
- ChannelStore::global(cx),
- cx.add_view(|cx| Editor::auto_height(4, None, cx)),
- cx,
- )
- });
- cx.foreground().run_until_parked();
- editor
+ fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
+ self.editor.to_any()
}
}
+
+// #[cfg(test)]
+// mod tests {
+// use super::*;
+// use client::{Client, User, UserStore};
+// use gpui::{TestAppContext, WindowHandle};
+// use language::{Language, LanguageConfig};
+// use rpc::proto;
+// use settings::SettingsStore;
+// use util::{http::FakeHttpClient, test::marked_text_ranges};
+
+// #[gpui::test]
+// async fn test_message_editor(cx: &mut TestAppContext) {
+// let editor = init_test(cx);
+// let editor = editor.root(cx);
+
+// editor.update(cx, |editor, cx| {
+// editor.set_members(
+// vec![
+// ChannelMembership {
+// user: Arc::new(User {
+// github_login: "a-b".into(),
+// id: 101,
+// avatar: None,
+// }),
+// kind: proto::channel_member::Kind::Member,
+// role: proto::ChannelRole::Member,
+// },
+// ChannelMembership {
+// user: Arc::new(User {
+// github_login: "C_D".into(),
+// id: 102,
+// avatar: None,
+// }),
+// kind: proto::channel_member::Kind::Member,
+// role: proto::ChannelRole::Member,
+// },
+// ],
+// cx,
+// );
+
+// editor.editor.update(cx, |editor, cx| {
+// editor.set_text("Hello, @a-b! Have you met @C_D?", cx)
+// });
+// });
+
+// cx.foreground().advance_clock(MENTIONS_DEBOUNCE_INTERVAL);
+
+// editor.update(cx, |editor, cx| {
+// let (text, ranges) = marked_text_ranges("Hello, ยซ@a-bยป! Have you met ยซ@C_Dยป?", false);
+// assert_eq!(
+// editor.take_message(cx),
+// MessageParams {
+// text,
+// mentions: vec![(ranges[0].clone(), 101), (ranges[1].clone(), 102)],
+// }
+// );
+// });
+// }
+
+// fn init_test(cx: &mut TestAppContext) -> WindowHandle<MessageEditor> {
+// cx.foreground().forbid_parking();
+
+// cx.update(|cx| {
+// 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.set_global(SettingsStore::test(cx));
+// theme::init((), cx);
+// language::init(cx);
+// editor::init(cx);
+// client::init(&client, cx);
+// channel::init(&client, user_store, cx);
+// });
+
+// let language_registry = Arc::new(LanguageRegistry::test());
+// language_registry.add(Arc::new(Language::new(
+// LanguageConfig {
+// name: "Markdown".into(),
+// ..Default::default()
+// },
+// Some(tree_sitter_markdown::language()),
+// )));
+
+// let editor = cx.add_window(|cx| {
+// MessageEditor::new(
+// language_registry,
+// ChannelStore::global(cx),
+// cx.add_view(|cx| Editor::auto_height(4, cx)),
+// cx,
+// )
+// });
+// cx.foreground().run_until_parked();
+// editor
+// }
+// }
@@ -192,6 +192,7 @@ use workspace::{
};
use crate::channel_view::ChannelView;
+use crate::chat_panel::ChatPanel;
use crate::{face_pile::FacePile, CollaborationPanelSettings};
use self::channel_modal::ChannelModal;
@@ -852,7 +853,7 @@ impl CollabPanel {
.extend(channel_store.ordered_channels().enumerate().map(
|(ix, (_, channel))| StringMatchCandidate {
id: ix,
- string: channel.name.clone(),
+ string: channel.name.clone().into(),
char_bag: channel.name.chars().collect(),
},
));
@@ -2102,14 +2103,13 @@ impl CollabPanel {
};
cx.window_context().defer(move |cx| {
workspace.update(cx, |workspace, cx| {
- todo!();
- // if let Some(panel) = workspace.focus_panel::<ChatPanel>(cx) {
- // panel.update(cx, |panel, cx| {
- // panel
- // .select_channel(channel_id, None, cx)
- // .detach_and_log_err(cx);
- // });
- // }
+ if let Some(panel) = workspace.focus_panel::<ChatPanel>(cx) {
+ panel.update(cx, |panel, cx| {
+ panel
+ .select_channel(channel_id, None, cx)
+ .detach_and_log_err(cx);
+ });
+ }
});
});
}
@@ -2262,7 +2262,7 @@ impl CollabPanel {
}
};
- Some(channel.name.as_str())
+ Some(channel.name.as_ref())
});
if let Some(name) = channel_name {
@@ -2603,9 +2603,14 @@ impl CollabPanel {
Color::Default
} else {
Color::Muted
+ })
+ .on_click(cx.listener(move |this, _, cx| {
+ this.join_channel_chat(channel_id, cx)
+ }))
+ .tooltip(|cx| {
+ Tooltip::text("Open channel chat", cx)
}),
- )
- .tooltip(|cx| Tooltip::text("Open channel chat", cx)),
+ ),
)
.child(
div()
@@ -12,6 +12,7 @@ use std::{rc::Rc, sync::Arc};
use call::{report_call_event_for_room, ActiveCall, Room};
pub use collab_panel::CollabPanel;
pub use collab_titlebar_item::CollabTitlebarItem;
+use feature_flags::{ChannelsAlpha, FeatureFlagAppExt};
use gpui::{
actions, point, AppContext, GlobalPixels, Pixels, PlatformDisplay, Size, Task, WindowBounds,
WindowKind, WindowOptions,
@@ -157,6 +158,6 @@ fn notification_window_options(
// .into_any()
// }
-// fn is_channels_feature_enabled(cx: &gpui::WindowContext<'_>) -> bool {
-// cx.is_staff() || cx.has_flag::<ChannelsAlpha>()
-// }
+fn is_channels_feature_enabled(cx: &gpui::WindowContext<'_>) -> bool {
+ cx.is_staff() || cx.has_flag::<ChannelsAlpha>()
+}
@@ -13,6 +13,7 @@ use smallvec::SmallVec;
use smol::future::FutureExt;
#[cfg(any(test, feature = "test-support"))]
pub use test_context::*;
+use time::UtcOffset;
use crate::{
current_platform, image_cache::ImageCache, init_app_menus, Action, ActionRegistry, Any,
@@ -536,6 +537,10 @@ impl AppContext {
self.platform.restart()
}
+ pub fn local_timezone(&self) -> UtcOffset {
+ self.platform.local_timezone()
+ }
+
pub(crate) fn push_effect(&mut self, effect: Effect) {
match &effect {
Effect::Notify { emitter } => {
@@ -0,0 +1,496 @@
+use crate::{
+ px, AnyElement, AvailableSpace, BorrowAppContext, DispatchPhase, Element, IntoElement, Pixels,
+ Point, ScrollWheelEvent, Size, Style, StyleRefinement, Styled, WindowContext,
+};
+use collections::VecDeque;
+use refineable::Refineable as _;
+use std::{cell::RefCell, ops::Range, rc::Rc};
+use sum_tree::{Bias, SumTree};
+
+pub fn list(state: ListState) -> List {
+ List {
+ state,
+ style: StyleRefinement::default(),
+ }
+}
+
+pub struct List {
+ state: ListState,
+ style: StyleRefinement,
+}
+
+#[derive(Clone)]
+pub struct ListState(Rc<RefCell<StateInner>>);
+
+struct StateInner {
+ last_layout_width: Option<Pixels>,
+ render_item: Box<dyn FnMut(usize, &mut WindowContext) -> AnyElement>,
+ items: SumTree<ListItem>,
+ logical_scroll_top: Option<ListOffset>,
+ alignment: ListAlignment,
+ overdraw: Pixels,
+ #[allow(clippy::type_complexity)]
+ scroll_handler: Option<Box<dyn FnMut(&ListScrollEvent, &mut WindowContext)>>,
+}
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub enum ListAlignment {
+ Top,
+ Bottom,
+}
+
+pub struct ListScrollEvent {
+ pub visible_range: Range<usize>,
+ pub count: usize,
+}
+
+#[derive(Clone)]
+enum ListItem {
+ Unrendered,
+ Rendered { height: Pixels },
+}
+
+#[derive(Clone, Debug, Default, PartialEq)]
+struct ListItemSummary {
+ count: usize,
+ rendered_count: usize,
+ unrendered_count: usize,
+ height: Pixels,
+}
+
+#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
+struct Count(usize);
+
+#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
+struct RenderedCount(usize);
+
+#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
+struct UnrenderedCount(usize);
+
+#[derive(Clone, Debug, Default)]
+struct Height(Pixels);
+
+impl ListState {
+ pub fn new<F>(
+ element_count: usize,
+ orientation: ListAlignment,
+ overdraw: Pixels,
+ render_item: F,
+ ) -> Self
+ where
+ F: 'static + FnMut(usize, &mut WindowContext) -> AnyElement,
+ {
+ let mut items = SumTree::new();
+ items.extend((0..element_count).map(|_| ListItem::Unrendered), &());
+ Self(Rc::new(RefCell::new(StateInner {
+ last_layout_width: None,
+ render_item: Box::new(render_item),
+ items,
+ logical_scroll_top: None,
+ alignment: orientation,
+ overdraw,
+ scroll_handler: None,
+ })))
+ }
+
+ pub fn reset(&self, element_count: usize) {
+ let state = &mut *self.0.borrow_mut();
+ state.logical_scroll_top = None;
+ state.items = SumTree::new();
+ state
+ .items
+ .extend((0..element_count).map(|_| ListItem::Unrendered), &());
+ }
+
+ pub fn item_count(&self) -> usize {
+ self.0.borrow().items.summary().count
+ }
+
+ pub fn splice(&self, old_range: Range<usize>, count: usize) {
+ let state = &mut *self.0.borrow_mut();
+
+ if let Some(ListOffset {
+ item_ix,
+ offset_in_item,
+ }) = state.logical_scroll_top.as_mut()
+ {
+ if old_range.contains(item_ix) {
+ *item_ix = old_range.start;
+ *offset_in_item = px(0.);
+ } else if old_range.end <= *item_ix {
+ *item_ix = *item_ix - (old_range.end - old_range.start) + count;
+ }
+ }
+
+ let mut old_heights = state.items.cursor::<Count>();
+ let mut new_heights = old_heights.slice(&Count(old_range.start), Bias::Right, &());
+ old_heights.seek_forward(&Count(old_range.end), Bias::Right, &());
+
+ new_heights.extend((0..count).map(|_| ListItem::Unrendered), &());
+ new_heights.append(old_heights.suffix(&()), &());
+ drop(old_heights);
+ state.items = new_heights;
+ }
+
+ pub fn set_scroll_handler(
+ &self,
+ handler: impl FnMut(&ListScrollEvent, &mut WindowContext) + 'static,
+ ) {
+ self.0.borrow_mut().scroll_handler = Some(Box::new(handler))
+ }
+
+ pub fn logical_scroll_top(&self) -> ListOffset {
+ self.0.borrow().logical_scroll_top()
+ }
+
+ pub fn scroll_to(&self, mut scroll_top: ListOffset) {
+ let state = &mut *self.0.borrow_mut();
+ let item_count = state.items.summary().count;
+ if scroll_top.item_ix >= item_count {
+ scroll_top.item_ix = item_count;
+ scroll_top.offset_in_item = px(0.);
+ }
+ state.logical_scroll_top = Some(scroll_top);
+ }
+}
+
+impl StateInner {
+ fn visible_range(&self, height: Pixels, scroll_top: &ListOffset) -> Range<usize> {
+ let mut cursor = self.items.cursor::<ListItemSummary>();
+ cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
+ let start_y = cursor.start().height + scroll_top.offset_in_item;
+ cursor.seek_forward(&Height(start_y + height), Bias::Left, &());
+ scroll_top.item_ix..cursor.start().count + 1
+ }
+
+ fn scroll(
+ &mut self,
+ scroll_top: &ListOffset,
+ height: Pixels,
+ delta: Point<Pixels>,
+ cx: &mut WindowContext,
+ ) {
+ let scroll_max = (self.items.summary().height - height).max(px(0.));
+ let new_scroll_top = (self.scroll_top(scroll_top) - delta.y)
+ .max(px(0.))
+ .min(scroll_max);
+
+ if self.alignment == ListAlignment::Bottom && new_scroll_top == scroll_max {
+ self.logical_scroll_top = None;
+ } else {
+ let mut cursor = self.items.cursor::<ListItemSummary>();
+ cursor.seek(&Height(new_scroll_top), Bias::Right, &());
+ let item_ix = cursor.start().count;
+ let offset_in_item = new_scroll_top - cursor.start().height;
+ self.logical_scroll_top = Some(ListOffset {
+ item_ix,
+ offset_in_item,
+ });
+ }
+
+ if self.scroll_handler.is_some() {
+ let visible_range = self.visible_range(height, scroll_top);
+ self.scroll_handler.as_mut().unwrap()(
+ &ListScrollEvent {
+ visible_range,
+ count: self.items.summary().count,
+ },
+ cx,
+ );
+ }
+
+ cx.notify();
+ }
+
+ fn logical_scroll_top(&self) -> ListOffset {
+ self.logical_scroll_top
+ .unwrap_or_else(|| match self.alignment {
+ ListAlignment::Top => ListOffset {
+ item_ix: 0,
+ offset_in_item: px(0.),
+ },
+ ListAlignment::Bottom => ListOffset {
+ item_ix: self.items.summary().count,
+ offset_in_item: px(0.),
+ },
+ })
+ }
+
+ fn scroll_top(&self, logical_scroll_top: &ListOffset) -> Pixels {
+ let mut cursor = self.items.cursor::<ListItemSummary>();
+ cursor.seek(&Count(logical_scroll_top.item_ix), Bias::Right, &());
+ cursor.start().height + logical_scroll_top.offset_in_item
+ }
+}
+
+impl std::fmt::Debug for ListItem {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Self::Unrendered => write!(f, "Unrendered"),
+ Self::Rendered { height, .. } => {
+ f.debug_struct("Rendered").field("height", height).finish()
+ }
+ }
+ }
+}
+
+#[derive(Debug, Clone, Copy)]
+pub struct ListOffset {
+ pub item_ix: usize,
+ pub offset_in_item: Pixels,
+}
+
+impl Element for List {
+ type State = ();
+
+ fn layout(
+ &mut self,
+ _state: Option<Self::State>,
+ cx: &mut crate::WindowContext,
+ ) -> (crate::LayoutId, Self::State) {
+ let mut style = Style::default();
+ style.refine(&self.style);
+ let layout_id = cx.with_text_style(style.text_style().cloned(), |cx| {
+ cx.request_layout(&style, None)
+ });
+ (layout_id, ())
+ }
+
+ fn paint(
+ self,
+ bounds: crate::Bounds<crate::Pixels>,
+ _state: &mut Self::State,
+ cx: &mut crate::WindowContext,
+ ) {
+ let state = &mut *self.state.0.borrow_mut();
+
+ // If the width of the list has changed, invalidate all cached item heights
+ if state.last_layout_width != Some(bounds.size.width) {
+ state.items = SumTree::from_iter(
+ (0..state.items.summary().count).map(|_| ListItem::Unrendered),
+ &(),
+ )
+ }
+
+ let old_items = state.items.clone();
+ let mut measured_items = VecDeque::new();
+ let mut item_elements = VecDeque::new();
+ let mut rendered_height = px(0.);
+ let mut scroll_top = state.logical_scroll_top();
+
+ let available_item_space = Size {
+ width: AvailableSpace::Definite(bounds.size.width),
+ height: AvailableSpace::MinContent,
+ };
+
+ // Render items after the scroll top, including those in the trailing overdraw
+ let mut cursor = old_items.cursor::<Count>();
+ cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
+ for (ix, item) in cursor.by_ref().enumerate() {
+ let visible_height = rendered_height - scroll_top.offset_in_item;
+ if visible_height >= bounds.size.height + state.overdraw {
+ break;
+ }
+
+ // Use the previously cached height if available
+ let mut height = if let ListItem::Rendered { height } = item {
+ Some(*height)
+ } else {
+ None
+ };
+
+ // If we're within the visible area or the height wasn't cached, render and measure the item's element
+ if visible_height < bounds.size.height || height.is_none() {
+ let mut element = (state.render_item)(scroll_top.item_ix + ix, cx);
+ let element_size = element.measure(available_item_space, cx);
+ height = Some(element_size.height);
+ if visible_height < bounds.size.height {
+ item_elements.push_back(element);
+ }
+ }
+
+ let height = height.unwrap();
+ rendered_height += height;
+ measured_items.push_back(ListItem::Rendered { height });
+ }
+
+ // Prepare to start walking upward from the item at the scroll top.
+ cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
+
+ // If the rendered items do not fill the visible region, then adjust
+ // the scroll top upward.
+ if rendered_height - scroll_top.offset_in_item < bounds.size.height {
+ while rendered_height < bounds.size.height {
+ cursor.prev(&());
+ if cursor.item().is_some() {
+ let mut element = (state.render_item)(cursor.start().0, cx);
+ let element_size = element.measure(available_item_space, cx);
+
+ rendered_height += element_size.height;
+ measured_items.push_front(ListItem::Rendered {
+ height: element_size.height,
+ });
+ item_elements.push_front(element)
+ } else {
+ break;
+ }
+ }
+
+ scroll_top = ListOffset {
+ item_ix: cursor.start().0,
+ offset_in_item: rendered_height - bounds.size.height,
+ };
+
+ match state.alignment {
+ ListAlignment::Top => {
+ scroll_top.offset_in_item = scroll_top.offset_in_item.max(px(0.));
+ state.logical_scroll_top = Some(scroll_top);
+ }
+ ListAlignment::Bottom => {
+ scroll_top = ListOffset {
+ item_ix: cursor.start().0,
+ offset_in_item: rendered_height - bounds.size.height,
+ };
+ state.logical_scroll_top = None;
+ }
+ };
+ }
+
+ // Measure items in the leading overdraw
+ let mut leading_overdraw = scroll_top.offset_in_item;
+ while leading_overdraw < state.overdraw {
+ cursor.prev(&());
+ if let Some(item) = cursor.item() {
+ let height = if let ListItem::Rendered { height } = item {
+ *height
+ } else {
+ let mut element = (state.render_item)(cursor.start().0, cx);
+ element.measure(available_item_space, cx).height
+ };
+
+ leading_overdraw += height;
+ measured_items.push_front(ListItem::Rendered { height });
+ } else {
+ break;
+ }
+ }
+
+ let measured_range = cursor.start().0..(cursor.start().0 + measured_items.len());
+ let mut cursor = old_items.cursor::<Count>();
+ let mut new_items = cursor.slice(&Count(measured_range.start), Bias::Right, &());
+ new_items.extend(measured_items, &());
+ cursor.seek(&Count(measured_range.end), Bias::Right, &());
+ new_items.append(cursor.suffix(&()), &());
+
+ // Paint the visible items
+ let mut item_origin = bounds.origin;
+ item_origin.y -= scroll_top.offset_in_item;
+ for mut item_element in item_elements {
+ let item_height = item_element.measure(available_item_space, cx).height;
+ item_element.draw(item_origin, available_item_space, cx);
+ item_origin.y += item_height;
+ }
+
+ state.items = new_items;
+ state.last_layout_width = Some(bounds.size.width);
+
+ let list_state = self.state.clone();
+ let height = bounds.size.height;
+ cx.on_mouse_event(move |event: &ScrollWheelEvent, phase, cx| {
+ if phase == DispatchPhase::Bubble {
+ list_state.0.borrow_mut().scroll(
+ &scroll_top,
+ height,
+ event.delta.pixel_delta(px(20.)),
+ cx,
+ )
+ }
+ });
+ }
+}
+
+impl IntoElement for List {
+ type Element = Self;
+
+ fn element_id(&self) -> Option<crate::ElementId> {
+ None
+ }
+
+ fn into_element(self) -> Self::Element {
+ self
+ }
+}
+
+impl Styled for List {
+ fn style(&mut self) -> &mut StyleRefinement {
+ &mut self.style
+ }
+}
+
+impl sum_tree::Item for ListItem {
+ type Summary = ListItemSummary;
+
+ fn summary(&self) -> Self::Summary {
+ match self {
+ ListItem::Unrendered => ListItemSummary {
+ count: 1,
+ rendered_count: 0,
+ unrendered_count: 1,
+ height: px(0.),
+ },
+ ListItem::Rendered { height } => ListItemSummary {
+ count: 1,
+ rendered_count: 1,
+ unrendered_count: 0,
+ height: *height,
+ },
+ }
+ }
+}
+
+impl sum_tree::Summary for ListItemSummary {
+ type Context = ();
+
+ fn add_summary(&mut self, summary: &Self, _: &()) {
+ self.count += summary.count;
+ self.rendered_count += summary.rendered_count;
+ self.unrendered_count += summary.unrendered_count;
+ self.height += summary.height;
+ }
+}
+
+impl<'a> sum_tree::Dimension<'a, ListItemSummary> for Count {
+ fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
+ self.0 += summary.count;
+ }
+}
+
+impl<'a> sum_tree::Dimension<'a, ListItemSummary> for RenderedCount {
+ fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
+ self.0 += summary.rendered_count;
+ }
+}
+
+impl<'a> sum_tree::Dimension<'a, ListItemSummary> for UnrenderedCount {
+ fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
+ self.0 += summary.unrendered_count;
+ }
+}
+
+impl<'a> sum_tree::Dimension<'a, ListItemSummary> for Height {
+ fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
+ self.0 += summary.height;
+ }
+}
+
+impl<'a> sum_tree::SeekTarget<'a, ListItemSummary, ListItemSummary> for Count {
+ fn cmp(&self, other: &ListItemSummary, _: &()) -> std::cmp::Ordering {
+ self.0.partial_cmp(&other.count).unwrap()
+ }
+}
+
+impl<'a> sum_tree::SeekTarget<'a, ListItemSummary, ListItemSummary> for Height {
+ fn cmp(&self, other: &ListItemSummary, _: &()) -> std::cmp::Ordering {
+ self.0.partial_cmp(&other.height).unwrap()
+ }
+}
@@ -1,6 +1,7 @@
mod canvas;
mod div;
mod img;
+mod list;
mod overlay;
mod svg;
mod text;
@@ -9,6 +10,7 @@ mod uniform_list;
pub use canvas::*;
pub use div::*;
pub use img::*;
+pub use list::*;
pub use overlay::*;
pub use svg::*;
pub use text::*;
@@ -131,7 +131,7 @@ impl Element for UniformList {
}
});
let height = match available_space.height {
- AvailableSpace::Definite(x) => desired_height.min(x),
+ AvailableSpace::Definite(height) => desired_height.min(height),
AvailableSpace::MinContent | AvailableSpace::MaxContent => {
desired_height
}
@@ -1,6 +1,7 @@
#[macro_use]
mod action;
mod app;
+
mod assets;
mod color;
mod element;
@@ -15,6 +16,7 @@ mod keymap;
mod platform;
pub mod prelude;
mod scene;
+mod shared_string;
mod style;
mod styled;
mod subscription;
@@ -57,6 +59,7 @@ pub use scene::*;
pub use serde;
pub use serde_derive;
pub use serde_json;
+pub use shared_string::*;
pub use smallvec;
pub use smol::Timer;
pub use style::*;
@@ -71,10 +74,9 @@ pub use util::arc_cow::ArcCow;
pub use view::*;
pub use window::*;
-use derive_more::{Deref, DerefMut};
use std::{
any::{Any, TypeId},
- borrow::{Borrow, BorrowMut},
+ borrow::BorrowMut,
};
use taffy::TaffyLayoutEngine;
@@ -209,42 +211,3 @@ impl<T> Flatten<T> for Result<T> {
self
}
}
-
-#[derive(Deref, DerefMut, Eq, PartialEq, Hash, Clone)]
-pub struct SharedString(ArcCow<'static, str>);
-
-impl Default for SharedString {
- fn default() -> Self {
- Self(ArcCow::Owned("".into()))
- }
-}
-
-impl AsRef<str> for SharedString {
- fn as_ref(&self) -> &str {
- &self.0
- }
-}
-
-impl Borrow<str> for SharedString {
- fn borrow(&self) -> &str {
- self.as_ref()
- }
-}
-
-impl std::fmt::Debug for SharedString {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- self.0.fmt(f)
- }
-}
-
-impl std::fmt::Display for SharedString {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- write!(f, "{}", self.0.as_ref())
- }
-}
-
-impl<T: Into<ArcCow<'static, str>>> From<T> for SharedString {
- fn from(value: T) -> Self {
- Self(value.into())
- }
-}
@@ -149,13 +149,19 @@ impl DispatchTree {
}
pub fn available_actions(&self, target: DispatchNodeId) -> Vec<Box<dyn Action>> {
- let mut actions = Vec::new();
+ let mut actions = Vec::<Box<dyn Action>>::new();
for node_id in self.dispatch_path(target) {
let node = &self.nodes[node_id.0];
for DispatchActionListener { action_type, .. } in &node.action_listeners {
- // Intentionally silence these errors without logging.
- // If an action cannot be built by default, it's not available.
- actions.extend(self.action_registry.build_action_type(action_type).ok());
+ if let Err(ix) = actions.binary_search_by_key(action_type, |a| a.as_any().type_id())
+ {
+ // Intentionally silence these errors without logging.
+ // If an action cannot be built by default, it's not available.
+ let action = self.action_registry.build_action_type(action_type).ok();
+ if let Some(action) = action {
+ actions.insert(ix, action);
+ }
+ }
}
}
actions
@@ -0,0 +1,101 @@
+use derive_more::{Deref, DerefMut};
+use serde::{Deserialize, Serialize};
+use std::{borrow::Borrow, sync::Arc};
+use util::arc_cow::ArcCow;
+
+#[derive(Deref, DerefMut, Eq, PartialEq, Hash, Clone)]
+pub struct SharedString(ArcCow<'static, str>);
+
+impl Default for SharedString {
+ fn default() -> Self {
+ Self(ArcCow::Owned("".into()))
+ }
+}
+
+impl AsRef<str> for SharedString {
+ fn as_ref(&self) -> &str {
+ &self.0
+ }
+}
+
+impl Borrow<str> for SharedString {
+ fn borrow(&self) -> &str {
+ self.as_ref()
+ }
+}
+
+impl std::fmt::Debug for SharedString {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ self.0.fmt(f)
+ }
+}
+
+impl std::fmt::Display for SharedString {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "{}", self.0.as_ref())
+ }
+}
+
+impl PartialEq<String> for SharedString {
+ fn eq(&self, other: &String) -> bool {
+ self.as_ref() == other
+ }
+}
+
+impl PartialEq<SharedString> for String {
+ fn eq(&self, other: &SharedString) -> bool {
+ self == other.as_ref()
+ }
+}
+
+impl PartialEq<str> for SharedString {
+ fn eq(&self, other: &str) -> bool {
+ self.as_ref() == other
+ }
+}
+
+impl<'a> PartialEq<&'a str> for SharedString {
+ fn eq(&self, other: &&'a str) -> bool {
+ self.as_ref() == *other
+ }
+}
+
+impl Into<Arc<str>> for SharedString {
+ fn into(self) -> Arc<str> {
+ match self.0 {
+ ArcCow::Borrowed(borrowed) => Arc::from(borrowed),
+ ArcCow::Owned(owned) => owned.clone(),
+ }
+ }
+}
+
+impl<T: Into<ArcCow<'static, str>>> From<T> for SharedString {
+ fn from(value: T) -> Self {
+ Self(value.into())
+ }
+}
+
+impl Into<String> for SharedString {
+ fn into(self) -> String {
+ self.0.to_string()
+ }
+}
+
+impl Serialize for SharedString {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: serde::Serializer,
+ {
+ serializer.serialize_str(self.as_ref())
+ }
+}
+
+impl<'de> Deserialize<'de> for SharedString {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: serde::Deserializer<'de>,
+ {
+ let s = String::deserialize(deserializer)?;
+ Ok(SharedString::from(s))
+ }
+}
@@ -2,7 +2,7 @@ use std::{ops::Range, sync::Arc};
use anyhow::bail;
use futures::FutureExt;
-use gpui::{AnyElement, FontStyle, FontWeight, HighlightStyle, UnderlineStyle};
+use gpui::{AnyElement, FontStyle, FontWeight, HighlightStyle, UnderlineStyle, WindowContext};
use language::{HighlightId, Language, LanguageRegistry};
use util::RangeExt;
@@ -56,12 +56,7 @@ pub struct Mention {
}
impl RichText {
- pub fn element(
- &self,
- // syntax: Arc<SyntaxTheme>,
- // style: RichTextStyle,
- // cx: &mut ViewContext<V>,
- ) -> AnyElement {
+ pub fn element(&self, _cx: &mut WindowContext) -> AnyElement {
todo!();
// let mut region_id = 0;
@@ -1,12 +1,10 @@
-use std::any::TypeId;
-
use crate::{ItemHandle, Pane};
use gpui::{
div, AnyView, Div, IntoElement, ParentElement, Render, Styled, Subscription, View, ViewContext,
WindowContext,
};
-use ui::h_stack;
-use ui::prelude::*;
+use std::any::TypeId;
+use ui::{h_stack, prelude::*};
use util::ResultExt;
pub trait StatusItemView: Render {
@@ -47,8 +45,8 @@ impl Render for StatusBar {
.w_full()
.h_8()
.bg(cx.theme().colors().status_bar_background)
- .child(h_stack().gap_1().child(self.render_left_tools(cx)))
- .child(h_stack().gap_4().child(self.render_right_tools(cx)))
+ .child(self.render_left_tools(cx))
+ .child(self.render_right_tools(cx))
}
}
@@ -160,8 +160,8 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
let assistant_panel = AssistantPanel::load(workspace_handle.clone(), cx.clone());
let channels_panel =
collab_ui::collab_panel::CollabPanel::load(workspace_handle.clone(), cx.clone());
- // let chat_panel =
- // collab_ui::chat_panel::ChatPanel::load(workspace_handle.clone(), cx.clone());
+ let chat_panel =
+ collab_ui::chat_panel::ChatPanel::load(workspace_handle.clone(), cx.clone());
// let notification_panel = collab_ui::notification_panel::NotificationPanel::load(
// workspace_handle.clone(),
// cx.clone(),
@@ -171,14 +171,14 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
terminal_panel,
assistant_panel,
channels_panel,
- // chat_panel,
+ chat_panel,
// notification_panel,
) = futures::try_join!(
project_panel,
terminal_panel,
assistant_panel,
channels_panel,
- // chat_panel,
+ chat_panel,
// notification_panel,
)?;
@@ -188,7 +188,7 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
workspace.add_panel(terminal_panel, cx);
workspace.add_panel(assistant_panel, cx);
workspace.add_panel(channels_panel, cx);
- // workspace.add_panel(chat_panel, cx);
+ workspace.add_panel(chat_panel, cx);
// workspace.add_panel(notification_panel, cx);
// if !was_deserialized