Enable chat panel in zed 2 (#3564)

This commit is contained in:
Max Brunsfeld
2023-12-08 13:25:59 -08:00
committed by GitHub
20 changed files with 1522 additions and 1242 deletions
+2 -2
View File
@@ -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)]
+4 -3
View File
@@ -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 {
+7 -4
View File
@@ -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)
}
+32 -32
View File
@@ -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,
File diff suppressed because it is too large Load Diff
+124 -131
View File
@@ -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};
impl Render for MessageEditor {
type Element = AnyView;
#[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, 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
// }
// }
+17 -12
View File
@@ -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()
+4 -3
View File
@@ -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>()
}
+5
View File
@@ -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 } => {
+496
View File
@@ -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()
}
}
+2
View File
@@ -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::*;
+1 -1
View File
@@ -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
}
+4 -41
View File
@@ -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())
}
}
+10 -4
View File
@@ -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
+101
View File
@@ -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
View File
@@ -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;
+4 -6
View File
@@ -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))
}
}
+5 -5
View File
@@ -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