From 5dee8914ed1b65df7750bbb0ac3184280294db64 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 19 Jan 2024 15:27:05 -0700 Subject: [PATCH 1/6] Make chat font sizes consistently small --- crates/collab_ui/src/chat_panel.rs | 12 +++++------- crates/collab_ui/src/chat_panel/message_editor.rs | 4 ++-- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/crates/collab_ui/src/chat_panel.rs b/crates/collab_ui/src/chat_panel.rs index ba8c71fb1e..2bb16637ad 100644 --- a/crates/collab_ui/src/chat_panel.rs +++ b/crates/collab_ui/src/chat_panel.rs @@ -349,15 +349,13 @@ impl ChatPanel { .when(!is_continuation_from_previous, |this| { this.pt_3().child( h_flex() - .child( - div().absolute().child( - Avatar::new(message.sender.avatar_uri.clone()) - .size(cx.rem_size() * 1.5), - ), - ) + .text_ui_sm() + .child(div().absolute().child( + Avatar::new(message.sender.avatar_uri.clone()).size(cx.rem_size()), + )) .child( div() - .pl(cx.rem_size() * 1.5 + px(6.0)) + .pl(cx.rem_size() + px(6.0)) .pr(px(8.0)) .font_weight(FontWeight::BOLD) .child(Label::new(message.sender.github_login.clone())), diff --git a/crates/collab_ui/src/chat_panel/message_editor.rs b/crates/collab_ui/src/chat_panel/message_editor.rs index 7999db529a..43b432cfb2 100644 --- a/crates/collab_ui/src/chat_panel/message_editor.rs +++ b/crates/collab_ui/src/chat_panel/message_editor.rs @@ -13,7 +13,7 @@ use lazy_static::lazy_static; use project::search::SearchQuery; use settings::Settings; use theme::ThemeSettings; -use ui::prelude::*; +use ui::{prelude::*, UiTextSize}; const MENTIONS_DEBOUNCE_INTERVAL: Duration = Duration::from_millis(50); @@ -216,7 +216,7 @@ impl Render for MessageEditor { }, font_family: settings.ui_font.family.clone(), font_features: settings.ui_font.features, - font_size: rems(0.875).into(), + font_size: UiTextSize::Small.rems().into(), font_weight: FontWeight::NORMAL, font_style: FontStyle::Normal, line_height: relative(1.3).into(), From 23d991962a91fc7519e418709c43c4e24c121f54 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 19 Jan 2024 16:59:17 -0700 Subject: [PATCH 2/6] Link previews in chat --- Cargo.lock | 1 + crates/gpui/src/elements/div.rs | 6 +- crates/gpui/src/elements/text.rs | 128 ++++++++++++++++++++++++++-- crates/rich_text/Cargo.toml | 1 + crates/rich_text/src/rich_text.rs | 13 +++ crates/ui/src/components/tooltip.rs | 91 +++++++++++++++----- 6 files changed, 209 insertions(+), 31 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 010e7763e4..e513df4e63 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6144,6 +6144,7 @@ dependencies = [ "smol", "sum_tree", "theme", + "ui", "util", ] diff --git a/crates/gpui/src/elements/div.rs b/crates/gpui/src/elements/div.rs index aa912eadbe..70b2df10cc 100644 --- a/crates/gpui/src/elements/div.rs +++ b/crates/gpui/src/elements/div.rs @@ -24,7 +24,7 @@ use taffy::style::Overflow; use util::ResultExt; const DRAG_THRESHOLD: f64 = 2.; -const TOOLTIP_DELAY: Duration = Duration::from_millis(500); +pub(crate) const TOOLTIP_DELAY: Duration = Duration::from_millis(500); pub struct GroupStyle { pub group: SharedString, @@ -1718,8 +1718,8 @@ pub struct InteractiveElementState { } pub struct ActiveTooltip { - tooltip: Option, - _task: Option>, + pub(crate) tooltip: Option, + pub(crate) _task: Option>, } /// Whether or not the element or a group that contains it is clicked by the mouse. diff --git a/crates/gpui/src/elements/text.rs b/crates/gpui/src/elements/text.rs index f72b7c6fa9..8ab847bf6b 100644 --- a/crates/gpui/src/elements/text.rs +++ b/crates/gpui/src/elements/text.rs @@ -1,12 +1,18 @@ use crate::{ - Bounds, DispatchPhase, Element, ElementId, HighlightStyle, IntoElement, LayoutId, - MouseDownEvent, MouseUpEvent, Pixels, Point, SharedString, Size, TextRun, TextStyle, - WhiteSpace, WindowContext, WrappedLine, + ActiveTooltip, AnyTooltip, AnyView, Bounds, DispatchPhase, Element, ElementId, HighlightStyle, + IntoElement, LayoutId, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Point, + SharedString, Size, TextRun, TextStyle, WhiteSpace, WindowContext, WrappedLine, TOOLTIP_DELAY, }; use anyhow::anyhow; use parking_lot::{Mutex, MutexGuard}; use smallvec::SmallVec; -use std::{cell::Cell, mem, ops::Range, rc::Rc, sync::Arc}; +use std::{ + cell::{Cell, RefCell}, + mem, + ops::Range, + rc::Rc, + sync::Arc, +}; use util::ResultExt; impl Element for &'static str { @@ -289,6 +295,8 @@ pub struct InteractiveText { text: StyledText, click_listener: Option], InteractiveTextClickEvent, &mut WindowContext<'_>)>>, + hover_listener: Option, MouseMoveEvent, &mut WindowContext<'_>)>>, + tooltip_builder: Option) -> Option>>, clickable_ranges: Vec>, } @@ -300,18 +308,25 @@ struct InteractiveTextClickEvent { pub struct InteractiveTextState { text_state: TextState, mouse_down_index: Rc>>, + hovered_index: Rc>>, + active_tooltip: Rc>>, } +/// InteractiveTest is a wrapper around StyledText that adds mouse interactions. impl InteractiveText { pub fn new(id: impl Into, text: StyledText) -> Self { Self { element_id: id.into(), text, click_listener: None, + hover_listener: None, + tooltip_builder: None, clickable_ranges: Vec::new(), } } + /// on_click is called when the user clicks on one of the given ranges, passing the index of + /// the clicked range. pub fn on_click( mut self, ranges: Vec>, @@ -328,6 +343,25 @@ impl InteractiveText { self.clickable_ranges = ranges; self } + + /// on_hover is called when the mouse moves over a character within the text, passing the + /// index of the hovered character, or None if the mouse leaves the text. + pub fn on_hover( + mut self, + listener: impl Fn(Option, MouseMoveEvent, &mut WindowContext<'_>) + 'static, + ) -> Self { + self.hover_listener = Some(Box::new(listener)); + self + } + + /// tooltip lets you specify a tooltip for a given character index in the string. + pub fn tooltip( + mut self, + builder: impl Fn(usize, &mut WindowContext<'_>) -> Option + 'static, + ) -> Self { + self.tooltip_builder = Some(Rc::new(builder)); + self + } } impl Element for InteractiveText { @@ -339,13 +373,18 @@ impl Element for InteractiveText { cx: &mut WindowContext, ) -> (LayoutId, Self::State) { if let Some(InteractiveTextState { - mouse_down_index, .. + mouse_down_index, + hovered_index, + active_tooltip, + .. }) = state { let (layout_id, text_state) = self.text.request_layout(None, cx); let element_state = InteractiveTextState { text_state, mouse_down_index, + hovered_index, + active_tooltip, }; (layout_id, element_state) } else { @@ -353,6 +392,8 @@ impl Element for InteractiveText { let element_state = InteractiveTextState { text_state, mouse_down_index: Rc::default(), + hovered_index: Rc::default(), + active_tooltip: Rc::default(), }; (layout_id, element_state) } @@ -408,6 +449,83 @@ impl Element for InteractiveText { }); } } + if let Some(hover_listener) = self.hover_listener.take() { + let text_state = state.text_state.clone(); + let hovered_index = state.hovered_index.clone(); + cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| { + if phase == DispatchPhase::Bubble { + let current = hovered_index.get(); + let updated = text_state.index_for_position(bounds, event.position); + if current != updated { + hovered_index.set(updated); + hover_listener(updated, event.clone(), cx); + cx.refresh(); + } + } + }); + } + if let Some(tooltip_builder) = self.tooltip_builder.clone() { + let active_tooltip = state.active_tooltip.clone(); + let pending_mouse_down = state.mouse_down_index.clone(); + let text_state = state.text_state.clone(); + + cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| { + let position = text_state.index_for_position(bounds, event.position); + let is_hovered = position.is_some() && pending_mouse_down.get().is_none(); + if !is_hovered { + active_tooltip.take(); + return; + } + let position = position.unwrap(); + + if phase != DispatchPhase::Bubble { + return; + } + + if active_tooltip.borrow().is_none() { + let task = cx.spawn({ + let active_tooltip = active_tooltip.clone(); + let tooltip_builder = tooltip_builder.clone(); + + move |mut cx| async move { + cx.background_executor().timer(TOOLTIP_DELAY).await; + cx.update(|_, cx| { + let new_tooltip = + tooltip_builder(position, cx).map(|tooltip| ActiveTooltip { + tooltip: Some(AnyTooltip { + view: tooltip, + cursor_offset: cx.mouse_position(), + }), + _task: None, + }); + *active_tooltip.borrow_mut() = new_tooltip; + cx.refresh(); + }) + .ok(); + } + }); + *active_tooltip.borrow_mut() = Some(ActiveTooltip { + tooltip: None, + _task: Some(task), + }); + } + }); + + let active_tooltip = state.active_tooltip.clone(); + cx.on_mouse_event(move |_: &MouseDownEvent, _, _| { + active_tooltip.take(); + }); + + if let Some(tooltip) = state + .active_tooltip + .clone() + .borrow() + .as_ref() + .and_then(|at| at.tooltip.clone()) + { + cx.set_tooltip(tooltip); + } + } self.text.paint(bounds, &mut state.text_state, cx) } diff --git a/crates/rich_text/Cargo.toml b/crates/rich_text/Cargo.toml index 609272cdf3..efec13c760 100644 --- a/crates/rich_text/Cargo.toml +++ b/crates/rich_text/Cargo.toml @@ -21,6 +21,7 @@ sum_tree = { path = "../sum_tree" } theme = { path = "../theme" } language = { path = "../language" } util = { path = "../util" } +ui = { path = "../ui" } anyhow.workspace = true futures.workspace = true lazy_static.workspace = true diff --git a/crates/rich_text/src/rich_text.rs b/crates/rich_text/src/rich_text.rs index 8bc0d73fd4..dc626ccdd1 100644 --- a/crates/rich_text/src/rich_text.rs +++ b/crates/rich_text/src/rich_text.rs @@ -6,6 +6,7 @@ use gpui::{ use language::{HighlightId, Language, LanguageRegistry}; use std::{ops::Range, sync::Arc}; use theme::ActiveTheme; +use ui::LinkPreview; use util::RangeExt; #[derive(Debug, Clone, PartialEq, Eq)] @@ -84,6 +85,18 @@ impl RichText { let link_urls = self.link_urls.clone(); move |ix, cx| cx.open_url(&link_urls[ix]) }) + .tooltip({ + let link_ranges = self.link_ranges.clone(); + let link_urls = self.link_urls.clone(); + move |idx, cx| { + for (ix, range) in link_ranges.iter().enumerate() { + if range.contains(&idx) { + return Some(LinkPreview::new(&link_urls[ix], cx)); + } + } + None + } + }) .into_any_element() } } diff --git a/crates/ui/src/components/tooltip.rs b/crates/ui/src/components/tooltip.rs index f76085daa3..6db1804740 100644 --- a/crates/ui/src/components/tooltip.rs +++ b/crates/ui/src/components/tooltip.rs @@ -69,29 +69,74 @@ impl Tooltip { impl Render for Tooltip { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone(); - overlay().child( - // padding to avoid mouse cursor - div().pl_2().pt_2p5().child( - v_flex() - .elevation_2(cx) - .font(ui_font) - .text_ui() - .text_color(cx.theme().colors().text) - .py_1() - .px_2() - .child( - h_flex() - .gap_4() - .child(self.title.clone()) - .when_some(self.key_binding.clone(), |this, key_binding| { - this.justify_between().child(key_binding) - }), - ) - .when_some(self.meta.clone(), |this, meta| { - this.child(Label::new(meta).size(LabelSize::Small).color(Color::Muted)) + tooltip_container(cx, |el, _| { + el.child( + h_flex() + .gap_4() + .child(self.title.clone()) + .when_some(self.key_binding.clone(), |this, key_binding| { + this.justify_between().child(key_binding) }), - ), - ) + ) + .when_some(self.meta.clone(), |this, meta| { + this.child(Label::new(meta).size(LabelSize::Small).color(Color::Muted)) + }) + }) + } +} + +fn tooltip_container( + cx: &mut ViewContext, + f: impl FnOnce(Div, &mut ViewContext) -> Div, +) -> impl IntoElement { + let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone(); + overlay().child( + // padding to avoid mouse cursor + div().pl_2().pt_2p5().child( + v_flex() + .elevation_2(cx) + .font(ui_font) + .text_ui() + .text_color(cx.theme().colors().text) + .py_1() + .px_2() + .map(|el| f(el, cx)), + ), + ) +} + +pub struct LinkPreview { + link: SharedString, +} + +impl LinkPreview { + pub fn new(url: &str, cx: &mut WindowContext) -> AnyView { + let mut wrapped_url = String::new(); + for (i, ch) in url.chars().enumerate() { + if i == 500 { + wrapped_url.push('…'); + break; + } + if i % 100 == 0 && i != 0 { + wrapped_url.push('\n'); + } + wrapped_url.push(ch); + } + cx.new_view(|_cx| LinkPreview { + link: wrapped_url.into(), + }) + .into() + } +} + +impl Render for LinkPreview { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + tooltip_container(cx, |el, _| { + el.child( + Label::new(self.link.clone()) + .size(LabelSize::XSmall) + .color(Color::Muted), + ) + }) } } From 42c81354fa1a20b6b37ad4246296b1b69e3cd8c7 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Sat, 20 Jan 2024 11:12:51 -0700 Subject: [PATCH 3/6] Fix placeholder height --- crates/collab_ui/src/chat_panel.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/collab_ui/src/chat_panel.rs b/crates/collab_ui/src/chat_panel.rs index 2bb16637ad..ceace9d3ad 100644 --- a/crates/collab_ui/src/chat_panel.rs +++ b/crates/collab_ui/src/chat_panel.rs @@ -595,7 +595,7 @@ impl Render for ChatPanel { el.child( div() .rounded_md() - .h_7() + .h_6() .w_full() .bg(cx.theme().colors().editor_background), ) From 72689b08ccf2064d089ad304374c613bf0f2b84d Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Sat, 20 Jan 2024 13:31:19 -0700 Subject: [PATCH 4/6] shift-enter for newline in chat --- assets/keymaps/default.json | 1 + 1 file changed, 1 insertion(+) diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index cd353d7767..8679296733 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -183,6 +183,7 @@ "context": "Editor && mode == auto_height", "bindings": { "ctrl-enter": "editor::Newline", + "shift-enter": "editor::Newline", "ctrl-shift-enter": "editor::NewlineBelow" } }, From 778856c10110eb34ea0436bd6867bc4155e83158 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Sat, 20 Jan 2024 13:32:31 -0700 Subject: [PATCH 5/6] Add a setting "use_autoclose" to control autoclose Also disable autoclose for Chat --- assets/settings/default.json | 3 +++ crates/collab_ui/src/chat_panel/message_editor.rs | 1 + crates/editor/src/editor.rs | 13 ++++++++++++- crates/language/src/language_settings.rs | 8 ++++++++ 4 files changed, 24 insertions(+), 1 deletion(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 87cf0517a2..a920a9c68a 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -72,6 +72,9 @@ // Whether to use additional LSP queries to format (and amend) the code after // every "trigger" symbol input, defined by LSP server capabilities. "use_on_type_format": true, + // Whether to automatically type closing characters for you. For example, + // when you type (, Zed will automatically add a closing ) at the correct position. + "use_autoclose": true, // Controls whether copilot provides suggestion immediately // or waits for a `copilot::Toggle` "show_copilot_suggestions": true, diff --git a/crates/collab_ui/src/chat_panel/message_editor.rs b/crates/collab_ui/src/chat_panel/message_editor.rs index 43b432cfb2..41d2c26f49 100644 --- a/crates/collab_ui/src/chat_panel/message_editor.rs +++ b/crates/collab_ui/src/chat_panel/message_editor.rs @@ -40,6 +40,7 @@ impl MessageEditor { ) -> Self { editor.update(cx, |editor, cx| { editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx); + editor.set_use_autoclose(false); }); let buffer = editor diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index b31dd54208..e6a27bc109 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -408,6 +408,7 @@ pub struct Editor { style: Option, editor_actions: Vec)>>, show_copilot_suggestions: bool, + use_autoclose: bool, } pub struct EditorSnapshot { @@ -1603,6 +1604,7 @@ impl Editor { keymap_context_layers: Default::default(), input_enabled: true, read_only: false, + use_autoclose: true, leader_peer_id: None, remote_id: None, hover_state: Default::default(), @@ -1880,6 +1882,10 @@ impl Editor { self.read_only = read_only; } + pub fn set_use_autoclose(&mut self, autoclose: bool) { + self.use_autoclose = autoclose; + } + pub fn set_show_copilot_suggestions(&mut self, show_copilot_suggestions: bool) { self.show_copilot_suggestions = show_copilot_suggestions; } @@ -2478,7 +2484,12 @@ impl Editor { ), &bracket_pair.start[..prefix_len], )); - if following_text_allows_autoclose && preceding_text_matches_prefix { + let autoclose = self.use_autoclose + && snapshot.settings_at(selection.start, cx).use_autoclose; + if autoclose + && following_text_allows_autoclose + && preceding_text_matches_prefix + { let anchor = snapshot.anchor_before(selection.end); new_selections.push((selection.map(|_| anchor), text.len())); new_autoclose_regions.push(( diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index 292e2ad9dc..cdd96ad6ab 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -91,6 +91,8 @@ pub struct LanguageSettings { pub extend_comment_on_newline: bool, /// Inlay hint related settings. pub inlay_hints: InlayHintSettings, + /// Whether to automatically close brackets. + pub use_autoclose: bool, } /// The settings for [GitHub Copilot](https://github.com/features/copilot). @@ -208,6 +210,11 @@ pub struct LanguageSettingsContent { /// Inlay hint related settings. #[serde(default)] pub inlay_hints: Option, + /// Whether to automatically type closing characters for you. For example, + /// when you type (, Zed will automatically add a closing ) at the correct position. + /// + /// Default: true + pub use_autoclose: Option, } /// The contents of the GitHub Copilot settings. @@ -540,6 +547,7 @@ fn merge_settings(settings: &mut LanguageSettings, src: &LanguageSettingsContent merge(&mut settings.tab_size, src.tab_size); merge(&mut settings.hard_tabs, src.hard_tabs); merge(&mut settings.soft_wrap, src.soft_wrap); + merge(&mut settings.use_autoclose, src.use_autoclose); merge(&mut settings.show_wrap_guides, src.show_wrap_guides); merge(&mut settings.wrap_guides, src.wrap_guides.clone()); From 6e1f44163ef1b4fa1deea2ea694005b6d0b66fd7 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Sat, 20 Jan 2024 13:45:44 -0700 Subject: [PATCH 6/6] Render newlines as newlines in chat --- crates/rich_text/src/rich_text.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/rich_text/src/rich_text.rs b/crates/rich_text/src/rich_text.rs index dc626ccdd1..0b980ed37d 100644 --- a/crates/rich_text/src/rich_text.rs +++ b/crates/rich_text/src/rich_text.rs @@ -250,7 +250,7 @@ pub fn render_markdown_mut( _ => {} }, Event::HardBreak => text.push('\n'), - Event::SoftBreak => text.push(' '), + Event::SoftBreak => text.push('\n'), _ => {} } }