diff --git a/assets/icons/jump.svg b/assets/icons/jump.svg
new file mode 100644
index 0000000000000000000000000000000000000000..4e89fc433964e88b5d255f5851b1aa51092dd4ca
--- /dev/null
+++ b/assets/icons/jump.svg
@@ -0,0 +1,3 @@
+
diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs
index b5361b4e5b0d7595bde196064c3e18bceca0500c..f39eab68adccb650fcf2d7aba173bc3e2250af6f 100644
--- a/crates/diagnostics/src/diagnostics.rs
+++ b/crates/diagnostics/src/diagnostics.rs
@@ -8,12 +8,13 @@ use editor::{
highlight_diagnostic_message, Autoscroll, Editor, ExcerptId, MultiBuffer, ToOffset,
};
use gpui::{
- actions, elements::*, fonts::TextStyle, serde_json, AnyViewHandle, AppContext, Entity,
- ModelHandle, MutableAppContext, RenderContext, Task, View, ViewContext, ViewHandle,
- WeakViewHandle,
+ actions, elements::*, fonts::TextStyle, impl_internal_actions, platform::CursorStyle,
+ serde_json, AnyViewHandle, AppContext, Entity, ModelHandle, MutableAppContext, RenderContext,
+ Task, View, ViewContext, ViewHandle, WeakViewHandle,
};
use language::{
- Bias, Buffer, Diagnostic, DiagnosticEntry, DiagnosticSeverity, Point, Selection, SelectionGoal,
+ Anchor, Bias, Buffer, Diagnostic, DiagnosticEntry, DiagnosticSeverity, Point, Selection,
+ SelectionGoal, ToPoint,
};
use project::{DiagnosticSummary, Project, ProjectPath};
use serde_json::json;
@@ -27,15 +28,18 @@ use std::{
path::PathBuf,
sync::Arc,
};
-use util::TryFutureExt;
+use util::{ResultExt, TryFutureExt};
use workspace::{ItemHandle as _, ItemNavHistory, Workspace};
actions!(diagnostics, [Deploy]);
+impl_internal_actions!(diagnostics, [Jump]);
+
const CONTEXT_LINE_COUNT: u32 = 1;
pub fn init(cx: &mut MutableAppContext) {
cx.add_action(ProjectDiagnosticsEditor::deploy);
+ cx.add_action(ProjectDiagnosticsEditor::jump);
items::init(cx);
}
@@ -56,6 +60,13 @@ struct PathState {
diagnostic_groups: Vec,
}
+#[derive(Clone, Debug)]
+struct Jump {
+ path: ProjectPath,
+ position: Point,
+ anchor: Anchor,
+}
+
struct DiagnosticGroupState {
primary_diagnostic: DiagnosticEntry,
primary_excerpt_ix: usize,
@@ -177,6 +188,30 @@ impl ProjectDiagnosticsEditor {
}
}
+ fn jump(workspace: &mut Workspace, action: &Jump, cx: &mut ViewContext) {
+ let editor = workspace.open_path(action.path.clone(), true, cx);
+ let position = action.position;
+ let anchor = action.anchor;
+ cx.spawn_weak(|_, mut cx| async move {
+ let editor = editor.await.log_err()?.downcast::()?;
+ editor.update(&mut cx, |editor, cx| {
+ let buffer = editor.buffer().read(cx).as_singleton()?;
+ let buffer = buffer.read(cx);
+ let cursor = if buffer.can_resolve(&anchor) {
+ anchor.to_point(buffer)
+ } else {
+ buffer.clip_point(position, Bias::Left)
+ };
+ editor.change_selections(Some(Autoscroll::Newest), cx, |s| {
+ s.select_ranges([cursor..cursor]);
+ });
+ Some(())
+ })?;
+ Some(())
+ })
+ .detach()
+ }
+
fn update_excerpts(&mut self, cx: &mut ViewContext) {
let paths = mem::take(&mut self.paths_to_update);
let project = self.project.clone();
@@ -312,13 +347,20 @@ impl ProjectDiagnosticsEditor {
is_first_excerpt_for_group = false;
let mut primary =
group.entries[group.primary_ix].diagnostic.clone();
+ let anchor = group.entries[group.primary_ix].range.start;
+ let position = anchor.to_point(&snapshot);
primary.message =
primary.message.split('\n').next().unwrap().to_string();
group_state.block_count += 1;
blocks_to_add.push(BlockProperties {
position: header_position,
height: 2,
- render: diagnostic_header_renderer(primary),
+ render: diagnostic_header_renderer(
+ primary,
+ path.clone(),
+ position,
+ anchor,
+ ),
disposition: BlockDisposition::Above,
});
}
@@ -575,12 +617,20 @@ impl workspace::Item for ProjectDiagnosticsEditor {
}
}
-fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
+fn diagnostic_header_renderer(
+ diagnostic: Diagnostic,
+ path: ProjectPath,
+ position: Point,
+ anchor: Anchor,
+) -> RenderBlock {
+ enum JumpIcon {}
+
let (message, highlights) = highlight_diagnostic_message(&diagnostic.message);
Arc::new(move |cx| {
let settings = cx.global::();
+ let tooltip_style = settings.theme.tooltip.clone();
let theme = &settings.theme.editor;
- let style = &theme.diagnostic_header;
+ let style = theme.diagnostic_header.clone();
let font_size = (style.text_scale_factor * settings.buffer_font_size).round();
let icon_width = cx.em_width * style.icon_width_factor;
let icon = if diagnostic.severity == DiagnosticSeverity::ERROR {
@@ -591,6 +641,7 @@ fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
.with_color(theme.warning_diagnostic.message.text.color)
};
+ let x_padding = cx.gutter_padding + cx.scroll_x * cx.em_width;
Flex::row()
.with_child(
icon.constrained()
@@ -618,9 +669,47 @@ fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
.aligned()
.boxed()
}))
+ .with_child(
+ MouseEventHandler::new::(diagnostic.group_id, cx, |state, _| {
+ let style = style.jump_icon.style_for(state, false);
+ Svg::new("icons/jump.svg")
+ .with_color(style.color)
+ .constrained()
+ .with_width(style.icon_width)
+ .aligned()
+ .contained()
+ .with_style(style.container)
+ .constrained()
+ .with_width(style.button_width)
+ .with_height(style.button_width)
+ .boxed()
+ })
+ .with_cursor_style(CursorStyle::PointingHand)
+ .on_click({
+ let path = path.clone();
+ move |_, _, cx| {
+ cx.dispatch_action(Jump {
+ path: path.clone(),
+ position,
+ anchor,
+ });
+ }
+ })
+ .with_tooltip(
+ diagnostic.group_id,
+ "Jump to diagnostic".to_string(),
+ Some(Box::new(editor::OpenExcerpts)),
+ tooltip_style,
+ cx,
+ )
+ .aligned()
+ .flex_float()
+ .boxed(),
+ )
.contained()
.with_style(style.container)
- .with_padding_left(cx.gutter_padding + cx.scroll_x * cx.em_width)
+ .with_padding_left(x_padding)
+ .with_padding_right(x_padding)
.expanded()
.named("diagnostic header")
})
@@ -702,7 +791,7 @@ mod tests {
use super::*;
use editor::{
display_map::{BlockContext, TransformBlock},
- DisplayPoint, EditorSnapshot,
+ DisplayPoint,
};
use gpui::TestAppContext;
use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16};
@@ -835,10 +924,8 @@ mod tests {
view.next_notification(&cx).await;
view.update(cx, |view, cx| {
- let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
-
assert_eq!(
- editor_blocks(&editor, cx),
+ editor_blocks(&view.editor, cx),
[
(0, "path header block".into()),
(2, "diagnostic header".into()),
@@ -848,7 +935,7 @@ mod tests {
]
);
assert_eq!(
- editor.text(),
+ view.editor.update(cx, |editor, cx| editor.display_text(cx)),
concat!(
//
// main.rs
@@ -923,10 +1010,8 @@ mod tests {
view.next_notification(&cx).await;
view.update(cx, |view, cx| {
- let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
-
assert_eq!(
- editor_blocks(&editor, cx),
+ editor_blocks(&view.editor, cx),
[
(0, "path header block".into()),
(2, "diagnostic header".into()),
@@ -938,7 +1023,7 @@ mod tests {
]
);
assert_eq!(
- editor.text(),
+ view.editor.update(cx, |editor, cx| editor.display_text(cx)),
concat!(
//
// consts.rs
@@ -1038,10 +1123,8 @@ mod tests {
view.next_notification(&cx).await;
view.update(cx, |view, cx| {
- let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
-
assert_eq!(
- editor_blocks(&editor, cx),
+ editor_blocks(&view.editor, cx),
[
(0, "path header block".into()),
(2, "diagnostic header".into()),
@@ -1055,7 +1138,7 @@ mod tests {
]
);
assert_eq!(
- editor.text(),
+ view.editor.update(cx, |editor, cx| editor.display_text(cx)),
concat!(
//
// consts.rs
@@ -1115,36 +1198,44 @@ mod tests {
});
}
- fn editor_blocks(editor: &EditorSnapshot, cx: &AppContext) -> Vec<(u32, String)> {
- editor
- .blocks_in_range(0..editor.max_point().row())
- .filter_map(|(row, block)| {
- let name = match block {
- TransformBlock::Custom(block) => block
- .render(&BlockContext {
- cx,
- anchor_x: 0.,
- scroll_x: 0.,
- gutter_padding: 0.,
- gutter_width: 0.,
- line_height: 0.,
- em_width: 0.,
- })
- .name()?
- .to_string(),
- TransformBlock::ExcerptHeader {
- starts_new_buffer, ..
- } => {
- if *starts_new_buffer {
- "path header block".to_string()
- } else {
- "collapsed context".to_string()
+ fn editor_blocks(
+ editor: &ViewHandle,
+ cx: &mut MutableAppContext,
+ ) -> Vec<(u32, String)> {
+ let mut presenter = cx.build_presenter(editor.id(), 0.);
+ let mut cx = presenter.build_layout_context(Default::default(), false, cx);
+ cx.render(editor, |editor, cx| {
+ let snapshot = editor.snapshot(cx);
+ snapshot
+ .blocks_in_range(0..snapshot.max_point().row())
+ .filter_map(|(row, block)| {
+ let name = match block {
+ TransformBlock::Custom(block) => block
+ .render(&mut BlockContext {
+ cx,
+ anchor_x: 0.,
+ scroll_x: 0.,
+ gutter_padding: 0.,
+ gutter_width: 0.,
+ line_height: 0.,
+ em_width: 0.,
+ })
+ .name()?
+ .to_string(),
+ TransformBlock::ExcerptHeader {
+ starts_new_buffer, ..
+ } => {
+ if *starts_new_buffer {
+ "path header block".to_string()
+ } else {
+ "collapsed context".to_string()
+ }
}
- }
- };
+ };
- Some((row, name))
- })
- .collect()
+ Some((row, name))
+ })
+ .collect()
+ })
}
}
diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs
index c5c06048e58d6c3ce9f210d98a878bc2ae240163..bdfff4862c41d156cfad9c5bf8376e0e84a6a9e7 100644
--- a/crates/editor/src/display_map/block_map.rs
+++ b/crates/editor/src/display_map/block_map.rs
@@ -4,14 +4,14 @@ use super::{
};
use crate::{Anchor, ToPoint as _};
use collections::{Bound, HashMap, HashSet};
-use gpui::{AppContext, ElementBox};
+use gpui::{ElementBox, RenderContext};
use language::{BufferSnapshot, Chunk, Patch};
use parking_lot::Mutex;
use std::{
cell::RefCell,
cmp::{self, Ordering},
fmt::Debug,
- ops::{Deref, Range},
+ ops::{Deref, DerefMut, Range},
sync::{
atomic::{AtomicUsize, Ordering::SeqCst},
Arc,
@@ -50,7 +50,7 @@ struct BlockRow(u32);
#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
struct WrapRow(u32);
-pub type RenderBlock = Arc ElementBox>;
+pub type RenderBlock = Arc ElementBox>;
pub struct Block {
id: BlockId,
@@ -67,12 +67,12 @@ where
{
pub position: P,
pub height: u8,
- pub render: Arc ElementBox>,
+ pub render: Arc ElementBox>,
pub disposition: BlockDisposition,
}
-pub struct BlockContext<'a> {
- pub cx: &'a AppContext,
+pub struct BlockContext<'a, 'b> {
+ pub cx: &'b mut RenderContext<'a, crate::Editor>,
pub anchor_x: f32,
pub scroll_x: f32,
pub gutter_width: f32,
@@ -916,16 +916,22 @@ impl BlockDisposition {
}
}
-impl<'a> Deref for BlockContext<'a> {
- type Target = AppContext;
+impl<'a, 'b> Deref for BlockContext<'a, 'b> {
+ type Target = RenderContext<'a, crate::Editor>;
fn deref(&self) -> &Self::Target {
- &self.cx
+ self.cx
+ }
+}
+
+impl<'a, 'b> DerefMut for BlockContext<'a, 'b> {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ self.cx
}
}
impl Block {
- pub fn render(&self, cx: &BlockContext) -> ElementBox {
+ pub fn render(&self, cx: &mut BlockContext) -> ElementBox {
self.render.lock()(cx)
}
@@ -1008,7 +1014,7 @@ mod tests {
let mut block_map = BlockMap::new(wraps_snapshot.clone(), 1, 1);
let mut writer = block_map.write(wraps_snapshot.clone(), Default::default());
- writer.insert(vec![
+ let block_ids = writer.insert(vec![
BlockProperties {
position: buffer_snapshot.anchor_after(Point::new(1, 0)),
height: 1,
@@ -1036,22 +1042,7 @@ mod tests {
.blocks_in_range(0..8)
.map(|(start_row, block)| {
let block = block.as_custom().unwrap();
- (
- start_row..start_row + block.height as u32,
- block
- .render(&BlockContext {
- cx,
- anchor_x: 0.,
- gutter_padding: 0.,
- scroll_x: 0.,
- gutter_width: 0.,
- line_height: 0.,
- em_width: 0.,
- })
- .name()
- .unwrap()
- .to_string(),
- )
+ (start_row..start_row + block.height as u32, block.id)
})
.collect::>();
@@ -1059,9 +1050,9 @@ mod tests {
assert_eq!(
blocks,
&[
- (1..2, "block 1".to_string()),
- (2..4, "block 2".to_string()),
- (7..10, "block 3".to_string()),
+ (1..2, block_ids[0]),
+ (2..4, block_ids[1]),
+ (7..10, block_ids[2]),
]
);
diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs
index d82a5baece26eed60bff60eefc043f517d9dd8df..32773c9d28ac010c39e9b05a8183df96a202e3fb 100644
--- a/crates/editor/src/editor.rs
+++ b/crates/editor/src/editor.rs
@@ -4745,7 +4745,7 @@ impl Editor {
height: 1,
render: Arc::new({
let editor = rename_editor.clone();
- move |cx: &BlockContext| {
+ move |cx: &mut BlockContext| {
ChildView::new(editor.clone())
.contained()
.with_padding_left(cx.anchor_x)
@@ -5866,7 +5866,7 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, is_valid: bool) -> Rend
highlighted_lines.push(highlight_diagnostic_message(line));
}
- Arc::new(move |cx: &BlockContext| {
+ Arc::new(move |cx: &mut BlockContext| {
let settings = cx.global::();
let theme = &settings.theme.editor;
let style = diagnostic_style(diagnostic.severity, is_valid, theme);
diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs
index d5a4f22eb563d6525c9c5594fb6c9af67de84ee8..771bc1049c5131db9d9e7c59386693685f46f864 100644
--- a/crates/editor/src/element.rs
+++ b/crates/editor/src/element.rs
@@ -755,6 +755,12 @@ impl EditorElement {
line_layouts: &[text_layout::Line],
cx: &mut LayoutContext,
) -> Vec<(u32, ElementBox)> {
+ let editor = if let Some(editor) = self.view.upgrade(cx) {
+ editor
+ } else {
+ return Default::default();
+ };
+
let scroll_x = snapshot.scroll_position.x();
snapshot
.blocks_in_range(rows.clone())
@@ -774,14 +780,16 @@ impl EditorElement {
.x_for_index(align_to.column() as usize)
};
- block.render(&BlockContext {
- cx,
- anchor_x,
- gutter_padding,
- line_height,
- scroll_x,
- gutter_width,
- em_width,
+ cx.render(&editor, |_, cx| {
+ block.render(&mut BlockContext {
+ cx,
+ anchor_x,
+ gutter_padding,
+ line_height,
+ scroll_x,
+ gutter_width,
+ em_width,
+ })
})
}
TransformBlock::ExcerptHeader {
@@ -1611,7 +1619,7 @@ mod tests {
// Don't panic.
let bounds = RectF::new(Default::default(), size);
- let mut paint_cx = presenter.build_paint_context(&mut scene, cx);
+ let mut paint_cx = presenter.build_paint_context(&mut scene, bounds.size(), cx);
element.paint(bounds, bounds, &mut state, &mut paint_cx);
}
}
diff --git a/crates/gpui/src/elements.rs b/crates/gpui/src/elements.rs
index 231339d9e07ed232bb61b1ac7c0ba38c2685c949..d2d254d93eb1d7da3949d60bbfdc011b7fa65293 100644
--- a/crates/gpui/src/elements.rs
+++ b/crates/gpui/src/elements.rs
@@ -16,13 +16,14 @@ mod overlay;
mod stack;
mod svg;
mod text;
+mod tooltip;
mod uniform_list;
use self::expanded::Expanded;
pub use self::{
align::*, canvas::*, constrained_box::*, container::*, empty::*, event_handler::*, flex::*,
hook::*, image::*, keystroke_label::*, label::*, list::*, mouse_event_handler::*, overlay::*,
- stack::*, svg::*, text::*, uniform_list::*,
+ stack::*, svg::*, text::*, tooltip::*, uniform_list::*,
};
pub use crate::presenter::ChildView;
use crate::{
@@ -30,7 +31,8 @@ use crate::{
rect::RectF,
vector::{vec2f, Vector2F},
},
- json, DebugContext, Event, EventContext, LayoutContext, PaintContext, SizeConstraint,
+ json, Action, DebugContext, Event, EventContext, LayoutContext, PaintContext, RenderContext,
+ SizeConstraint, View,
};
use core::panic;
use json::ToJson;
@@ -154,6 +156,20 @@ pub trait Element {
{
FlexItem::new(self.boxed()).float()
}
+
+ fn with_tooltip(
+ self,
+ id: usize,
+ text: String,
+ action: Option>,
+ style: TooltipStyle,
+ cx: &mut RenderContext,
+ ) -> Tooltip
+ where
+ Self: 'static + Sized,
+ {
+ Tooltip::new(id, text, action, style, self.boxed(), cx)
+ }
}
pub enum Lifecycle {
diff --git a/crates/gpui/src/elements/mouse_event_handler.rs b/crates/gpui/src/elements/mouse_event_handler.rs
index 2ad6eaf028fc9bb71426135a705b9cb4dbab69d0..1dea333400fd17d56b7e5957683b11702e7bb5e9 100644
--- a/crates/gpui/src/elements/mouse_event_handler.rs
+++ b/crates/gpui/src/elements/mouse_event_handler.rs
@@ -25,6 +25,7 @@ pub struct MouseEventHandler {
mouse_down_out: Option>,
right_mouse_down_out: Option>,
drag: Option>,
+ hover: Option>,
padding: Padding,
}
@@ -47,6 +48,7 @@ impl MouseEventHandler {
mouse_down_out: None,
right_mouse_down_out: None,
drag: None,
+ hover: None,
padding: Default::default(),
}
}
@@ -109,6 +111,14 @@ impl MouseEventHandler {
self
}
+ pub fn on_hover(
+ mut self,
+ handler: impl Fn(Vector2F, bool, &mut EventContext) + 'static,
+ ) -> Self {
+ self.hover = Some(Rc::new(handler));
+ self
+ }
+
pub fn with_padding(mut self, padding: Padding) -> Self {
self.padding = padding;
self
@@ -153,7 +163,7 @@ impl Element for MouseEventHandler {
view_id: cx.current_view_id(),
discriminant: Some((self.tag, self.id)),
bounds: self.hit_bounds(bounds),
- hover: None,
+ hover: self.hover.clone(),
click: self.click.clone(),
mouse_down: self.mouse_down.clone(),
right_click: self.right_click.clone(),
diff --git a/crates/gpui/src/elements/tooltip.rs b/crates/gpui/src/elements/tooltip.rs
new file mode 100644
index 0000000000000000000000000000000000000000..9ed0f5cba6ac86f56c2ccee040608a461688fbec
--- /dev/null
+++ b/crates/gpui/src/elements/tooltip.rs
@@ -0,0 +1,217 @@
+use super::{
+ ContainerStyle, Element, ElementBox, Flex, KeystrokeLabel, MouseEventHandler, ParentElement,
+ Text,
+};
+use crate::{
+ fonts::TextStyle,
+ geometry::{rect::RectF, vector::Vector2F},
+ json::json,
+ Action, Axis, ElementStateHandle, LayoutContext, PaintContext, RenderContext, SizeConstraint,
+ Task, View,
+};
+use serde::Deserialize;
+use std::{
+ cell::{Cell, RefCell},
+ rc::Rc,
+ time::Duration,
+};
+
+const DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(500);
+
+pub struct Tooltip {
+ child: ElementBox,
+ tooltip: Option,
+ state: ElementStateHandle>,
+}
+
+#[derive(Default)]
+struct TooltipState {
+ visible: Cell,
+ position: Cell,
+ debounce: RefCell