diff --git a/crates/auto_update2/src/update_notification.rs b/crates/auto_update2/src/update_notification.rs index e6a22b73248a8fce898c6871abb06d602a3a8e7a..9cb1550bd431eb81d7c0e0c8dc4a49655f5d73ce 100644 --- a/crates/auto_update2/src/update_notification.rs +++ b/crates/auto_update2/src/update_notification.rs @@ -1,12 +1,13 @@ -use gpui::{div, Div, EventEmitter, ParentElement, Render, SemanticVersion, ViewContext}; +use gpui::{ + div, DismissEvent, Div, EventEmitter, ParentElement, Render, SemanticVersion, ViewContext, +}; use menu::Cancel; -use workspace::notifications::NotificationEvent; pub struct UpdateNotification { _version: SemanticVersion, } -impl EventEmitter for UpdateNotification {} +impl EventEmitter for UpdateNotification {} impl Render for UpdateNotification { type Element = Div; @@ -82,6 +83,6 @@ impl UpdateNotification { } pub fn _dismiss(&mut self, _: &Cancel, cx: &mut ViewContext) { - cx.emit(NotificationEvent::Dismiss); + cx.emit(DismissEvent::Dismiss); } } diff --git a/crates/command_palette2/src/command_palette.rs b/crates/command_palette2/src/command_palette.rs index 3c6f2fff92150fd302576635e7589eb238e5d01b..07b819d3a10c054ea20cddac787d28e792d3b187 100644 --- a/crates/command_palette2/src/command_palette.rs +++ b/crates/command_palette2/src/command_palette.rs @@ -1,8 +1,9 @@ use collections::{CommandPaletteFilter, HashMap}; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ - actions, div, prelude::*, Action, AppContext, Div, EventEmitter, FocusHandle, FocusableView, - Keystroke, Manager, ParentElement, Render, Styled, View, ViewContext, VisualContext, WeakView, + actions, div, prelude::*, Action, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, + FocusableView, Keystroke, ParentElement, Render, Styled, View, ViewContext, VisualContext, + WeakView, }; use picker::{Picker, PickerDelegate}; use std::{ @@ -68,7 +69,7 @@ impl CommandPalette { } } -impl EventEmitter for CommandPalette {} +impl EventEmitter for CommandPalette {} impl FocusableView for CommandPalette { fn focus_handle(&self, cx: &AppContext) -> FocusHandle { @@ -268,7 +269,7 @@ impl PickerDelegate for CommandPaletteDelegate { fn dismissed(&mut self, cx: &mut ViewContext>) { self.command_palette - .update(cx, |_, cx| cx.emit(Manager::Dismiss)) + .update(cx, |_, cx| cx.emit(DismissEvent::Dismiss)) .log_err(); } diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index 3aa53077182a26d0c8f813db3ef18fd8d8d048ff..a15d18f030852101b36dfbe501b399849c19cae4 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -531,8 +531,6 @@ pub fn init(cx: &mut AppContext) { // cx.register_action_type(Editor::context_menu_next); // cx.register_action_type(Editor::context_menu_last); - hover_popover::init(cx); - workspace::register_project_item::(cx); workspace::register_followable_item::(cx); workspace::register_deserializable_item::(cx); diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index e591dd84cf55da1ed71292dd39ac102d38736554..5b510095ff116745d020e5ecb8604d06e9af0a02 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -5,7 +5,9 @@ use crate::{ }, editor_settings::ShowScrollbar, git::{diff_hunk_to_display, DisplayDiffHunk}, - hover_popover::hover_at, + hover_popover::{ + self, hover_at, HOVER_POPOVER_GAP, MIN_POPOVER_CHARACTER_WIDTH, MIN_POPOVER_LINE_HEIGHT, + }, link_go_to_definition::{ go_to_fetched_definition, go_to_fetched_type_definition, update_go_to_definition_link, update_inlay_link_and_hover_points, GoToDefinitionTrigger, @@ -257,6 +259,7 @@ impl EditorElement { // on_action(cx, Editor::open_excerpts); todo!() register_action(view, cx, Editor::toggle_soft_wrap); register_action(view, cx, Editor::toggle_inlay_hints); + register_action(view, cx, hover_popover::hover); register_action(view, cx, Editor::reveal_in_finder); register_action(view, cx, Editor::copy_path); register_action(view, cx, Editor::copy_relative_path); @@ -1024,8 +1027,8 @@ impl EditorElement { } }); - if let Some((position, mut context_menu)) = layout.context_menu.take() { - cx.with_z_index(1, |cx| { + cx.with_z_index(1, |cx| { + if let Some((position, mut context_menu)) = layout.context_menu.take() { let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent); let context_menu_size = context_menu.measure(available_space, cx); @@ -1052,81 +1055,74 @@ impl EditorElement { list_origin.y -= layout.position_map.line_height + list_height; } - context_menu.draw(list_origin, available_space, cx); - }) - } + cx.break_content_mask(|cx| { + context_menu.draw(list_origin, available_space, cx) + }); + } - // if let Some((position, hover_popovers)) = layout.hover_popovers.as_mut() { - // cx.scene().push_stacking_context(None, None); - - // // This is safe because we check on layout whether the required row is available - // let hovered_row_layout = - // &layout.position_map.line_layouts[(position.row() - start_row) as usize].line; - - // // Minimum required size: Take the first popover, and add 1.5 times the minimum popover - // // height. This is the size we will use to decide whether to render popovers above or below - // // the hovered line. - // let first_size = hover_popovers[0].size(); - // let height_to_reserve = first_size.y - // + 1.5 * MIN_POPOVER_LINE_HEIGHT as f32 * layout.position_map.line_height; - - // // Compute Hovered Point - // let x = hovered_row_layout.x_for_index(position.column() as usize) - scroll_left; - // let y = position.row() as f32 * layout.position_map.line_height - scroll_top; - // let hovered_point = content_origin + point(x, y); - - // if hovered_point.y - height_to_reserve > 0.0 { - // // There is enough space above. Render popovers above the hovered point - // let mut current_y = hovered_point.y; - // for hover_popover in hover_popovers { - // let size = hover_popover.size(); - // let mut popover_origin = point(hovered_point.x, current_y - size.y); - - // let x_out_of_bounds = bounds.max_x - (popover_origin.x + size.x); - // if x_out_of_bounds < 0.0 { - // popover_origin.set_x(popover_origin.x + x_out_of_bounds); - // } - - // hover_popover.paint( - // popover_origin, - // Bounds::::from_points( - // gpui::Point::::zero(), - // point(f32::MAX, f32::MAX), - // ), // Let content bleed outside of editor - // editor, - // cx, - // ); - - // current_y = popover_origin.y - HOVER_POPOVER_GAP; - // } - // } else { - // // There is not enough space above. Render popovers below the hovered point - // let mut current_y = hovered_point.y + layout.position_map.line_height; - // for hover_popover in hover_popovers { - // let size = hover_popover.size(); - // let mut popover_origin = point(hovered_point.x, current_y); - - // let x_out_of_bounds = bounds.max_x - (popover_origin.x + size.x); - // if x_out_of_bounds < 0.0 { - // popover_origin.set_x(popover_origin.x + x_out_of_bounds); - // } - - // hover_popover.paint( - // popover_origin, - // Bounds::::from_points( - // gpui::Point::::zero(), - // point(f32::MAX, f32::MAX), - // ), // Let content bleed outside of editor - // editor, - // cx, - // ); - - // current_y = popover_origin.y + size.y + HOVER_POPOVER_GAP; - // } - // } - - // cx.scene().pop_stacking_context(); - // } + if let Some((position, mut hover_popovers)) = layout.hover_popovers.take() { + let available_space = + size(AvailableSpace::MinContent, AvailableSpace::MinContent); + + // This is safe because we check on layout whether the required row is available + let hovered_row_layout = &layout.position_map.line_layouts + [(position.row() - start_row) as usize] + .line; + + // Minimum required size: Take the first popover, and add 1.5 times the minimum popover + // height. This is the size we will use to decide whether to render popovers above or below + // the hovered line. + let first_size = hover_popovers[0].measure(available_space, cx); + let height_to_reserve = first_size.height + + 1.5 * MIN_POPOVER_LINE_HEIGHT * layout.position_map.line_height; + + // Compute Hovered Point + let x = hovered_row_layout.x_for_index(position.column() as usize) + - layout.position_map.scroll_position.x; + let y = position.row() as f32 * layout.position_map.line_height + - layout.position_map.scroll_position.y; + let hovered_point = content_origin + point(x, y); + + if hovered_point.y - height_to_reserve > Pixels::ZERO { + // There is enough space above. Render popovers above the hovered point + let mut current_y = hovered_point.y; + for mut hover_popover in hover_popovers { + let size = hover_popover.measure(available_space, cx); + let mut popover_origin = + point(hovered_point.x, current_y - size.height); + + let x_out_of_bounds = + text_bounds.upper_right().x - (popover_origin.x + size.width); + if x_out_of_bounds < Pixels::ZERO { + popover_origin.x = popover_origin.x + x_out_of_bounds; + } + + cx.break_content_mask(|cx| { + hover_popover.draw(popover_origin, available_space, cx) + }); + + current_y = popover_origin.y - HOVER_POPOVER_GAP; + } + } else { + // There is not enough space above. Render popovers below the hovered point + let mut current_y = hovered_point.y + layout.position_map.line_height; + for mut hover_popover in hover_popovers { + let size = hover_popover.measure(available_space, cx); + let mut popover_origin = point(hovered_point.x, current_y); + + let x_out_of_bounds = + text_bounds.upper_right().x - (popover_origin.x + size.width); + if x_out_of_bounds < Pixels::ZERO { + popover_origin.x = popover_origin.x + x_out_of_bounds; + } + + hover_popover.draw(popover_origin, available_space, cx); + + current_y = popover_origin.y + size.height + HOVER_POPOVER_GAP; + } + } + } + }) }, ) } @@ -1992,15 +1988,23 @@ impl EditorElement { } let visible_rows = start_row..start_row + line_layouts.len() as u32; - // todo!("hover") - // let mut hover = editor.hover_state.render( - // &snapshot, - // &style, - // visible_rows, - // editor.workspace.as_ref().map(|(w, _)| w.clone()), - // cx, - // ); - // let mode = editor.mode; + let max_size = size( + (120. * em_width) // Default size + .min(bounds.size.width / 2.) // Shrink to half of the editor width + .max(MIN_POPOVER_CHARACTER_WIDTH * em_width), // Apply minimum width of 20 characters + (16. * line_height) // Default size + .min(bounds.size.height / 2.) // Shrink to half of the editor height + .max(MIN_POPOVER_LINE_HEIGHT * line_height), // Apply minimum height of 4 lines + ); + + let mut hover = editor.hover_state.render( + &snapshot, + &style, + visible_rows, + max_size, + editor.workspace.as_ref().map(|(w, _)| w.clone()), + cx, + ); let mut fold_indicators = cx.with_element_id(Some("gutter_fold_indicators"), |cx| { editor.render_fold_indicators( @@ -2013,27 +2017,6 @@ impl EditorElement { ) }); - // todo!("hover popovers") - // if let Some((_, hover_popovers)) = hover.as_mut() { - // for hover_popover in hover_popovers.iter_mut() { - // hover_popover.layout( - // SizeConstraint { - // min: gpui::Point::::zero(), - // max: point( - // (120. * em_width) // Default size - // .min(size.x / 2.) // Shrink to half of the editor width - // .max(MIN_POPOVER_CHARACTER_WIDTH * em_width), // Apply minimum width of 20 characters - // (16. * line_height) // Default size - // .min(size.y / 2.) // Shrink to half of the editor height - // .max(MIN_POPOVER_LINE_HEIGHT * line_height), // Apply minimum height of 4 lines - // ), - // }, - // editor, - // cx, - // ); - // } - // } - let invisible_symbol_font_size = font_size / 2.; let tab_invisible = cx .text_system() @@ -2102,7 +2085,7 @@ impl EditorElement { fold_indicators, tab_invisible, space_invisible, - // hover_popovers: hover, + hover_popovers: hover, } }) } @@ -3287,7 +3270,7 @@ pub struct LayoutState { max_row: u32, context_menu: Option<(DisplayPoint, AnyElement)>, code_actions_indicator: Option, - // hover_popovers: Option<(DisplayPoint, Vec)>, + hover_popovers: Option<(DisplayPoint, Vec)>, fold_indicators: Vec>, tab_invisible: ShapedLine, space_invisible: ShapedLine, diff --git a/crates/editor2/src/hover_popover.rs b/crates/editor2/src/hover_popover.rs index 07d108cd6525babd12bf55404ba1b561cf2d67f4..37c7df650b126c859c28339a09077a96f4844bf2 100644 --- a/crates/editor2/src/hover_popover.rs +++ b/crates/editor2/src/hover_popover.rs @@ -1,15 +1,21 @@ use crate::{ - display_map::InlayOffset, + display_map::{InlayOffset, ToDisplayPoint}, link_go_to_definition::{InlayHighlight, RangeInEditor}, Anchor, AnchorRangeExt, DisplayPoint, Editor, EditorSettings, EditorSnapshot, EditorStyle, ExcerptId, RangeToAnchorExt, }; use futures::FutureExt; -use gpui::{AnyElement, AppContext, Model, Task, ViewContext, WeakView}; +use gpui::{ + actions, div, px, AnyElement, AppContext, CursorStyle, InteractiveElement, IntoElement, Model, + MouseButton, ParentElement, Pixels, SharedString, Size, StatefulInteractiveElement, Styled, + Task, ViewContext, WeakView, +}; use language::{markdown, Bias, DiagnosticEntry, Language, LanguageRegistry, ParsedMarkdown}; +use lsp::DiagnosticSeverity; use project::{HoverBlock, HoverBlockKind, InlayHintLabelPart, Project}; use settings::Settings; use std::{ops::Range, sync::Arc, time::Duration}; +use ui::Tooltip; use util::TryFutureExt; use workspace::Workspace; @@ -17,22 +23,17 @@ pub const HOVER_DELAY_MILLIS: u64 = 350; pub const HOVER_REQUEST_DELAY_MILLIS: u64 = 200; pub const MIN_POPOVER_CHARACTER_WIDTH: f32 = 20.; -pub const MIN_POPOVER_LINE_HEIGHT: f32 = 4.; -pub const HOVER_POPOVER_GAP: f32 = 10.; +pub const MIN_POPOVER_LINE_HEIGHT: Pixels = px(4.); +pub const HOVER_POPOVER_GAP: Pixels = px(10.); -// actions!(editor, [Hover]); +actions!(Hover); -pub fn init(cx: &mut AppContext) { - // cx.add_action(hover); +/// Bindable action which uses the most recent selection head to trigger a hover +pub fn hover(editor: &mut Editor, _: &Hover, cx: &mut ViewContext) { + let head = editor.selections.newest_display(cx).head(); + show_hover(editor, head, true, cx); } -// todo!() -// /// Bindable action which uses the most recent selection head to trigger a hover -// pub fn hover(editor: &mut Editor, _: &Hover, cx: &mut ViewContext) { -// let head = editor.selections.newest_display(cx).head(); -// show_hover(editor, head, true, cx); -// } - /// The internal hover action dispatches between `show_hover` or `hide_hover` /// depending on whether a point to hover over is provided. pub fn hover_at(editor: &mut Editor, point: Option, cx: &mut ViewContext) { @@ -74,64 +75,63 @@ pub fn find_hovered_hint_part( } pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut ViewContext) { - todo!() - // if EditorSettings::get_global(cx).hover_popover_enabled { - // if editor.pending_rename.is_some() { - // return; - // } - - // let Some(project) = editor.project.clone() else { - // return; - // }; - - // if let Some(InfoPopover { symbol_range, .. }) = &editor.hover_state.info_popover { - // if let RangeInEditor::Inlay(range) = symbol_range { - // if range == &inlay_hover.range { - // // Hover triggered from same location as last time. Don't show again. - // return; - // } - // } - // hide_hover(editor, cx); - // } - - // let task = cx.spawn(|this, mut cx| { - // async move { - // cx.background_executor() - // .timer(Duration::from_millis(HOVER_DELAY_MILLIS)) - // .await; - // this.update(&mut cx, |this, _| { - // this.hover_state.diagnostic_popover = None; - // })?; - - // let language_registry = project.update(&mut cx, |p, _| p.languages().clone())?; - // let blocks = vec![inlay_hover.tooltip]; - // let parsed_content = parse_blocks(&blocks, &language_registry, None).await; - - // let hover_popover = InfoPopover { - // project: project.clone(), - // symbol_range: RangeInEditor::Inlay(inlay_hover.range.clone()), - // blocks, - // parsed_content, - // }; - - // this.update(&mut cx, |this, cx| { - // // Highlight the selected symbol using a background highlight - // this.highlight_inlay_background::( - // vec![inlay_hover.range], - // |theme| theme.editor.hover_popover.highlight, - // cx, - // ); - // this.hover_state.info_popover = Some(hover_popover); - // cx.notify(); - // })?; - - // anyhow::Ok(()) - // } - // .log_err() - // }); - - // editor.hover_state.info_task = Some(task); - // } + if EditorSettings::get_global(cx).hover_popover_enabled { + if editor.pending_rename.is_some() { + return; + } + + let Some(project) = editor.project.clone() else { + return; + }; + + if let Some(InfoPopover { symbol_range, .. }) = &editor.hover_state.info_popover { + if let RangeInEditor::Inlay(range) = symbol_range { + if range == &inlay_hover.range { + // Hover triggered from same location as last time. Don't show again. + return; + } + } + hide_hover(editor, cx); + } + + let task = cx.spawn(|this, mut cx| { + async move { + cx.background_executor() + .timer(Duration::from_millis(HOVER_DELAY_MILLIS)) + .await; + this.update(&mut cx, |this, _| { + this.hover_state.diagnostic_popover = None; + })?; + + let language_registry = project.update(&mut cx, |p, _| p.languages().clone())?; + let blocks = vec![inlay_hover.tooltip]; + let parsed_content = parse_blocks(&blocks, &language_registry, None).await; + + let hover_popover = InfoPopover { + project: project.clone(), + symbol_range: RangeInEditor::Inlay(inlay_hover.range.clone()), + blocks, + parsed_content, + }; + + this.update(&mut cx, |this, cx| { + // Highlight the selected symbol using a background highlight + this.highlight_inlay_background::( + vec![inlay_hover.range], + |theme| theme.element_hover, // todo!("use a proper background here") + cx, + ); + this.hover_state.info_popover = Some(hover_popover); + cx.notify(); + })?; + + anyhow::Ok(()) + } + .log_err() + }); + + editor.hover_state.info_task = Some(task); + } } /// Hides the type information popup. @@ -420,43 +420,42 @@ impl HoverState { snapshot: &EditorSnapshot, style: &EditorStyle, visible_rows: Range, + max_size: Size, workspace: Option>, cx: &mut ViewContext, ) -> Option<(DisplayPoint, Vec)> { - todo!("old version below") + // If there is a diagnostic, position the popovers based on that. + // Otherwise use the start of the hover range + let anchor = self + .diagnostic_popover + .as_ref() + .map(|diagnostic_popover| &diagnostic_popover.local_diagnostic.range.start) + .or_else(|| { + self.info_popover + .as_ref() + .map(|info_popover| match &info_popover.symbol_range { + RangeInEditor::Text(range) => &range.start, + RangeInEditor::Inlay(range) => &range.inlay_position, + }) + })?; + let point = anchor.to_display_point(&snapshot.display_snapshot); + + // Don't render if the relevant point isn't on screen + if !self.visible() || !visible_rows.contains(&point.row()) { + return None; + } + + let mut elements = Vec::new(); + + if let Some(diagnostic_popover) = self.diagnostic_popover.as_ref() { + elements.push(diagnostic_popover.render(style, max_size, cx)); + } + if let Some(info_popover) = self.info_popover.as_mut() { + elements.push(info_popover.render(style, max_size, workspace, cx)); + } + + Some((point, elements)) } - // // If there is a diagnostic, position the popovers based on that. - // // Otherwise use the start of the hover range - // let anchor = self - // .diagnostic_popover - // .as_ref() - // .map(|diagnostic_popover| &diagnostic_popover.local_diagnostic.range.start) - // .or_else(|| { - // self.info_popover - // .as_ref() - // .map(|info_popover| match &info_popover.symbol_range { - // RangeInEditor::Text(range) => &range.start, - // RangeInEditor::Inlay(range) => &range.inlay_position, - // }) - // })?; - // let point = anchor.to_display_point(&snapshot.display_snapshot); - - // // Don't render if the relevant point isn't on screen - // if !self.visible() || !visible_rows.contains(&point.row()) { - // return None; - // } - - // let mut elements = Vec::new(); - - // if let Some(diagnostic_popover) = self.diagnostic_popover.as_ref() { - // elements.push(diagnostic_popover.render(style, cx)); - // } - // if let Some(info_popover) = self.info_popover.as_mut() { - // elements.push(info_popover.render(style, workspace, cx)); - // } - - // Some((point, elements)) - // } } #[derive(Debug, Clone)] @@ -467,35 +466,36 @@ pub struct InfoPopover { parsed_content: ParsedMarkdown, } -// impl InfoPopover { -// pub fn render( -// &mut self, -// style: &EditorStyle, -// workspace: Option>, -// cx: &mut ViewContext, -// ) -> AnyElement { -// MouseEventHandler::new::(0, cx, |_, cx| { -// Flex::column() -// .scrollable::(0, None, cx) -// .with_child(crate::render_parsed_markdown::( -// &self.parsed_content, -// style, -// workspace, -// cx, -// )) -// .contained() -// .with_style(style.hover_popover.container) -// }) -// .on_move(|_, _, _| {}) // Consume move events so they don't reach regions underneath. -// .with_cursor_style(CursorStyle::Arrow) -// .with_padding(Padding { -// bottom: HOVER_POPOVER_GAP, -// top: HOVER_POPOVER_GAP, -// ..Default::default() -// }) -// .into_any() -// } -// } +impl InfoPopover { + pub fn render( + &mut self, + style: &EditorStyle, + max_size: Size, + workspace: Option>, + cx: &mut ViewContext, + ) -> AnyElement { + div() + .id("info_popover") + .overflow_y_scroll() + .bg(gpui::red()) + .max_w(max_size.width) + .max_h(max_size.height) + // Prevent a mouse move on the popover from being propagated to the editor, + // because that would dismiss the popover. + .on_mouse_move(|_, cx| cx.stop_propagation()) + // Prevent a mouse down on the popover from being propagated to the editor, + // because that would move the cursor. + .on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation()) + .child(crate::render_parsed_markdown( + "content", + &self.parsed_content, + style, + workspace, + cx, + )) + .into_any_element() + } +} #[derive(Debug, Clone)] pub struct DiagnosticPopover { @@ -504,57 +504,40 @@ pub struct DiagnosticPopover { } impl DiagnosticPopover { - pub fn render(&self, style: &EditorStyle, cx: &mut ViewContext) -> AnyElement { - todo!() - // enum PrimaryDiagnostic {} - - // let mut text_style = style.hover_popover.prose.clone(); - // text_style.font_size = style.text.font_size; - // let diagnostic_source_style = style.hover_popover.diagnostic_source_highlight.clone(); - - // let text = match &self.local_diagnostic.diagnostic.source { - // Some(source) => Text::new( - // format!("{source}: {}", self.local_diagnostic.diagnostic.message), - // text_style, - // ) - // .with_highlights(vec![(0..source.len(), diagnostic_source_style)]), - - // None => Text::new(self.local_diagnostic.diagnostic.message.clone(), text_style), - // }; - - // let container_style = match self.local_diagnostic.diagnostic.severity { - // DiagnosticSeverity::HINT => style.hover_popover.info_container, - // DiagnosticSeverity::INFORMATION => style.hover_popover.info_container, - // DiagnosticSeverity::WARNING => style.hover_popover.warning_container, - // DiagnosticSeverity::ERROR => style.hover_popover.error_container, - // _ => style.hover_popover.container, - // }; - - // let tooltip_style = theme::current(cx).tooltip.clone(); - - // MouseEventHandler::new::(0, cx, |_, _| { - // text.with_soft_wrap(true) - // .contained() - // .with_style(container_style) - // }) - // .with_padding(Padding { - // top: HOVER_POPOVER_GAP, - // bottom: HOVER_POPOVER_GAP, - // ..Default::default() - // }) - // .on_move(|_, _, _| {}) // Consume move events so they don't reach regions underneath. - // .on_click(MouseButton::Left, |_, this, cx| { - // this.go_to_diagnostic(&Default::default(), cx) - // }) - // .with_cursor_style(CursorStyle::PointingHand) - // .with_tooltip::( - // 0, - // "Go To Diagnostic".to_string(), - // Some(Box::new(crate::GoToDiagnostic)), - // tooltip_style, - // cx, - // ) - // .into_any() + pub fn render( + &self, + style: &EditorStyle, + max_size: Size, + cx: &mut ViewContext, + ) -> AnyElement { + let text = match &self.local_diagnostic.diagnostic.source { + Some(source) => format!("{source}: {}", self.local_diagnostic.diagnostic.message), + None => self.local_diagnostic.diagnostic.message.clone(), + }; + + let container_bg = crate::diagnostic_style( + self.local_diagnostic.diagnostic.severity, + true, + &style.diagnostic_style, + ); + + div() + .id("diagnostic") + .overflow_y_scroll() + .bg(container_bg) + .max_w(max_size.width) + .max_h(max_size.height) + .cursor(CursorStyle::PointingHand) + .tooltip(move |cx| Tooltip::for_action("Go To Diagnostic", &crate::GoToDiagnostic, cx)) + // Prevent a mouse move on the popover from being propagated to the editor, + // because that would dismiss the popover. + .on_mouse_move(|_, cx| cx.stop_propagation()) + // Prevent a mouse down on the popover from being propagated to the editor, + // because that would move the cursor. + .on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation()) + .on_click(cx.listener(|editor, _, cx| editor.go_to_diagnostic(&Default::default(), cx))) + .child(SharedString::from(text)) + .into_any_element() } pub fn activation_info(&self) -> (usize, Anchor) { @@ -567,763 +550,763 @@ impl DiagnosticPopover { } } -// #[cfg(test)] -// mod tests { -// use super::*; -// use crate::{ -// editor_tests::init_test, -// element::PointForPosition, -// inlay_hint_cache::tests::{cached_hint_labels, visible_hint_labels}, -// link_go_to_definition::update_inlay_link_and_hover_points, -// test::editor_lsp_test_context::EditorLspTestContext, -// InlayId, -// }; -// use collections::BTreeSet; -// use gpui::fonts::{HighlightStyle, Underline, Weight}; -// use indoc::indoc; -// use language::{language_settings::InlayHintSettings, Diagnostic, DiagnosticSet}; -// use lsp::LanguageServerId; -// use project::{HoverBlock, HoverBlockKind}; -// use smol::stream::StreamExt; -// use unindent::Unindent; -// use util::test::marked_text_ranges; - -// #[gpui::test] -// async fn test_mouse_hover_info_popover(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); - -// let mut cx = EditorLspTestContext::new_rust( -// lsp::ServerCapabilities { -// hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), -// ..Default::default() -// }, -// cx, -// ) -// .await; - -// // Basic hover delays and then pops without moving the mouse -// cx.set_state(indoc! {" -// fn ˇtest() { println!(); } -// "}); -// let hover_point = cx.display_point(indoc! {" -// fn test() { printˇln!(); } -// "}); - -// cx.update_editor(|editor, cx| hover_at(editor, Some(hover_point), cx)); -// assert!(!cx.editor(|editor, _| editor.hover_state.visible())); - -// // After delay, hover should be visible. -// let symbol_range = cx.lsp_range(indoc! {" -// fn test() { «println!»(); } -// "}); -// let mut requests = -// cx.handle_request::(move |_, _, _| async move { -// Ok(Some(lsp::Hover { -// contents: lsp::HoverContents::Markup(lsp::MarkupContent { -// kind: lsp::MarkupKind::Markdown, -// value: "some basic docs".to_string(), -// }), -// range: Some(symbol_range), -// })) -// }); -// cx.foreground() -// .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); -// requests.next().await; - -// cx.editor(|editor, _| { -// assert!(editor.hover_state.visible()); -// assert_eq!( -// editor.hover_state.info_popover.clone().unwrap().blocks, -// vec![HoverBlock { -// text: "some basic docs".to_string(), -// kind: HoverBlockKind::Markdown, -// },] -// ) -// }); - -// // Mouse moved with no hover response dismisses -// let hover_point = cx.display_point(indoc! {" -// fn teˇst() { println!(); } -// "}); -// let mut request = cx -// .lsp -// .handle_request::(|_, _| async move { Ok(None) }); -// cx.update_editor(|editor, cx| hover_at(editor, Some(hover_point), cx)); -// cx.foreground() -// .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); -// request.next().await; -// cx.editor(|editor, _| { -// assert!(!editor.hover_state.visible()); -// }); -// } - -// #[gpui::test] -// async fn test_keyboard_hover_info_popover(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); - -// let mut cx = EditorLspTestContext::new_rust( -// lsp::ServerCapabilities { -// hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), -// ..Default::default() -// }, -// cx, -// ) -// .await; - -// // Hover with keyboard has no delay -// cx.set_state(indoc! {" -// fˇn test() { println!(); } -// "}); -// cx.update_editor(|editor, cx| hover(editor, &Hover, cx)); -// let symbol_range = cx.lsp_range(indoc! {" -// «fn» test() { println!(); } -// "}); -// cx.handle_request::(move |_, _, _| async move { -// Ok(Some(lsp::Hover { -// contents: lsp::HoverContents::Markup(lsp::MarkupContent { -// kind: lsp::MarkupKind::Markdown, -// value: "some other basic docs".to_string(), -// }), -// range: Some(symbol_range), -// })) -// }) -// .next() -// .await; - -// cx.condition(|editor, _| editor.hover_state.visible()).await; -// cx.editor(|editor, _| { -// assert_eq!( -// editor.hover_state.info_popover.clone().unwrap().blocks, -// vec![HoverBlock { -// text: "some other basic docs".to_string(), -// kind: HoverBlockKind::Markdown, -// }] -// ) -// }); -// } - -// #[gpui::test] -// async fn test_empty_hovers_filtered(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); - -// let mut cx = EditorLspTestContext::new_rust( -// lsp::ServerCapabilities { -// hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), -// ..Default::default() -// }, -// cx, -// ) -// .await; - -// // Hover with keyboard has no delay -// cx.set_state(indoc! {" -// fˇn test() { println!(); } -// "}); -// cx.update_editor(|editor, cx| hover(editor, &Hover, cx)); -// let symbol_range = cx.lsp_range(indoc! {" -// «fn» test() { println!(); } -// "}); -// cx.handle_request::(move |_, _, _| async move { -// Ok(Some(lsp::Hover { -// contents: lsp::HoverContents::Array(vec![ -// lsp::MarkedString::String("regular text for hover to show".to_string()), -// lsp::MarkedString::String("".to_string()), -// lsp::MarkedString::LanguageString(lsp::LanguageString { -// language: "Rust".to_string(), -// value: "".to_string(), -// }), -// ]), -// range: Some(symbol_range), -// })) -// }) -// .next() -// .await; - -// cx.condition(|editor, _| editor.hover_state.visible()).await; -// cx.editor(|editor, _| { -// assert_eq!( -// editor.hover_state.info_popover.clone().unwrap().blocks, -// vec![HoverBlock { -// text: "regular text for hover to show".to_string(), -// kind: HoverBlockKind::Markdown, -// }], -// "No empty string hovers should be shown" -// ); -// }); -// } - -// #[gpui::test] -// async fn test_line_ends_trimmed(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); - -// let mut cx = EditorLspTestContext::new_rust( -// lsp::ServerCapabilities { -// hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), -// ..Default::default() -// }, -// cx, -// ) -// .await; - -// // Hover with keyboard has no delay -// cx.set_state(indoc! {" -// fˇn test() { println!(); } -// "}); -// cx.update_editor(|editor, cx| hover(editor, &Hover, cx)); -// let symbol_range = cx.lsp_range(indoc! {" -// «fn» test() { println!(); } -// "}); - -// let code_str = "\nlet hovered_point: Vector2F // size = 8, align = 0x4\n"; -// let markdown_string = format!("\n```rust\n{code_str}```"); - -// let closure_markdown_string = markdown_string.clone(); -// cx.handle_request::(move |_, _, _| { -// let future_markdown_string = closure_markdown_string.clone(); -// async move { -// Ok(Some(lsp::Hover { -// contents: lsp::HoverContents::Markup(lsp::MarkupContent { -// kind: lsp::MarkupKind::Markdown, -// value: future_markdown_string, -// }), -// range: Some(symbol_range), -// })) -// } -// }) -// .next() -// .await; - -// cx.condition(|editor, _| editor.hover_state.visible()).await; -// cx.editor(|editor, _| { -// let blocks = editor.hover_state.info_popover.clone().unwrap().blocks; -// assert_eq!( -// blocks, -// vec![HoverBlock { -// text: markdown_string, -// kind: HoverBlockKind::Markdown, -// }], -// ); - -// let rendered = smol::block_on(parse_blocks(&blocks, &Default::default(), None)); -// assert_eq!( -// rendered.text, -// code_str.trim(), -// "Should not have extra line breaks at end of rendered hover" -// ); -// }); -// } - -// #[gpui::test] -// async fn test_hover_diagnostic_and_info_popovers(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); - -// let mut cx = EditorLspTestContext::new_rust( -// lsp::ServerCapabilities { -// hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), -// ..Default::default() -// }, -// cx, -// ) -// .await; - -// // Hover with just diagnostic, pops DiagnosticPopover immediately and then -// // info popover once request completes -// cx.set_state(indoc! {" -// fn teˇst() { println!(); } -// "}); - -// // Send diagnostic to client -// let range = cx.text_anchor_range(indoc! {" -// fn «test»() { println!(); } -// "}); -// cx.update_buffer(|buffer, cx| { -// let snapshot = buffer.text_snapshot(); -// let set = DiagnosticSet::from_sorted_entries( -// vec![DiagnosticEntry { -// range, -// diagnostic: Diagnostic { -// message: "A test diagnostic message.".to_string(), -// ..Default::default() -// }, -// }], -// &snapshot, -// ); -// buffer.update_diagnostics(LanguageServerId(0), set, cx); -// }); - -// // Hover pops diagnostic immediately -// cx.update_editor(|editor, cx| hover(editor, &Hover, cx)); -// cx.foreground().run_until_parked(); - -// cx.editor(|Editor { hover_state, .. }, _| { -// assert!(hover_state.diagnostic_popover.is_some() && hover_state.info_popover.is_none()) -// }); - -// // Info Popover shows after request responded to -// let range = cx.lsp_range(indoc! {" -// fn «test»() { println!(); } -// "}); -// cx.handle_request::(move |_, _, _| async move { -// Ok(Some(lsp::Hover { -// contents: lsp::HoverContents::Markup(lsp::MarkupContent { -// kind: lsp::MarkupKind::Markdown, -// value: "some new docs".to_string(), -// }), -// range: Some(range), -// })) -// }); -// cx.foreground() -// .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); - -// cx.foreground().run_until_parked(); -// cx.editor(|Editor { hover_state, .. }, _| { -// hover_state.diagnostic_popover.is_some() && hover_state.info_task.is_some() -// }); -// } - -// #[gpui::test] -// fn test_render_blocks(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); - -// cx.add_window(|cx| { -// let editor = Editor::single_line(None, cx); -// let style = editor.style(cx); - -// struct Row { -// blocks: Vec, -// expected_marked_text: String, -// expected_styles: Vec, -// } - -// let rows = &[ -// // Strong emphasis -// Row { -// blocks: vec![HoverBlock { -// text: "one **two** three".to_string(), -// kind: HoverBlockKind::Markdown, -// }], -// expected_marked_text: "one «two» three".to_string(), -// expected_styles: vec![HighlightStyle { -// weight: Some(Weight::BOLD), -// ..Default::default() -// }], -// }, -// // Links -// Row { -// blocks: vec![HoverBlock { -// text: "one [two](https://the-url) three".to_string(), -// kind: HoverBlockKind::Markdown, -// }], -// expected_marked_text: "one «two» three".to_string(), -// expected_styles: vec![HighlightStyle { -// underline: Some(Underline { -// thickness: 1.0.into(), -// ..Default::default() -// }), -// ..Default::default() -// }], -// }, -// // Lists -// Row { -// blocks: vec![HoverBlock { -// text: " -// lists: -// * one -// - a -// - b -// * two -// - [c](https://the-url) -// - d" -// .unindent(), -// kind: HoverBlockKind::Markdown, -// }], -// expected_marked_text: " -// lists: -// - one -// - a -// - b -// - two -// - «c» -// - d" -// .unindent(), -// expected_styles: vec![HighlightStyle { -// underline: Some(Underline { -// thickness: 1.0.into(), -// ..Default::default() -// }), -// ..Default::default() -// }], -// }, -// // Multi-paragraph list items -// Row { -// blocks: vec![HoverBlock { -// text: " -// * one two -// three - -// * four five -// * six seven -// eight - -// nine -// * ten -// * six" -// .unindent(), -// kind: HoverBlockKind::Markdown, -// }], -// expected_marked_text: " -// - one two three -// - four five -// - six seven eight - -// nine -// - ten -// - six" -// .unindent(), -// expected_styles: vec![HighlightStyle { -// underline: Some(Underline { -// thickness: 1.0.into(), -// ..Default::default() -// }), -// ..Default::default() -// }], -// }, -// ]; - -// for Row { -// blocks, -// expected_marked_text, -// expected_styles, -// } in &rows[0..] -// { -// let rendered = smol::block_on(parse_blocks(&blocks, &Default::default(), None)); - -// let (expected_text, ranges) = marked_text_ranges(expected_marked_text, false); -// let expected_highlights = ranges -// .into_iter() -// .zip(expected_styles.iter().cloned()) -// .collect::>(); -// assert_eq!( -// rendered.text, expected_text, -// "wrong text for input {blocks:?}" -// ); - -// let rendered_highlights: Vec<_> = rendered -// .highlights -// .iter() -// .filter_map(|(range, highlight)| { -// let highlight = highlight.to_highlight_style(&style.syntax)?; -// Some((range.clone(), highlight)) -// }) -// .collect(); - -// assert_eq!( -// rendered_highlights, expected_highlights, -// "wrong highlights for input {blocks:?}" -// ); -// } - -// editor -// }); -// } - -// #[gpui::test] -// async fn test_hover_inlay_label_parts(cx: &mut gpui::TestAppContext) { -// init_test(cx, |settings| { -// settings.defaults.inlay_hints = Some(InlayHintSettings { -// enabled: true, -// show_type_hints: true, -// show_parameter_hints: true, -// show_other_hints: true, -// }) -// }); - -// let mut cx = EditorLspTestContext::new_rust( -// lsp::ServerCapabilities { -// inlay_hint_provider: Some(lsp::OneOf::Right( -// lsp::InlayHintServerCapabilities::Options(lsp::InlayHintOptions { -// resolve_provider: Some(true), -// ..Default::default() -// }), -// )), -// ..Default::default() -// }, -// cx, -// ) -// .await; - -// cx.set_state(indoc! {" -// struct TestStruct; - -// // ================== - -// struct TestNewType(T); - -// fn main() { -// let variableˇ = TestNewType(TestStruct); -// } -// "}); - -// let hint_start_offset = cx.ranges(indoc! {" -// struct TestStruct; - -// // ================== - -// struct TestNewType(T); - -// fn main() { -// let variableˇ = TestNewType(TestStruct); -// } -// "})[0] -// .start; -// let hint_position = cx.to_lsp(hint_start_offset); -// let new_type_target_range = cx.lsp_range(indoc! {" -// struct TestStruct; - -// // ================== - -// struct «TestNewType»(T); - -// fn main() { -// let variable = TestNewType(TestStruct); -// } -// "}); -// let struct_target_range = cx.lsp_range(indoc! {" -// struct «TestStruct»; - -// // ================== - -// struct TestNewType(T); - -// fn main() { -// let variable = TestNewType(TestStruct); -// } -// "}); - -// let uri = cx.buffer_lsp_url.clone(); -// let new_type_label = "TestNewType"; -// let struct_label = "TestStruct"; -// let entire_hint_label = ": TestNewType"; -// let closure_uri = uri.clone(); -// cx.lsp -// .handle_request::(move |params, _| { -// let task_uri = closure_uri.clone(); -// async move { -// assert_eq!(params.text_document.uri, task_uri); -// Ok(Some(vec![lsp::InlayHint { -// position: hint_position, -// label: lsp::InlayHintLabel::LabelParts(vec![lsp::InlayHintLabelPart { -// value: entire_hint_label.to_string(), -// ..Default::default() -// }]), -// kind: Some(lsp::InlayHintKind::TYPE), -// text_edits: None, -// tooltip: None, -// padding_left: Some(false), -// padding_right: Some(false), -// data: None, -// }])) -// } -// }) -// .next() -// .await; -// cx.foreground().run_until_parked(); -// cx.update_editor(|editor, cx| { -// let expected_layers = vec![entire_hint_label.to_string()]; -// assert_eq!(expected_layers, cached_hint_labels(editor)); -// assert_eq!(expected_layers, visible_hint_labels(editor, cx)); -// }); - -// let inlay_range = cx -// .ranges(indoc! {" -// struct TestStruct; - -// // ================== - -// struct TestNewType(T); - -// fn main() { -// let variable« »= TestNewType(TestStruct); -// } -// "}) -// .get(0) -// .cloned() -// .unwrap(); -// let new_type_hint_part_hover_position = cx.update_editor(|editor, cx| { -// let snapshot = editor.snapshot(cx); -// let previous_valid = inlay_range.start.to_display_point(&snapshot); -// let next_valid = inlay_range.end.to_display_point(&snapshot); -// assert_eq!(previous_valid.row(), next_valid.row()); -// assert!(previous_valid.column() < next_valid.column()); -// let exact_unclipped = DisplayPoint::new( -// previous_valid.row(), -// previous_valid.column() -// + (entire_hint_label.find(new_type_label).unwrap() + new_type_label.len() / 2) -// as u32, -// ); -// PointForPosition { -// previous_valid, -// next_valid, -// exact_unclipped, -// column_overshoot_after_line_end: 0, -// } -// }); -// cx.update_editor(|editor, cx| { -// update_inlay_link_and_hover_points( -// &editor.snapshot(cx), -// new_type_hint_part_hover_position, -// editor, -// true, -// false, -// cx, -// ); -// }); - -// let resolve_closure_uri = uri.clone(); -// cx.lsp -// .handle_request::( -// move |mut hint_to_resolve, _| { -// let mut resolved_hint_positions = BTreeSet::new(); -// let task_uri = resolve_closure_uri.clone(); -// async move { -// let inserted = resolved_hint_positions.insert(hint_to_resolve.position); -// assert!(inserted, "Hint {hint_to_resolve:?} was resolved twice"); - -// // `: TestNewType` -// hint_to_resolve.label = lsp::InlayHintLabel::LabelParts(vec![ -// lsp::InlayHintLabelPart { -// value: ": ".to_string(), -// ..Default::default() -// }, -// lsp::InlayHintLabelPart { -// value: new_type_label.to_string(), -// location: Some(lsp::Location { -// uri: task_uri.clone(), -// range: new_type_target_range, -// }), -// tooltip: Some(lsp::InlayHintLabelPartTooltip::String(format!( -// "A tooltip for `{new_type_label}`" -// ))), -// ..Default::default() -// }, -// lsp::InlayHintLabelPart { -// value: "<".to_string(), -// ..Default::default() -// }, -// lsp::InlayHintLabelPart { -// value: struct_label.to_string(), -// location: Some(lsp::Location { -// uri: task_uri, -// range: struct_target_range, -// }), -// tooltip: Some(lsp::InlayHintLabelPartTooltip::MarkupContent( -// lsp::MarkupContent { -// kind: lsp::MarkupKind::Markdown, -// value: format!("A tooltip for `{struct_label}`"), -// }, -// )), -// ..Default::default() -// }, -// lsp::InlayHintLabelPart { -// value: ">".to_string(), -// ..Default::default() -// }, -// ]); - -// Ok(hint_to_resolve) -// } -// }, -// ) -// .next() -// .await; -// cx.foreground().run_until_parked(); - -// cx.update_editor(|editor, cx| { -// update_inlay_link_and_hover_points( -// &editor.snapshot(cx), -// new_type_hint_part_hover_position, -// editor, -// true, -// false, -// cx, -// ); -// }); -// cx.foreground() -// .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); -// cx.foreground().run_until_parked(); -// cx.update_editor(|editor, cx| { -// let hover_state = &editor.hover_state; -// assert!(hover_state.diagnostic_popover.is_none() && hover_state.info_popover.is_some()); -// let popover = hover_state.info_popover.as_ref().unwrap(); -// let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx)); -// assert_eq!( -// popover.symbol_range, -// RangeInEditor::Inlay(InlayHighlight { -// inlay: InlayId::Hint(0), -// inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right), -// range: ": ".len()..": ".len() + new_type_label.len(), -// }), -// "Popover range should match the new type label part" -// ); -// assert_eq!( -// popover.parsed_content.text, -// format!("A tooltip for `{new_type_label}`"), -// "Rendered text should not anyhow alter backticks" -// ); -// }); - -// let struct_hint_part_hover_position = cx.update_editor(|editor, cx| { -// let snapshot = editor.snapshot(cx); -// let previous_valid = inlay_range.start.to_display_point(&snapshot); -// let next_valid = inlay_range.end.to_display_point(&snapshot); -// assert_eq!(previous_valid.row(), next_valid.row()); -// assert!(previous_valid.column() < next_valid.column()); -// let exact_unclipped = DisplayPoint::new( -// previous_valid.row(), -// previous_valid.column() -// + (entire_hint_label.find(struct_label).unwrap() + struct_label.len() / 2) -// as u32, -// ); -// PointForPosition { -// previous_valid, -// next_valid, -// exact_unclipped, -// column_overshoot_after_line_end: 0, -// } -// }); -// cx.update_editor(|editor, cx| { -// update_inlay_link_and_hover_points( -// &editor.snapshot(cx), -// struct_hint_part_hover_position, -// editor, -// true, -// false, -// cx, -// ); -// }); -// cx.foreground() -// .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); -// cx.foreground().run_until_parked(); -// cx.update_editor(|editor, cx| { -// let hover_state = &editor.hover_state; -// assert!(hover_state.diagnostic_popover.is_none() && hover_state.info_popover.is_some()); -// let popover = hover_state.info_popover.as_ref().unwrap(); -// let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx)); -// assert_eq!( -// popover.symbol_range, -// RangeInEditor::Inlay(InlayHighlight { -// inlay: InlayId::Hint(0), -// inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right), -// range: ": ".len() + new_type_label.len() + "<".len() -// ..": ".len() + new_type_label.len() + "<".len() + struct_label.len(), -// }), -// "Popover range should match the struct label part" -// ); -// assert_eq!( -// popover.parsed_content.text, -// format!("A tooltip for {struct_label}"), -// "Rendered markdown element should remove backticks from text" -// ); -// }); -// } -// } +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + editor_tests::init_test, + element::PointForPosition, + inlay_hint_cache::tests::{cached_hint_labels, visible_hint_labels}, + link_go_to_definition::update_inlay_link_and_hover_points, + test::editor_lsp_test_context::EditorLspTestContext, + InlayId, + }; + use collections::BTreeSet; + use gpui::{FontWeight, HighlightStyle, UnderlineStyle}; + use indoc::indoc; + use language::{language_settings::InlayHintSettings, Diagnostic, DiagnosticSet}; + use lsp::LanguageServerId; + use project::{HoverBlock, HoverBlockKind}; + use smol::stream::StreamExt; + use unindent::Unindent; + use util::test::marked_text_ranges; + + #[gpui::test] + async fn test_mouse_hover_info_popover(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), + ..Default::default() + }, + cx, + ) + .await; + + // Basic hover delays and then pops without moving the mouse + cx.set_state(indoc! {" + fn ˇtest() { println!(); } + "}); + let hover_point = cx.display_point(indoc! {" + fn test() { printˇln!(); } + "}); + + cx.update_editor(|editor, cx| hover_at(editor, Some(hover_point), cx)); + assert!(!cx.editor(|editor, _| editor.hover_state.visible())); + + // After delay, hover should be visible. + let symbol_range = cx.lsp_range(indoc! {" + fn test() { «println!»(); } + "}); + let mut requests = + cx.handle_request::(move |_, _, _| async move { + Ok(Some(lsp::Hover { + contents: lsp::HoverContents::Markup(lsp::MarkupContent { + kind: lsp::MarkupKind::Markdown, + value: "some basic docs".to_string(), + }), + range: Some(symbol_range), + })) + }); + cx.background_executor + .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); + requests.next().await; + + cx.editor(|editor, _| { + assert!(editor.hover_state.visible()); + assert_eq!( + editor.hover_state.info_popover.clone().unwrap().blocks, + vec![HoverBlock { + text: "some basic docs".to_string(), + kind: HoverBlockKind::Markdown, + },] + ) + }); + + // Mouse moved with no hover response dismisses + let hover_point = cx.display_point(indoc! {" + fn teˇst() { println!(); } + "}); + let mut request = cx + .lsp + .handle_request::(|_, _| async move { Ok(None) }); + cx.update_editor(|editor, cx| hover_at(editor, Some(hover_point), cx)); + cx.background_executor + .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); + request.next().await; + cx.editor(|editor, _| { + assert!(!editor.hover_state.visible()); + }); + } + + #[gpui::test] + async fn test_keyboard_hover_info_popover(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), + ..Default::default() + }, + cx, + ) + .await; + + // Hover with keyboard has no delay + cx.set_state(indoc! {" + fˇn test() { println!(); } + "}); + cx.update_editor(|editor, cx| hover(editor, &Hover, cx)); + let symbol_range = cx.lsp_range(indoc! {" + «fn» test() { println!(); } + "}); + cx.handle_request::(move |_, _, _| async move { + Ok(Some(lsp::Hover { + contents: lsp::HoverContents::Markup(lsp::MarkupContent { + kind: lsp::MarkupKind::Markdown, + value: "some other basic docs".to_string(), + }), + range: Some(symbol_range), + })) + }) + .next() + .await; + + cx.condition(|editor, _| editor.hover_state.visible()).await; + cx.editor(|editor, _| { + assert_eq!( + editor.hover_state.info_popover.clone().unwrap().blocks, + vec![HoverBlock { + text: "some other basic docs".to_string(), + kind: HoverBlockKind::Markdown, + }] + ) + }); + } + + #[gpui::test] + async fn test_empty_hovers_filtered(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), + ..Default::default() + }, + cx, + ) + .await; + + // Hover with keyboard has no delay + cx.set_state(indoc! {" + fˇn test() { println!(); } + "}); + cx.update_editor(|editor, cx| hover(editor, &Hover, cx)); + let symbol_range = cx.lsp_range(indoc! {" + «fn» test() { println!(); } + "}); + cx.handle_request::(move |_, _, _| async move { + Ok(Some(lsp::Hover { + contents: lsp::HoverContents::Array(vec![ + lsp::MarkedString::String("regular text for hover to show".to_string()), + lsp::MarkedString::String("".to_string()), + lsp::MarkedString::LanguageString(lsp::LanguageString { + language: "Rust".to_string(), + value: "".to_string(), + }), + ]), + range: Some(symbol_range), + })) + }) + .next() + .await; + + cx.condition(|editor, _| editor.hover_state.visible()).await; + cx.editor(|editor, _| { + assert_eq!( + editor.hover_state.info_popover.clone().unwrap().blocks, + vec![HoverBlock { + text: "regular text for hover to show".to_string(), + kind: HoverBlockKind::Markdown, + }], + "No empty string hovers should be shown" + ); + }); + } + + #[gpui::test] + async fn test_line_ends_trimmed(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), + ..Default::default() + }, + cx, + ) + .await; + + // Hover with keyboard has no delay + cx.set_state(indoc! {" + fˇn test() { println!(); } + "}); + cx.update_editor(|editor, cx| hover(editor, &Hover, cx)); + let symbol_range = cx.lsp_range(indoc! {" + «fn» test() { println!(); } + "}); + + let code_str = "\nlet hovered_point: Vector2F // size = 8, align = 0x4\n"; + let markdown_string = format!("\n```rust\n{code_str}```"); + + let closure_markdown_string = markdown_string.clone(); + cx.handle_request::(move |_, _, _| { + let future_markdown_string = closure_markdown_string.clone(); + async move { + Ok(Some(lsp::Hover { + contents: lsp::HoverContents::Markup(lsp::MarkupContent { + kind: lsp::MarkupKind::Markdown, + value: future_markdown_string, + }), + range: Some(symbol_range), + })) + } + }) + .next() + .await; + + cx.condition(|editor, _| editor.hover_state.visible()).await; + cx.editor(|editor, _| { + let blocks = editor.hover_state.info_popover.clone().unwrap().blocks; + assert_eq!( + blocks, + vec![HoverBlock { + text: markdown_string, + kind: HoverBlockKind::Markdown, + }], + ); + + let rendered = smol::block_on(parse_blocks(&blocks, &Default::default(), None)); + assert_eq!( + rendered.text, + code_str.trim(), + "Should not have extra line breaks at end of rendered hover" + ); + }); + } + + #[gpui::test] + async fn test_hover_diagnostic_and_info_popovers(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), + ..Default::default() + }, + cx, + ) + .await; + + // Hover with just diagnostic, pops DiagnosticPopover immediately and then + // info popover once request completes + cx.set_state(indoc! {" + fn teˇst() { println!(); } + "}); + + // Send diagnostic to client + let range = cx.text_anchor_range(indoc! {" + fn «test»() { println!(); } + "}); + cx.update_buffer(|buffer, cx| { + let snapshot = buffer.text_snapshot(); + let set = DiagnosticSet::from_sorted_entries( + vec![DiagnosticEntry { + range, + diagnostic: Diagnostic { + message: "A test diagnostic message.".to_string(), + ..Default::default() + }, + }], + &snapshot, + ); + buffer.update_diagnostics(LanguageServerId(0), set, cx); + }); + + // Hover pops diagnostic immediately + cx.update_editor(|editor, cx| hover(editor, &Hover, cx)); + cx.background_executor.run_until_parked(); + + cx.editor(|Editor { hover_state, .. }, _| { + assert!(hover_state.diagnostic_popover.is_some() && hover_state.info_popover.is_none()) + }); + + // Info Popover shows after request responded to + let range = cx.lsp_range(indoc! {" + fn «test»() { println!(); } + "}); + cx.handle_request::(move |_, _, _| async move { + Ok(Some(lsp::Hover { + contents: lsp::HoverContents::Markup(lsp::MarkupContent { + kind: lsp::MarkupKind::Markdown, + value: "some new docs".to_string(), + }), + range: Some(range), + })) + }); + cx.background_executor + .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); + + cx.background_executor.run_until_parked(); + cx.editor(|Editor { hover_state, .. }, _| { + hover_state.diagnostic_popover.is_some() && hover_state.info_task.is_some() + }); + } + + #[gpui::test] + fn test_render_blocks(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let editor = cx.add_window(|cx| Editor::single_line(cx)); + editor + .update(cx, |editor, cx| { + let style = editor.style.clone().unwrap(); + + struct Row { + blocks: Vec, + expected_marked_text: String, + expected_styles: Vec, + } + + let rows = &[ + // Strong emphasis + Row { + blocks: vec![HoverBlock { + text: "one **two** three".to_string(), + kind: HoverBlockKind::Markdown, + }], + expected_marked_text: "one «two» three".to_string(), + expected_styles: vec![HighlightStyle { + font_weight: Some(FontWeight::BOLD), + ..Default::default() + }], + }, + // Links + Row { + blocks: vec![HoverBlock { + text: "one [two](https://the-url) three".to_string(), + kind: HoverBlockKind::Markdown, + }], + expected_marked_text: "one «two» three".to_string(), + expected_styles: vec![HighlightStyle { + underline: Some(UnderlineStyle { + thickness: 1.0.into(), + ..Default::default() + }), + ..Default::default() + }], + }, + // Lists + Row { + blocks: vec![HoverBlock { + text: " + lists: + * one + - a + - b + * two + - [c](https://the-url) + - d" + .unindent(), + kind: HoverBlockKind::Markdown, + }], + expected_marked_text: " + lists: + - one + - a + - b + - two + - «c» + - d" + .unindent(), + expected_styles: vec![HighlightStyle { + underline: Some(UnderlineStyle { + thickness: 1.0.into(), + ..Default::default() + }), + ..Default::default() + }], + }, + // Multi-paragraph list items + Row { + blocks: vec![HoverBlock { + text: " + * one two + three + + * four five + * six seven + eight + + nine + * ten + * six" + .unindent(), + kind: HoverBlockKind::Markdown, + }], + expected_marked_text: " + - one two three + - four five + - six seven eight + + nine + - ten + - six" + .unindent(), + expected_styles: vec![HighlightStyle { + underline: Some(UnderlineStyle { + thickness: 1.0.into(), + ..Default::default() + }), + ..Default::default() + }], + }, + ]; + + for Row { + blocks, + expected_marked_text, + expected_styles, + } in &rows[0..] + { + let rendered = smol::block_on(parse_blocks(&blocks, &Default::default(), None)); + + let (expected_text, ranges) = marked_text_ranges(expected_marked_text, false); + let expected_highlights = ranges + .into_iter() + .zip(expected_styles.iter().cloned()) + .collect::>(); + assert_eq!( + rendered.text, expected_text, + "wrong text for input {blocks:?}" + ); + + let rendered_highlights: Vec<_> = rendered + .highlights + .iter() + .filter_map(|(range, highlight)| { + let highlight = highlight.to_highlight_style(&style.syntax)?; + Some((range.clone(), highlight)) + }) + .collect(); + + assert_eq!( + rendered_highlights, expected_highlights, + "wrong highlights for input {blocks:?}" + ); + } + }) + .unwrap(); + } + + #[gpui::test] + async fn test_hover_inlay_label_parts(cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + show_type_hints: true, + show_parameter_hints: true, + show_other_hints: true, + }) + }); + + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + inlay_hint_provider: Some(lsp::OneOf::Right( + lsp::InlayHintServerCapabilities::Options(lsp::InlayHintOptions { + resolve_provider: Some(true), + ..Default::default() + }), + )), + ..Default::default() + }, + cx, + ) + .await; + + cx.set_state(indoc! {" + struct TestStruct; + + // ================== + + struct TestNewType(T); + + fn main() { + let variableˇ = TestNewType(TestStruct); + } + "}); + + let hint_start_offset = cx.ranges(indoc! {" + struct TestStruct; + + // ================== + + struct TestNewType(T); + + fn main() { + let variableˇ = TestNewType(TestStruct); + } + "})[0] + .start; + let hint_position = cx.to_lsp(hint_start_offset); + let new_type_target_range = cx.lsp_range(indoc! {" + struct TestStruct; + + // ================== + + struct «TestNewType»(T); + + fn main() { + let variable = TestNewType(TestStruct); + } + "}); + let struct_target_range = cx.lsp_range(indoc! {" + struct «TestStruct»; + + // ================== + + struct TestNewType(T); + + fn main() { + let variable = TestNewType(TestStruct); + } + "}); + + let uri = cx.buffer_lsp_url.clone(); + let new_type_label = "TestNewType"; + let struct_label = "TestStruct"; + let entire_hint_label = ": TestNewType"; + let closure_uri = uri.clone(); + cx.lsp + .handle_request::(move |params, _| { + let task_uri = closure_uri.clone(); + async move { + assert_eq!(params.text_document.uri, task_uri); + Ok(Some(vec![lsp::InlayHint { + position: hint_position, + label: lsp::InlayHintLabel::LabelParts(vec![lsp::InlayHintLabelPart { + value: entire_hint_label.to_string(), + ..Default::default() + }]), + kind: Some(lsp::InlayHintKind::TYPE), + text_edits: None, + tooltip: None, + padding_left: Some(false), + padding_right: Some(false), + data: None, + }])) + } + }) + .next() + .await; + cx.background_executor.run_until_parked(); + cx.update_editor(|editor, cx| { + let expected_layers = vec![entire_hint_label.to_string()]; + assert_eq!(expected_layers, cached_hint_labels(editor)); + assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + }); + + let inlay_range = cx + .ranges(indoc! {" + struct TestStruct; + + // ================== + + struct TestNewType(T); + + fn main() { + let variable« »= TestNewType(TestStruct); + } + "}) + .get(0) + .cloned() + .unwrap(); + let new_type_hint_part_hover_position = cx.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + let previous_valid = inlay_range.start.to_display_point(&snapshot); + let next_valid = inlay_range.end.to_display_point(&snapshot); + assert_eq!(previous_valid.row(), next_valid.row()); + assert!(previous_valid.column() < next_valid.column()); + let exact_unclipped = DisplayPoint::new( + previous_valid.row(), + previous_valid.column() + + (entire_hint_label.find(new_type_label).unwrap() + new_type_label.len() / 2) + as u32, + ); + PointForPosition { + previous_valid, + next_valid, + exact_unclipped, + column_overshoot_after_line_end: 0, + } + }); + cx.update_editor(|editor, cx| { + update_inlay_link_and_hover_points( + &editor.snapshot(cx), + new_type_hint_part_hover_position, + editor, + true, + false, + cx, + ); + }); + + let resolve_closure_uri = uri.clone(); + cx.lsp + .handle_request::( + move |mut hint_to_resolve, _| { + let mut resolved_hint_positions = BTreeSet::new(); + let task_uri = resolve_closure_uri.clone(); + async move { + let inserted = resolved_hint_positions.insert(hint_to_resolve.position); + assert!(inserted, "Hint {hint_to_resolve:?} was resolved twice"); + + // `: TestNewType` + hint_to_resolve.label = lsp::InlayHintLabel::LabelParts(vec![ + lsp::InlayHintLabelPart { + value: ": ".to_string(), + ..Default::default() + }, + lsp::InlayHintLabelPart { + value: new_type_label.to_string(), + location: Some(lsp::Location { + uri: task_uri.clone(), + range: new_type_target_range, + }), + tooltip: Some(lsp::InlayHintLabelPartTooltip::String(format!( + "A tooltip for `{new_type_label}`" + ))), + ..Default::default() + }, + lsp::InlayHintLabelPart { + value: "<".to_string(), + ..Default::default() + }, + lsp::InlayHintLabelPart { + value: struct_label.to_string(), + location: Some(lsp::Location { + uri: task_uri, + range: struct_target_range, + }), + tooltip: Some(lsp::InlayHintLabelPartTooltip::MarkupContent( + lsp::MarkupContent { + kind: lsp::MarkupKind::Markdown, + value: format!("A tooltip for `{struct_label}`"), + }, + )), + ..Default::default() + }, + lsp::InlayHintLabelPart { + value: ">".to_string(), + ..Default::default() + }, + ]); + + Ok(hint_to_resolve) + } + }, + ) + .next() + .await; + cx.background_executor.run_until_parked(); + + cx.update_editor(|editor, cx| { + update_inlay_link_and_hover_points( + &editor.snapshot(cx), + new_type_hint_part_hover_position, + editor, + true, + false, + cx, + ); + }); + cx.background_executor + .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); + cx.background_executor.run_until_parked(); + cx.update_editor(|editor, cx| { + let hover_state = &editor.hover_state; + assert!(hover_state.diagnostic_popover.is_none() && hover_state.info_popover.is_some()); + let popover = hover_state.info_popover.as_ref().unwrap(); + let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx)); + assert_eq!( + popover.symbol_range, + RangeInEditor::Inlay(InlayHighlight { + inlay: InlayId::Hint(0), + inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right), + range: ": ".len()..": ".len() + new_type_label.len(), + }), + "Popover range should match the new type label part" + ); + assert_eq!( + popover.parsed_content.text, + format!("A tooltip for `{new_type_label}`"), + "Rendered text should not anyhow alter backticks" + ); + }); + + let struct_hint_part_hover_position = cx.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + let previous_valid = inlay_range.start.to_display_point(&snapshot); + let next_valid = inlay_range.end.to_display_point(&snapshot); + assert_eq!(previous_valid.row(), next_valid.row()); + assert!(previous_valid.column() < next_valid.column()); + let exact_unclipped = DisplayPoint::new( + previous_valid.row(), + previous_valid.column() + + (entire_hint_label.find(struct_label).unwrap() + struct_label.len() / 2) + as u32, + ); + PointForPosition { + previous_valid, + next_valid, + exact_unclipped, + column_overshoot_after_line_end: 0, + } + }); + cx.update_editor(|editor, cx| { + update_inlay_link_and_hover_points( + &editor.snapshot(cx), + struct_hint_part_hover_position, + editor, + true, + false, + cx, + ); + }); + cx.background_executor + .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); + cx.background_executor.run_until_parked(); + cx.update_editor(|editor, cx| { + let hover_state = &editor.hover_state; + assert!(hover_state.diagnostic_popover.is_none() && hover_state.info_popover.is_some()); + let popover = hover_state.info_popover.as_ref().unwrap(); + let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx)); + assert_eq!( + popover.symbol_range, + RangeInEditor::Inlay(InlayHighlight { + inlay: InlayId::Hint(0), + inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right), + range: ": ".len() + new_type_label.len() + "<".len() + ..": ".len() + new_type_label.len() + "<".len() + struct_label.len(), + }), + "Popover range should match the struct label part" + ); + assert_eq!( + popover.parsed_content.text, + format!("A tooltip for {struct_label}"), + "Rendered markdown element should remove backticks from text" + ); + }); + } +} diff --git a/crates/file_finder2/src/file_finder.rs b/crates/file_finder2/src/file_finder.rs index 873054a68c77c81ecac723b2ad1169ef9eca3775..ea578fbb0ebb5703f3f31ed142f5589a0345c814 100644 --- a/crates/file_finder2/src/file_finder.rs +++ b/crates/file_finder2/src/file_finder.rs @@ -2,8 +2,8 @@ use collections::HashMap; use editor::{scroll::autoscroll::Autoscroll, Bias, Editor}; use fuzzy::{CharBag, PathMatch, PathMatchCandidate}; use gpui::{ - actions, div, AppContext, Div, EventEmitter, FocusHandle, FocusableView, InteractiveElement, - IntoElement, Manager, Model, ParentElement, Render, Styled, Task, View, ViewContext, + actions, div, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView, + InteractiveElement, IntoElement, Model, ParentElement, Render, Styled, Task, View, ViewContext, VisualContext, WeakView, }; use picker::{Picker, PickerDelegate}; @@ -111,7 +111,7 @@ impl FileFinder { } } -impl EventEmitter for FileFinder {} +impl EventEmitter for FileFinder {} impl FocusableView for FileFinder { fn focus_handle(&self, cx: &AppContext) -> FocusHandle { self.picker.focus_handle(cx) @@ -690,7 +690,7 @@ impl PickerDelegate for FileFinderDelegate { } } finder - .update(&mut cx, |_, cx| cx.emit(Manager::Dismiss)) + .update(&mut cx, |_, cx| cx.emit(DismissEvent::Dismiss)) .ok()?; Some(()) @@ -702,7 +702,7 @@ impl PickerDelegate for FileFinderDelegate { fn dismissed(&mut self, cx: &mut ViewContext>) { self.file_finder - .update(cx, |_, cx| cx.emit(Manager::Dismiss)) + .update(cx, |_, cx| cx.emit(DismissEvent::Dismiss)) .log_err(); } diff --git a/crates/go_to_line2/src/go_to_line.rs b/crates/go_to_line2/src/go_to_line.rs index 61f5742750a08ea35ffec2592c526e47f8b0a377..d1119de9b454ed83eb809fb6a657669ee061dfcb 100644 --- a/crates/go_to_line2/src/go_to_line.rs +++ b/crates/go_to_line2/src/go_to_line.rs @@ -1,7 +1,8 @@ use editor::{display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Editor}; use gpui::{ - actions, div, prelude::*, AppContext, Div, EventEmitter, FocusHandle, FocusableView, Manager, - Render, SharedString, Styled, Subscription, View, ViewContext, VisualContext, WindowContext, + actions, div, prelude::*, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, + FocusableView, Render, SharedString, Styled, Subscription, View, ViewContext, VisualContext, + WindowContext, }; use text::{Bias, Point}; use theme::ActiveTheme; @@ -28,7 +29,7 @@ impl FocusableView for GoToLine { self.active_editor.focus_handle(cx) } } -impl EventEmitter for GoToLine {} +impl EventEmitter for GoToLine {} impl GoToLine { fn register(workspace: &mut Workspace, _: &mut ViewContext) { @@ -88,7 +89,7 @@ impl GoToLine { ) { match event { // todo!() this isn't working... - editor::EditorEvent::Blurred => cx.emit(Manager::Dismiss), + editor::EditorEvent::Blurred => cx.emit(DismissEvent::Dismiss), editor::EditorEvent::BufferEdited { .. } => self.highlight_current_line(cx), _ => {} } @@ -123,7 +124,7 @@ impl GoToLine { } fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext) { - cx.emit(Manager::Dismiss); + cx.emit(DismissEvent::Dismiss); } fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext) { @@ -140,7 +141,7 @@ impl GoToLine { self.prev_scroll_position.take(); } - cx.emit(Manager::Dismiss); + cx.emit(DismissEvent::Dismiss); } } diff --git a/crates/gpui2/src/app/async_context.rs b/crates/gpui2/src/app/async_context.rs index cc3b0ace57b37d639656ef06fbc5a2c2344b7877..11420bee69d2c8acd9cb179df5e68bcd7c3e9e5b 100644 --- a/crates/gpui2/src/app/async_context.rs +++ b/crates/gpui2/src/app/async_context.rs @@ -1,7 +1,7 @@ use crate::{ - AnyView, AnyWindowHandle, AppCell, AppContext, BackgroundExecutor, Context, FocusableView, - ForegroundExecutor, Manager, Model, ModelContext, Render, Result, Task, View, ViewContext, - VisualContext, WindowContext, WindowHandle, + AnyView, AnyWindowHandle, AppCell, AppContext, BackgroundExecutor, Context, DismissEvent, + FocusableView, ForegroundExecutor, Model, ModelContext, Render, Result, Task, View, + ViewContext, VisualContext, WindowContext, WindowHandle, }; use anyhow::{anyhow, Context as _}; use derive_more::{Deref, DerefMut}; @@ -326,7 +326,7 @@ impl VisualContext for AsyncWindowContext { V: crate::ManagedView, { self.window.update(self, |_, cx| { - view.update(cx, |_, cx| cx.emit(Manager::Dismiss)) + view.update(cx, |_, cx| cx.emit(DismissEvent::Dismiss)) }) } } diff --git a/crates/gpui2/src/app/test_context.rs b/crates/gpui2/src/app/test_context.rs index 2bd3a069caa28c22deb84c156ee590c14029d67d..71bc8e3d8172713e1f2c0ab7d7f5ffb33fe4171e 100644 --- a/crates/gpui2/src/app/test_context.rs +++ b/crates/gpui2/src/app/test_context.rs @@ -611,7 +611,7 @@ impl<'a> VisualContext for VisualTestContext<'a> { { self.window .update(self.cx, |_, cx| { - view.update(cx, |_, cx| cx.emit(crate::Manager::Dismiss)) + view.update(cx, |_, cx| cx.emit(crate::DismissEvent::Dismiss)) }) .unwrap() } diff --git a/crates/gpui2/src/elements/overlay.rs b/crates/gpui2/src/elements/overlay.rs index 3d5c7ac0b551d36a3045240c7afc1d783f684dcb..1a8084ec0adf0e6b005dec2d29b47405016eddfe 100644 --- a/crates/gpui2/src/elements/overlay.rs +++ b/crates/gpui2/src/elements/overlay.rs @@ -145,9 +145,11 @@ impl Element for Overlay { } cx.with_element_offset(desired.origin - bounds.origin, |cx| { - for child in self.children { - child.paint(cx); - } + cx.break_content_mask(|cx| { + for child in self.children { + child.paint(cx); + } + }) }) } } diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 7b39089ae0773295f8cf5d34090acdaa67f10003..5d33f0161c687325d77db16d6dc5ec316236ff2c 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -193,11 +193,11 @@ pub trait FocusableView: 'static + Render { /// ManagedView is a view (like a Modal, Popover, Menu, etc.) /// where the lifecycle of the view is handled by another view. -pub trait ManagedView: FocusableView + EventEmitter {} +pub trait ManagedView: FocusableView + EventEmitter {} -impl> ManagedView for M {} +impl> ManagedView for M {} -pub enum Manager { +pub enum DismissEvent { Dismiss, } @@ -1663,7 +1663,7 @@ impl VisualContext for WindowContext<'_> { where V: ManagedView, { - self.update_view(view, |_, cx| cx.emit(Manager::Dismiss)) + self.update_view(view, |_, cx| cx.emit(DismissEvent::Dismiss)) } } @@ -1752,6 +1752,24 @@ pub trait BorrowWindow: BorrowMut + BorrowMut { } } + /// Invoke the given function with the content mask reset to that + /// of the window. + fn break_content_mask(&mut self, f: impl FnOnce(&mut Self) -> R) -> R { + let mask = ContentMask { + bounds: Bounds { + origin: Point::default(), + size: self.window().viewport_size, + }, + }; + self.window_mut() + .current_frame + .content_mask_stack + .push(mask); + let result = f(self); + self.window_mut().current_frame.content_mask_stack.pop(); + result + } + /// Update the global element offset relative to the current offset. This is used to implement /// scrolling. fn with_element_offset( @@ -2349,7 +2367,7 @@ impl<'a, V: 'static> ViewContext<'a, V> { where V: ManagedView, { - self.defer(|_, cx| cx.emit(Manager::Dismiss)) + self.defer(|_, cx| cx.emit(DismissEvent::Dismiss)) } pub fn listener( diff --git a/crates/ui2/src/components/context_menu.rs b/crates/ui2/src/components/context_menu.rs index d5adaf586be080d3c07f166710a7c254c18760a4..a92c08d82fae0164d0488e350f273df86f7ae48f 100644 --- a/crates/ui2/src/components/context_menu.rs +++ b/crates/ui2/src/components/context_menu.rs @@ -4,9 +4,9 @@ use std::rc::Rc; use crate::{prelude::*, v_stack, Label, List}; use crate::{ListItem, ListSeparator, ListSubHeader}; use gpui::{ - overlay, px, Action, AnchorCorner, AnyElement, AppContext, Bounds, ClickEvent, DispatchPhase, - Div, EventEmitter, FocusHandle, FocusableView, IntoElement, LayoutId, ManagedView, Manager, - MouseButton, MouseDownEvent, Pixels, Point, Render, View, VisualContext, + overlay, px, Action, AnchorCorner, AnyElement, AppContext, Bounds, ClickEvent, DismissEvent, + DispatchPhase, Div, EventEmitter, FocusHandle, FocusableView, IntoElement, LayoutId, + ManagedView, MouseButton, MouseDownEvent, Pixels, Point, Render, View, VisualContext, }; pub enum ContextMenuItem { @@ -26,7 +26,7 @@ impl FocusableView for ContextMenu { } } -impl EventEmitter for ContextMenu {} +impl EventEmitter for ContextMenu {} impl ContextMenu { pub fn build( @@ -74,11 +74,11 @@ impl ContextMenu { pub fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext) { // todo!() - cx.emit(Manager::Dismiss); + cx.emit(DismissEvent::Dismiss); } pub fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext) { - cx.emit(Manager::Dismiss); + cx.emit(DismissEvent::Dismiss); } } @@ -111,7 +111,7 @@ impl Render for ContextMenu { } ContextMenuItem::Entry(entry, callback) => { let callback = callback.clone(); - let dismiss = cx.listener(|_, _, cx| cx.emit(Manager::Dismiss)); + let dismiss = cx.listener(|_, _, cx| cx.emit(DismissEvent::Dismiss)); ListItem::new(entry.clone()) .child(Label::new(entry.clone())) @@ -265,7 +265,7 @@ impl Element for MenuHandle { let new_menu = (builder)(cx); let menu2 = menu.clone(); cx.subscribe(&new_menu, move |modal, e, cx| match e { - &Manager::Dismiss => { + &DismissEvent::Dismiss => { *menu2.borrow_mut() = None; cx.notify(); } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index a19d2c5b58b083619af3c080a379fd2febb67593..268c4f2ca0ea8fa26ecc36d04687f4527692a4f1 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -63,7 +63,7 @@ use crate::{ }; use dock::{Dock, DockPosition, Panel, PanelButtons, PanelHandle}; use lazy_static::lazy_static; -use notifications::{NotificationHandle, NotifyResultExt}; +use notifications::{simple_message_notification, NotificationHandle, NotifyResultExt}; pub use pane::*; pub use pane_group::*; use persistence::{model::SerializedItem, DB}; @@ -776,7 +776,23 @@ impl Workspace { }), ]; - cx.defer(|this, cx| this.update_window_title(cx)); + cx.defer(|this, cx| { + this.update_window_title(cx); + + this.show_notification(0, cx, |cx| { + cx.add_view(|_cx| { + simple_message_notification::MessageNotification::new(format!( + "Error: what happens if this message is very very very very very long " + )) + .with_click_message("Click here because!") + }) + }); + this.show_notification(1, cx, |cx| { + cx.add_view(|_cx| { + simple_message_notification::MessageNotification::new(format!("Nope")) + }) + }); + }); Workspace { weak_self: weak_handle.clone(), modal: None, diff --git a/crates/workspace2/src/notifications.rs b/crates/workspace2/src/notifications.rs index 9b8557c62c0367a5280edcae015e22a3f4960a20..def13c518e17308ef91df1b5cc940c71beae6e77 100644 --- a/crates/workspace2/src/notifications.rs +++ b/crates/workspace2/src/notifications.rs @@ -1,6 +1,9 @@ use crate::{Toast, Workspace}; use collections::HashMap; -use gpui::{AnyView, AppContext, Entity, EntityId, EventEmitter, Render, View, ViewContext}; +use gpui::{ + AnyView, AppContext, AsyncWindowContext, DismissEvent, Entity, EntityId, EventEmitter, Render, + View, ViewContext, VisualContext, +}; use std::{any::TypeId, ops::DerefMut}; pub fn init(cx: &mut AppContext) { @@ -9,13 +12,9 @@ pub fn init(cx: &mut AppContext) { // simple_message_notification::init(cx); } -pub enum NotificationEvent { - Dismiss, -} - -pub trait Notification: EventEmitter + Render {} +pub trait Notification: EventEmitter + Render {} -impl + Render> Notification for V {} +impl + Render> Notification for V {} pub trait NotificationHandle: Send { fn id(&self) -> EntityId; @@ -107,8 +106,8 @@ impl Workspace { let notification = build_notification(cx); cx.subscribe( ¬ification, - move |this, handle, event: &NotificationEvent, cx| match event { - NotificationEvent::Dismiss => { + move |this, handle, event: &DismissEvent, cx| match event { + DismissEvent::Dismiss => { this.dismiss_notification_internal(type_id, id, cx); } }, @@ -120,6 +119,17 @@ impl Workspace { } } + pub fn show_error(&mut self, err: &E, cx: &mut ViewContext) + where + E: std::fmt::Debug, + { + self.show_notification(0, cx, |cx| { + cx.build_view(|_cx| { + simple_message_notification::MessageNotification::new(format!("Error: {err:?}")) + }) + }); + } + pub fn dismiss_notification(&mut self, id: usize, cx: &mut ViewContext) { let type_id = TypeId::of::(); @@ -166,13 +176,14 @@ impl Workspace { } pub mod simple_message_notification { - use super::NotificationEvent; - use gpui::{AnyElement, AppContext, Div, EventEmitter, Render, TextStyle, ViewContext}; + use gpui::{ + div, AnyElement, AppContext, DismissEvent, Div, EventEmitter, InteractiveElement, + ParentElement, Render, SharedString, StatefulInteractiveElement, Styled, TextStyle, + ViewContext, + }; use serde::Deserialize; use std::{borrow::Cow, sync::Arc}; - - // todo!() - // actions!(message_notifications, [CancelMessageNotification]); + use ui::{h_stack, v_stack, Button, Icon, IconElement, Label, StyledExt}; #[derive(Clone, Default, Deserialize, PartialEq)] pub struct OsOpen(pub Cow<'static, str>); @@ -197,22 +208,22 @@ pub mod simple_message_notification { // } enum NotificationMessage { - Text(Cow<'static, str>), + Text(SharedString), Element(fn(TextStyle, &AppContext) -> AnyElement), } pub struct MessageNotification { message: NotificationMessage, on_click: Option) + Send + Sync>>, - click_message: Option>, + click_message: Option, } - impl EventEmitter for MessageNotification {} + impl EventEmitter for MessageNotification {} impl MessageNotification { pub fn new(message: S) -> MessageNotification where - S: Into>, + S: Into, { Self { message: NotificationMessage::Text(message.into()), @@ -221,19 +232,20 @@ pub mod simple_message_notification { } } - pub fn new_element( - message: fn(TextStyle, &AppContext) -> AnyElement, - ) -> MessageNotification { - Self { - message: NotificationMessage::Element(message), - on_click: None, - click_message: None, - } - } + // not needed I think (only for the "new panel" toast, which is outdated now) + // pub fn new_element( + // message: fn(TextStyle, &AppContext) -> AnyElement, + // ) -> MessageNotification { + // Self { + // message: NotificationMessage::Element(message), + // on_click: None, + // click_message: None, + // } + // } pub fn with_click_message(mut self, message: S) -> Self where - S: Into>, + S: Into, { self.click_message = Some(message.into()); self @@ -247,17 +259,43 @@ pub mod simple_message_notification { self } - // todo!() - // pub fn dismiss(&mut self, _: &CancelMessageNotification, cx: &mut ViewContext) { - // cx.emit(MessageNotificationEvent::Dismiss); - // } + pub fn dismiss(&mut self, cx: &mut ViewContext) { + cx.emit(DismissEvent::Dismiss); + } } impl Render for MessageNotification { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { - todo!() + v_stack() + .elevation_3(cx) + .p_4() + .child( + h_stack() + .justify_between() + .child(div().max_w_80().child(match &self.message { + NotificationMessage::Text(text) => Label::new(text.clone()), + NotificationMessage::Element(element) => { + todo!() + } + })) + .child( + div() + .id("cancel") + .child(IconElement::new(Icon::Close)) + .cursor_pointer() + .on_click(cx.listener(|this, event, cx| this.dismiss(cx))), + ), + ) + .children(self.click_message.iter().map(|message| { + Button::new(message.clone()).on_click(cx.listener(|this, _, cx| { + if let Some(on_click) = this.on_click.as_ref() { + (on_click)(cx) + }; + this.dismiss(cx) + })) + })) } } // todo!() @@ -359,8 +397,6 @@ pub mod simple_message_notification { // .into_any() // } // } - - impl EventEmitter for MessageNotification {} } pub trait NotifyResultExt { @@ -371,6 +407,8 @@ pub trait NotifyResultExt { workspace: &mut Workspace, cx: &mut ViewContext, ) -> Option; + + fn notify_async_err(self, cx: &mut AsyncWindowContext) -> Option; } impl NotifyResultExt for Result @@ -384,14 +422,23 @@ where Ok(value) => Some(value), Err(err) => { log::error!("TODO {err:?}"); - // todo!() - // workspace.show_notification(0, cx, |cx| { - // cx.add_view(|_cx| { - // simple_message_notification::MessageNotification::new(format!( - // "Error: {err:?}", - // )) - // }) - // }); + workspace.show_error(&err, cx); + None + } + } + } + + fn notify_async_err(self, cx: &mut AsyncWindowContext) -> Option { + match self { + Ok(value) => Some(value), + Err(err) => { + log::error!("TODO {err:?}"); + cx.update(|view, cx| { + if let Ok(workspace) = view.downcast::() { + workspace.update(cx, |workspace, cx| workspace.show_error(&err, cx)) + } + }) + .ok(); None } } diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index b09b47d24c47551e530ff3d9d08c3e129b3f1a22..5480ac4d3c597b161cf6f14cf7ee7b00cfb5eca5 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -683,7 +683,21 @@ impl Workspace { }), ]; - cx.defer(|this, cx| this.update_window_title(cx)); + cx.defer(|this, cx| { + this.update_window_title(cx); + // todo! @nate - these are useful for testing notifications + // this.show_error( + // &anyhow::anyhow!("what happens if this message is very very very very very long"), + // cx, + // ); + + // this.show_notification(1, cx, |cx| { + // cx.build_view(|_cx| { + // simple_message_notification::MessageNotification::new(format!("Error:")) + // .with_click_message("click here because!") + // }) + // }); + }); Workspace { window_self: window_handle, weak_self: weak_handle.clone(), @@ -2566,32 +2580,31 @@ impl Workspace { // } // } - // fn render_notifications( - // &self, - // theme: &theme::Workspace, - // cx: &AppContext, - // ) -> Option> { - // if self.notifications.is_empty() { - // None - // } else { - // Some( - // Flex::column() - // .with_children(self.notifications.iter().map(|(_, _, notification)| { - // ChildView::new(notification.as_any(), cx) - // .contained() - // .with_style(theme.notification) - // })) - // .constrained() - // .with_width(theme.notifications.width) - // .contained() - // .with_style(theme.notifications.container) - // .aligned() - // .bottom() - // .right() - // .into_any(), - // ) - // } - // } + fn render_notifications(&self, cx: &ViewContext) -> Option
{ + if self.notifications.is_empty() { + None + } else { + Some( + div() + .absolute() + .z_index(100) + .right_3() + .bottom_3() + .w_96() + .h_full() + .flex() + .flex_col() + .justify_end() + .gap_2() + .children(self.notifications.iter().map(|(_, _, notification)| { + div() + .on_any_mouse_down(|_, cx| cx.stop_propagation()) + .on_any_mouse_up(|_, cx| cx.stop_propagation()) + .child(notification.to_any()) + })), + ) + } + } // // RPC handlers @@ -3653,7 +3666,6 @@ impl Render for Workspace { .bg(cx.theme().colors().background) .children(self.titlebar_item.clone()) .child( - // todo! should this be a component a view? div() .id("workspace") .relative() @@ -3703,7 +3715,8 @@ impl Render for Workspace { .overflow_hidden() .child(self.right_dock.clone()), ), - ), + ) + .children(self.render_notifications(cx)), ) .child(self.status_bar.clone()) }