Start work on chat panel and non-uniform list

Co-authored-by: Nathan <nathan@zed.dev>
This commit is contained in:
Max Brunsfeld 2023-12-07 18:14:10 -08:00
parent 4353bdb9d5
commit 6955579f19
9 changed files with 758 additions and 400 deletions

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)]

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 {

View File

@ -1,6 +1,4 @@
// use crate::{
// channel_view::ChannelView, is_channels_feature_enabled, render_avatar, ChatPanelSettings,
// };
// use crate::{channel_view::ChannelView, is_channels_feature_enabled, ChatPanelSettings};
// use anyhow::Result;
// use call::ActiveCall;
// use channel::{ChannelChat, ChannelChatEvent, ChannelMessageId, ChannelStore};
@ -9,13 +7,9 @@
// use db::kvp::KEY_VALUE_STORE;
// use editor::Editor;
// use gpui::{
// actions,
// elements::*,
// platform::{CursorStyle, MouseButton},
// serde_json,
// views::{ItemType, Select, SelectStyle},
// AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelHandle, Subscription, Task, View,
// ViewContext, ViewHandle, WeakViewHandle,
// actions, div, list, px, serde_json, AnyElement, AnyView, AppContext, AsyncAppContext, Div,
// Entity, EventEmitter, FocusableView, ListOffset, ListScrollHandle, Model, Orientation, Render,
// Subscription, Task, View, ViewContext, WeakView,
// };
// use language::LanguageRegistry;
// use menu::Confirm;
@ -23,10 +17,10 @@
// use project::Fs;
// use rich_text::RichText;
// use serde::{Deserialize, Serialize};
// use settings::SettingsStore;
// use settings::{Settings, SettingsStore};
// use std::sync::Arc;
// use theme::{IconButton, Theme};
// use time::{OffsetDateTime, UtcOffset};
// use ui::{h_stack, v_stack, Avatar, Button, Label};
// use util::{ResultExt, TryFutureExt};
// use workspace::{
// dock::{DockPosition, Panel},
@ -40,19 +34,18 @@
// pub struct ChatPanel {
// client: Arc<Client>,
// channel_store: ModelHandle<ChannelStore>,
// channel_store: Model<ChannelStore>,
// languages: Arc<LanguageRegistry>,
// active_chat: Option<(ModelHandle<ChannelChat>, Subscription)>,
// message_list: ListState<ChatPanel>,
// input_editor: ViewHandle<MessageEditor>,
// channel_select: ViewHandle<Select>,
// list_scroll: ListScrollHandle,
// active_chat: Option<(Model<ChannelChat>, Subscription)>,
// input_editor: View<MessageEditor>,
// local_timezone: UtcOffset,
// fs: Arc<dyn Fs>,
// width: Option<f32>,
// active: bool,
// pending_serialization: Task<Option<()>>,
// subscriptions: Vec<gpui::Subscription>,
// workspace: WeakViewHandle<Workspace>,
// workspace: WeakView<Workspace>,
// is_scrolled_to_bottom: bool,
// has_focus: bool,
// markdown_data: HashMap<ChannelMessageId, RichText>,
@ -70,10 +63,7 @@
// Dismissed,
// }
// actions!(
// chat_panel,
// [LoadMoreMessages, ToggleFocus, OpenChannelNotes, JoinCall]
// );
// actions!(LoadMoreMessages, ToggleFocus, OpenChannelNotes, JoinCall);
// pub fn init(cx: &mut AppContext) {
// cx.add_action(ChatPanel::send);
@ -83,7 +73,7 @@
// }
// impl ChatPanel {
// pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> ViewHandle<Self> {
// pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> View<Self> {
// let fs = workspace.app_state().fs.clone();
// let client = workspace.app_state().client.clone();
// let channel_store = ChannelStore::global(cx);
@ -93,53 +83,46 @@
// MessageEditor::new(
// languages.clone(),
// channel_store.clone(),
// cx.add_view(|cx| {
// Editor::auto_height(
// 4,
// Some(Arc::new(|theme| theme.chat_panel.input_editor.clone())),
// cx,
// )
// }),
// cx.add_view(|cx| Editor::auto_height(4, cx)),
// cx,
// )
// });
// let workspace_handle = workspace.weak_handle();
// let channel_select = cx.add_view(|cx| {
// let channel_store = channel_store.clone();
// let workspace = workspace_handle.clone();
// Select::new(0, cx, {
// move |ix, item_type, is_hovered, cx| {
// Self::render_channel_name(
// &channel_store,
// ix,
// item_type,
// is_hovered,
// workspace,
// cx,
// )
// }
// })
// .with_style(move |cx| {
// let style = &theme::current(cx).chat_panel.channel_select;
// SelectStyle {
// header: Default::default(),
// menu: style.menu,
// }
// })
// });
// // let channel_select = cx.add_view(|cx| {
// // let channel_store = channel_store.clone();
// // let workspace = workspace_handle.clone();
// // Select::new(0, cx, {
// // move |ix, item_type, is_hovered, cx| {
// // Self::render_channel_name(
// // &channel_store,
// // ix,
// // item_type,
// // is_hovered,
// // workspace,
// // cx,
// // )
// // }
// // })
// // .with_style(move |cx| {
// // let style = &cx.theme().chat_panel.channel_select;
// // SelectStyle {
// // header: Default::default(),
// // menu: style.menu,
// // }
// // })
// // });
// let mut message_list =
// ListState::<Self>::new(0, Orientation::Bottom, 10., move |this, ix, cx| {
// this.render_message(ix, cx)
// });
// message_list.set_scroll_handler(|visible_range, count, this, cx| {
// if visible_range.start < MESSAGE_LOADING_THRESHOLD {
// this.load_more_messages(&LoadMoreMessages, cx);
// }
// this.is_scrolled_to_bottom = visible_range.end == count;
// });
// // let mut message_list = ListState::new(0, Orientation::Bottom, 10., move |this, ix, cx| {
// // this.render_message(ix, cx)
// // });
// // message_list.set_scroll_handler(cx.listener(|this, event: &ListScrollEvent, cx| {
// // if event.visible_range.start < MESSAGE_LOADING_THRESHOLD {
// // this.load_more_messages(cx);
// // }
// // this.is_scrolled_to_bottom = event.visible_range.end == event.count;
// // }));
// cx.add_view(|cx| {
// let mut this = Self {
@ -147,11 +130,10 @@
// client,
// channel_store,
// languages,
// list_scroll: ListScrollHandle::new(),
// active_chat: Default::default(),
// pending_serialization: Task::ready(None),
// message_list,
// input_editor,
// channel_select,
// local_timezone: cx.platform().local_timezone(),
// has_focus: false,
// subscriptions: Vec::new(),
@ -204,14 +186,11 @@
// self.is_scrolled_to_bottom
// }
// pub fn active_chat(&self) -> Option<ModelHandle<ChannelChat>> {
// pub fn active_chat(&self) -> Option<Model<ChannelChat>> {
// self.active_chat.as_ref().map(|(chat, _)| chat.clone())
// }
// pub fn load(
// workspace: WeakViewHandle<Workspace>,
// cx: AsyncAppContext,
// ) -> Task<Result<ViewHandle<Self>>> {
// pub fn load(workspace: WeakView<Workspace>, cx: AsyncAppContext) -> Task<Result<View<Self>>> {
// cx.spawn(|mut cx| async move {
// let serialized_panel = if let Some(panel) = cx
// .background()
@ -261,7 +240,7 @@
// });
// }
// fn set_active_chat(&mut self, chat: ModelHandle<ChannelChat>, cx: &mut ViewContext<Self>) {
// fn set_active_chat(&mut self, chat: Model<ChannelChat>, cx: &mut ViewContext<Self>) {
// if self.active_chat.as_ref().map(|e| &e.0) != Some(&chat) {
// let channel_id = chat.read(cx).channel_id;
// {
@ -288,7 +267,7 @@
// fn channel_did_change(
// &mut self,
// _: ModelHandle<ChannelChat>,
// _: Model<ChannelChat>,
// event: &ChannelChatEvent,
// cx: &mut ViewContext<Self>,
// ) {
@ -326,30 +305,29 @@
// }
// }
// fn render_channel(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
// let theme = theme::current(cx);
// Flex::column()
// .with_child(
// ChildView::new(&self.channel_select, cx)
// .contained()
// .with_style(theme.chat_panel.channel_select.container),
// )
// .with_child(self.render_active_channel_messages(&theme))
// .with_child(self.render_input_box(&theme, cx))
// fn render_channel(&self, cx: &mut ViewContext<Self>) -> AnyElement {
// v_stack()
// .child(Label::new(
// self.active_chat.map_or(Default::default(), |c| {
// c.0.read(cx).channel(cx)?.name.into()
// }),
// ))
// .child(self.render_active_channel_messages(cx))
// .child(self.input_editor.to_any())
// .into_any()
// }
// fn render_active_channel_messages(&self, theme: &Arc<Theme>) -> AnyElement<Self> {
// let messages = if self.active_chat.is_some() {
// List::new(self.message_list.clone())
// .contained()
// .with_style(theme.chat_panel.list)
// .into_any()
// fn render_active_channel_messages(&self, cx: &mut ViewContext<Self>) -> AnyElement {
// if self.active_chat.is_some() {
// list(
// Orientation::Bottom,
// 10.,
// cx.listener(move |this, ix, cx| this.render_message(ix, cx)),
// )
// .into_any_element()
// } else {
// Empty::new().into_any()
// };
// messages.flex(1., true).into_any()
// div().into_any_element()
// }
// }
// fn render_message(&mut self, ix: usize, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
@ -388,21 +366,12 @@
// });
// let is_pending = message.is_pending();
// let theme = theme::current(cx);
// let text = self.markdown_data.entry(message.id).or_insert_with(|| {
// Self::render_markdown_with_mentions(&self.languages, self.client.id(), &message)
// });
// let now = OffsetDateTime::now_utc();
// let style = if is_pending {
// &theme.chat_panel.pending_message
// } else if is_continuation {
// &theme.chat_panel.continuation_message
// } else {
// &theme.chat_panel.message
// };
// let belongs_to_user = Some(message.sender.id) == self.client.user_id();
// let message_id_to_remove = if let (ChannelMessageId::Saved(id), true) =
// (message.id, belongs_to_user || is_admin)
@ -412,89 +381,37 @@
// None
// };
// enum MessageBackgroundHighlight {}
// MouseEventHandler::new::<MessageBackgroundHighlight, _>(ix, cx, |state, cx| {
// let container = style.style_for(state);
// if is_continuation {
// Flex::row()
// .with_child(
// text.element(
// theme.editor.syntax.clone(),
// theme.chat_panel.rich_text.clone(),
// cx,
// if is_continuation {
// h_stack()
// .child(text.element(cx))
// .child(render_remove(message_id_to_remove, cx))
// .mb_1()
// .into_any()
// } else {
// v_stack()
// .child(
// h_stack()
// .child(Avatar::data(message.sender.avatar.clone()))
// .child(Label::new(message.sender.github_login.clone()))
// .child(
// Label::new(format_timestamp(
// message.timestamp,
// now,
// self.local_timezone,
// ))
// .flex(1., true),
// )
// .flex(1., true),
// )
// .with_child(render_remove(message_id_to_remove, cx, &theme))
// .contained()
// .with_style(*container)
// .with_margin_bottom(if is_last {
// theme.chat_panel.last_message_bottom_spacing
// } else {
// 0.
// })
// .into_any()
// } else {
// Flex::column()
// .with_child(
// Flex::row()
// .with_child(
// Flex::row()
// .with_child(render_avatar(
// message.sender.avatar.clone(),
// &theme.chat_panel.avatar,
// theme.chat_panel.avatar_container,
// ))
// .with_child(
// Label::new(
// message.sender.github_login.clone(),
// theme.chat_panel.message_sender.text.clone(),
// )
// .contained()
// .with_style(theme.chat_panel.message_sender.container),
// )
// .with_child(
// Label::new(
// format_timestamp(
// message.timestamp,
// now,
// self.local_timezone,
// ),
// theme.chat_panel.message_timestamp.text.clone(),
// )
// .contained()
// .with_style(theme.chat_panel.message_timestamp.container),
// )
// .align_children_center()
// .flex(1., true),
// )
// .with_child(render_remove(message_id_to_remove, cx, &theme))
// .align_children_center(),
// )
// .with_child(
// Flex::row()
// .with_child(
// text.element(
// theme.editor.syntax.clone(),
// theme.chat_panel.rich_text.clone(),
// cx,
// )
// .flex(1., true),
// )
// // Add a spacer to make everything line up
// .with_child(render_remove(None, cx, &theme)),
// )
// .contained()
// .with_style(*container)
// .with_margin_bottom(if is_last {
// theme.chat_panel.last_message_bottom_spacing
// } else {
// 0.
// })
// .into_any()
// }
// })
// .into_any()
// .child(render_remove(message_id_to_remove, cx))
// .align_children_center(),
// )
// .child(
// h_stack()
// .child(text.element(cx))
// .child(render_remove(None, cx)),
// )
// .mb_1()
// .into_any()
// }
// }
// fn render_markdown_with_mentions(
@ -514,127 +431,106 @@
// rich_text::render_markdown(message.body.clone(), &mentions, language_registry, None)
// }
// fn render_input_box(&self, theme: &Arc<Theme>, cx: &AppContext) -> AnyElement<Self> {
// ChildView::new(&self.input_editor, cx)
// .contained()
// .with_style(theme.chat_panel.input_editor.container)
// .into_any()
// }
// // fn render_channel_name(
// // channel_store: &Model<ChannelStore>,
// // ix: usize,
// // item_type: ItemType,
// // is_hovered: bool,
// // workspace: WeakView<Workspace>,
// // cx: &mut ViewContext<Select>,
// // ) -> AnyElement<Select> {
// // let theme = theme::current(cx);
// // let tooltip_style = &theme.tooltip;
// // let theme = &theme.chat_panel;
// // let style = match (&item_type, is_hovered) {
// // (ItemType::Header, _) => &theme.channel_select.header,
// // (ItemType::Selected, _) => &theme.channel_select.active_item,
// // (ItemType::Unselected, false) => &theme.channel_select.item,
// // (ItemType::Unselected, true) => &theme.channel_select.hovered_item,
// // };
// fn render_channel_name(
// channel_store: &ModelHandle<ChannelStore>,
// ix: usize,
// item_type: ItemType,
// is_hovered: bool,
// workspace: WeakViewHandle<Workspace>,
// cx: &mut ViewContext<Select>,
// ) -> AnyElement<Select> {
// let theme = theme::current(cx);
// let tooltip_style = &theme.tooltip;
// let theme = &theme.chat_panel;
// let style = match (&item_type, is_hovered) {
// (ItemType::Header, _) => &theme.channel_select.header,
// (ItemType::Selected, _) => &theme.channel_select.active_item,
// (ItemType::Unselected, false) => &theme.channel_select.item,
// (ItemType::Unselected, true) => &theme.channel_select.hovered_item,
// };
// // let channel = &channel_store.read(cx).channel_at(ix).unwrap();
// // let channel_id = channel.id;
// let channel = &channel_store.read(cx).channel_at(ix).unwrap();
// let channel_id = channel.id;
// // let mut row = Flex::row()
// // .with_child(
// // Label::new("#".to_string(), style.hash.text.clone())
// // .contained()
// // .with_style(style.hash.container),
// // )
// // .with_child(Label::new(channel.name.clone(), style.name.clone()));
// let mut row = Flex::row()
// .with_child(
// Label::new("#".to_string(), style.hash.text.clone())
// .contained()
// .with_style(style.hash.container),
// )
// .with_child(Label::new(channel.name.clone(), style.name.clone()));
// // if matches!(item_type, ItemType::Header) {
// // row.add_children([
// // MouseEventHandler::new::<OpenChannelNotes, _>(0, cx, |mouse_state, _| {
// // render_icon_button(theme.icon_button.style_for(mouse_state), "icons/file.svg")
// // })
// // .on_click(MouseButton::Left, move |_, _, cx| {
// // if let Some(workspace) = workspace.upgrade(cx) {
// // ChannelView::open(channel_id, workspace, cx).detach();
// // }
// // })
// // .with_tooltip::<OpenChannelNotes>(
// // channel_id as usize,
// // "Open Notes",
// // Some(Box::new(OpenChannelNotes)),
// // tooltip_style.clone(),
// // cx,
// // )
// // .flex_float(),
// // MouseEventHandler::new::<ActiveCall, _>(0, cx, |mouse_state, _| {
// // render_icon_button(
// // theme.icon_button.style_for(mouse_state),
// // "icons/speaker-loud.svg",
// // )
// // })
// // .on_click(MouseButton::Left, move |_, _, cx| {
// // ActiveCall::global(cx)
// // .update(cx, |call, cx| call.join_channel(channel_id, cx))
// // .detach_and_log_err(cx);
// // })
// // .with_tooltip::<ActiveCall>(
// // channel_id as usize,
// // "Join Call",
// // Some(Box::new(JoinCall)),
// // tooltip_style.clone(),
// // cx,
// // )
// // .flex_float(),
// // ]);
// // }
// if matches!(item_type, ItemType::Header) {
// row.add_children([
// MouseEventHandler::new::<OpenChannelNotes, _>(0, cx, |mouse_state, _| {
// render_icon_button(theme.icon_button.style_for(mouse_state), "icons/file.svg")
// })
// .on_click(MouseButton::Left, move |_, _, cx| {
// if let Some(workspace) = workspace.upgrade(cx) {
// ChannelView::open(channel_id, workspace, cx).detach();
// }
// })
// .with_tooltip::<OpenChannelNotes>(
// channel_id as usize,
// "Open Notes",
// Some(Box::new(OpenChannelNotes)),
// tooltip_style.clone(),
// cx,
// )
// .flex_float(),
// MouseEventHandler::new::<ActiveCall, _>(0, cx, |mouse_state, _| {
// render_icon_button(
// theme.icon_button.style_for(mouse_state),
// "icons/speaker-loud.svg",
// )
// })
// .on_click(MouseButton::Left, move |_, _, cx| {
// ActiveCall::global(cx)
// .update(cx, |call, cx| call.join_channel(channel_id, cx))
// .detach_and_log_err(cx);
// })
// .with_tooltip::<ActiveCall>(
// channel_id as usize,
// "Join Call",
// Some(Box::new(JoinCall)),
// tooltip_style.clone(),
// cx,
// )
// .flex_float(),
// ]);
// }
// // row.align_children_center()
// // .contained()
// // .with_style(style.container)
// // .into_any()
// // }
// row.align_children_center()
// .contained()
// .with_style(style.container)
// .into_any()
// }
// fn render_sign_in_prompt(
// &self,
// theme: &Arc<Theme>,
// cx: &mut ViewContext<Self>,
// ) -> AnyElement<Self> {
// fn render_sign_in_prompt(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
// enum SignInPromptLabel {}
// MouseEventHandler::new::<SignInPromptLabel, _>(0, cx, |mouse_state, _| {
// Label::new(
// "Sign in to use chat".to_string(),
// theme
// .chat_panel
// .sign_in_prompt
// .style_for(mouse_state)
// .clone(),
// )
// })
// .with_cursor_style(CursorStyle::PointingHand)
// .on_click(MouseButton::Left, move |_, this, cx| {
// let client = this.client.clone();
// cx.spawn(|this, mut cx| async move {
// if client
// .authenticate_and_connect(true, &cx)
// .log_err()
// .await
// .is_some()
// {
// this.update(&mut cx, |this, cx| {
// if cx.handle().is_focused(cx) {
// cx.focus(&this.input_editor);
// }
// })
// .ok();
// }
// Button::new("sign-in", "Sign in to use chat")
// .on_click(move |_, this, cx| {
// let client = this.client.clone();
// cx.spawn(|this, mut cx| async move {
// if client
// .authenticate_and_connect(true, &cx)
// .log_err()
// .await
// .is_some()
// {
// this.update(&mut cx, |this, cx| {
// if cx.handle().is_focused(cx) {
// cx.focus(&this.input_editor);
// }
// })
// .ok();
// }
// })
// .detach();
// })
// .detach();
// })
// .aligned()
// .into_any()
// .aligned()
// .into_any()
// }
// fn send(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
@ -700,9 +596,9 @@
// {
// this.update(&mut cx, |this, cx| {
// if this.active_chat.as_ref().map_or(false, |(c, _)| *c == chat) {
// this.message_list.scroll_to(ListOffset {
// this.list_scroll.scroll_to(ListOffset {
// item_ix,
// offset_in_item: 0.,
// offset_in_item: px(0.0),
// });
// cx.notify();
// }
@ -733,11 +629,7 @@
// }
// }
// fn render_remove(
// message_id_to_remove: Option<u64>,
// cx: &mut ViewContext<'_, '_, ChatPanel>,
// theme: &Arc<Theme>,
// ) -> AnyElement<ChatPanel> {
// fn render_remove(message_id_to_remove: Option<u64>, cx: &mut ViewContext<ChatPanel>) -> AnyElement {
// enum DeleteMessage {}
// message_id_to_remove
@ -773,49 +665,31 @@
// })
// }
// impl Entity for ChatPanel {
// type Event = Event;
// impl EventEmitter<Event> for ChatPanel {}
// impl Render for ChatPanel {
// type Element = Div;
// fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
// div()
// .child(if self.client.user_id().is_some() {
// self.render_channel(cx)
// } else {
// self.render_sign_in_prompt(cx)
// })
// .min_w(px(150.))
// }
// }
// impl View for ChatPanel {
// fn ui_name() -> &'static str {
// "ChatPanel"
// }
// fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
// let theme = theme::current(cx);
// let element = if self.client.user_id().is_some() {
// self.render_channel(cx)
// } else {
// self.render_sign_in_prompt(&theme, cx)
// };
// element
// .contained()
// .with_style(theme.chat_panel.container)
// .constrained()
// .with_min_width(150.)
// .into_any()
// }
// fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
// self.has_focus = true;
// if matches!(
// *self.client.status().borrow(),
// client::Status::Connected { .. }
// ) {
// let editor = self.input_editor.read(cx).editor.clone();
// cx.focus(&editor);
// }
// }
// fn focus_out(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {
// self.has_focus = false;
// impl FocusableView for ChatPanel {
// fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
// self.input_editor.read(cx).focus_handle(cx)
// }
// }
// impl Panel for ChatPanel {
// fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
// settings::get::<ChatPanelSettings>(cx).dock
// ChatPanelSettings::get_global(cx).dock
// }
// fn position_is_valid(&self, position: DockPosition) -> bool {
@ -830,7 +704,7 @@
// fn size(&self, cx: &gpui::WindowContext) -> f32 {
// self.width
// .unwrap_or_else(|| settings::get::<ChatPanelSettings>(cx).default_width)
// .unwrap_or_else(|| ChatPanelSettings::get_global(cx).default_width)
// }
// fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>) {
@ -849,29 +723,16 @@
// }
// }
// fn icon_path(&self, cx: &gpui::WindowContext) -> Option<&'static str> {
// (settings::get::<ChatPanelSettings>(cx).button && is_channels_feature_enabled(cx))
// .then(|| "icons/conversations.svg")
// fn persistent_name() -> &'static str {
// todo!()
// }
// fn icon_tooltip(&self) -> (String, Option<Box<dyn gpui::Action>>) {
// ("Chat Panel".to_string(), Some(Box::new(ToggleFocus)))
// fn icon(&self, cx: &ui::prelude::WindowContext) -> Option<ui::Icon> {
// Some(ui::Icon::MessageBubbles)
// }
// fn should_change_position_on_event(event: &Self::Event) -> bool {
// matches!(event, Event::DockPositionChanged)
// }
// fn should_close_on_event(event: &Self::Event) -> bool {
// matches!(event, Event::Dismissed)
// }
// fn has_focus(&self, _cx: &gpui::WindowContext) -> bool {
// self.has_focus
// }
// fn is_focus_event(event: &Self::Event) -> bool {
// matches!(event, Event::Focus)
// fn toggle_action(&self) -> Box<dyn gpui::Action> {
// todo!()
// }
// }

View File

@ -2,14 +2,12 @@ use channel::{ChannelId, ChannelMembership, ChannelStore, MessageParams};
use client::UserId;
use collections::HashMap;
use editor::{AnchorRangeExt, Editor};
use gpui::{
elements::ChildView, AnyElement, AsyncAppContext, Element, Entity, ModelHandle, Task, View,
ViewContext, ViewHandle, WeakViewHandle,
};
use gpui::{AnyView, AsyncAppContext, Model, Render, 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 +17,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 +28,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| {
@ -132,7 +130,7 @@ impl MessageEditor {
fn on_buffer_event(
&mut self,
buffer: ModelHandle<Buffer>,
buffer: Model<Buffer>,
event: &language::Event,
cx: &mut ViewContext<Self>,
) {
@ -146,7 +144,7 @@ impl MessageEditor {
}
async fn find_mentions(
this: WeakViewHandle<MessageEditor>,
this: WeakView<MessageEditor>,
buffer: BufferSnapshot,
mut cx: AsyncAppContext,
) {
@ -180,11 +178,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;
@ -194,19 +188,11 @@ impl MessageEditor {
}
}
impl Entity for MessageEditor {
type Event = ();
}
impl Render for MessageEditor {
type Element = AnyView;
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);
}
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
self.editor.to_any()
}
}
@ -297,7 +283,7 @@ mod tests {
MessageEditor::new(
language_registry,
ChannelStore::global(cx),
cx.add_view(|cx| Editor::auto_height(4, None, cx)),
cx.add_view(|cx| Editor::auto_height(4, cx)),
cx,
)
});

View File

@ -0,0 +1,493 @@
use crate::{
px, AnyElement, AvailableSpace, BorrowAppContext, DispatchPhase, Element, IntoElement, Pixels,
Point, ScrollWheelEvent, Size, Style, StyleRefinement, ViewContext, WindowContext,
};
use collections::VecDeque;
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>,
orientation: Orientation,
overdraw: Pixels,
#[allow(clippy::type_complexity)]
scroll_handler: Option<Box<dyn FnMut(&ListScrollEvent, &mut WindowContext)>>,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Orientation {
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, V>(
element_count: usize,
orientation: Orientation,
overdraw: Pixels,
cx: &mut ViewContext<V>,
mut render_item: F,
) -> Self
where
F: 'static + FnMut(&mut V, usize, &mut ViewContext<V>) -> AnyElement,
V: 'static,
{
let mut items = SumTree::new();
items.extend((0..element_count).map(|_| ListItem::Unrendered), &());
let view = cx.view().clone();
Self(Rc::new(RefCell::new(StateInner {
last_layout_width: None,
render_item: Box::new(move |ix, cx| {
view.update(cx, |view, cx| render_item(view, ix, cx))
}),
items,
logical_scroll_top: None,
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(
&mut 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.orientation == Orientation::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.orientation {
Orientation::Top => ListOffset {
item_ix: 0,
offset_in_item: px(0.),
},
Orientation::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 style = Style::from(self.style.clone());
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.orientation {
Orientation::Top => {
scroll_top.offset_in_item = scroll_top.offset_in_item.max(px(0.));
state.logical_scroll_top = Some(scroll_top);
}
Orientation::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 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()
}
}

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::*;

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
}

View File

@ -72,6 +72,7 @@ pub use view::*;
pub use window::*;
use derive_more::{Deref, DerefMut};
use serde::{Deserialize, Serialize};
use std::{
any::{Any, TypeId},
borrow::{Borrow, BorrowMut},
@ -248,3 +249,22 @@ impl<T: Into<ArcCow<'static, str>>> From<T> for SharedString {
Self(value.into())
}
}
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))
}
}

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;