Cargo.lock 🔗
@@ -6145,6 +6145,7 @@ dependencies = [
"smol",
"sum_tree",
"theme",
+ "ui",
"util",
]
Joseph T. Lyons created
Cargo.lock | 1
assets/settings/default.json | 3
crates/collab_ui/src/chat_panel.rs | 14 -
crates/collab_ui/src/chat_panel/message_editor.rs | 5
crates/editor/src/editor.rs | 13 +
crates/gpui/src/elements/div.rs | 6
crates/gpui/src/elements/text.rs | 128 ++++++++++++++++
crates/language/src/language_settings.rs | 8 +
crates/rich_text/Cargo.toml | 1
crates/rich_text/src/rich_text.rs | 15 +
crates/ui/src/components/tooltip.rs | 91 +++++++++---
11 files changed, 242 insertions(+), 43 deletions(-)
@@ -6145,6 +6145,7 @@ dependencies = [
"smol",
"sum_tree",
"theme",
+ "ui",
"util",
]
@@ -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,
@@ -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())),
@@ -597,7 +595,7 @@ impl Render for ChatPanel {
el.child(
div()
.rounded_md()
- .h_7()
+ .h_6()
.w_full()
.bg(cx.theme().colors().editor_background),
)
@@ -18,7 +18,7 @@ use project::search::SearchQuery;
use settings::Settings;
use std::{sync::Arc, time::Duration};
use theme::ThemeSettings;
-use ui::prelude::*;
+use ui::{prelude::*, UiTextSize};
const MENTIONS_DEBOUNCE_INTERVAL: Duration = Duration::from_millis(50);
@@ -83,6 +83,7 @@ impl MessageEditor {
let this = cx.view().downgrade();
editor.update(cx, |editor, cx| {
editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
+ editor.set_use_autoclose(false);
editor.set_completion_provider(Box::new(MessageEditorCompletionProvider(this)));
});
@@ -325,7 +326,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(),
@@ -409,6 +409,7 @@ pub struct Editor {
style: Option<EditorStyle>,
editor_actions: Vec<Box<dyn Fn(&mut ViewContext<Self>)>>,
show_copilot_suggestions: bool,
+ use_autoclose: bool,
}
pub struct EditorSnapshot {
@@ -1411,6 +1412,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(),
@@ -1692,6 +1694,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;
}
@@ -2290,7 +2296,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((
@@ -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<AnyTooltip>,
- _task: Option<Task<()>>,
+ pub(crate) tooltip: Option<AnyTooltip>,
+ pub(crate) _task: Option<Task<()>>,
}
/// Whether or not the element or a group that contains it is clicked by the mouse.
@@ -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<Box<dyn Fn(&[Range<usize>], InteractiveTextClickEvent, &mut WindowContext<'_>)>>,
+ hover_listener: Option<Box<dyn Fn(Option<usize>, MouseMoveEvent, &mut WindowContext<'_>)>>,
+ tooltip_builder: Option<Rc<dyn Fn(usize, &mut WindowContext<'_>) -> Option<AnyView>>>,
clickable_ranges: Vec<Range<usize>>,
}
@@ -300,18 +308,25 @@ struct InteractiveTextClickEvent {
pub struct InteractiveTextState {
text_state: TextState,
mouse_down_index: Rc<Cell<Option<usize>>>,
+ hovered_index: Rc<Cell<Option<usize>>>,
+ active_tooltip: Rc<RefCell<Option<ActiveTooltip>>>,
}
+/// InteractiveTest is a wrapper around StyledText that adds mouse interactions.
impl InteractiveText {
pub fn new(id: impl Into<ElementId>, 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<Range<usize>>,
@@ -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<usize>, 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<AnyView> + '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)
}
@@ -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<InlayHintSettings>,
+ /// 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<bool>,
}
/// 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());
@@ -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
@@ -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()
}
}
@@ -237,7 +250,7 @@ pub fn render_markdown_mut(
_ => {}
},
Event::HardBreak => text.push('\n'),
- Event::SoftBreak => text.push(' '),
+ Event::SoftBreak => text.push('\n'),
_ => {}
}
}
@@ -69,29 +69,74 @@ impl Tooltip {
impl Render for Tooltip {
fn render(&mut self, cx: &mut ViewContext<Self>) -> 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<V>(
+ cx: &mut ViewContext<V>,
+ f: impl FnOnce(Div, &mut ViewContext<V>) -> 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<Self>) -> impl IntoElement {
+ tooltip_container(cx, |el, _| {
+ el.child(
+ Label::new(self.link.clone())
+ .size(LabelSize::XSmall)
+ .color(Color::Muted),
+ )
+ })
}
}