Enable chat panel in zed 2 (#3564)
This commit is contained in:
@@ -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,
|
||||
|
||||
+694
-983
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user