From a7145021b6e799df6dd795af621396607d55c392 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 25 Apr 2023 14:05:23 -0700 Subject: [PATCH 01/14] Extract a named struct from text_layout::Line's style runs --- crates/gpui/src/text_layout.rs | 37 +++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/crates/gpui/src/text_layout.rs b/crates/gpui/src/text_layout.rs index e665859319..b557afc319 100644 --- a/crates/gpui/src/text_layout.rs +++ b/crates/gpui/src/text_layout.rs @@ -177,7 +177,14 @@ impl<'a> Hash for CacheKeyRef<'a> { #[derive(Default, Debug, Clone)] pub struct Line { layout: Arc, - style_runs: SmallVec<[(u32, Color, Underline); 32]>, + style_runs: SmallVec<[StyleRun; 32]>, +} + +#[derive(Debug, Clone, Copy)] +struct StyleRun { + len: u32, + color: Color, + underline: Underline, } #[derive(Default, Debug)] @@ -208,7 +215,11 @@ impl Line { fn new(layout: Arc, runs: &[(usize, RunStyle)]) -> Self { let mut style_runs = SmallVec::new(); for (len, style) in runs { - style_runs.push((*len as u32, style.color, style.underline)); + style_runs.push(StyleRun { + len: *len as u32, + color: style.color, + underline: style.underline, + }); } Self { layout, style_runs } } @@ -301,28 +312,30 @@ impl Line { let mut finished_underline = None; if glyph.index >= run_end { - if let Some((run_len, run_color, run_underline)) = style_runs.next() { + if let Some(style_run) = style_runs.next() { if let Some((_, underline_style)) = underline { - if *run_underline != underline_style { + if style_run.underline != underline_style { finished_underline = underline.take(); } } - if run_underline.thickness.into_inner() > 0. { + if style_run.underline.thickness.into_inner() > 0. { underline.get_or_insert(( vec2f( glyph_origin.x(), origin.y() + baseline_offset.y() + 0.618 * self.layout.descent, ), Underline { - color: Some(run_underline.color.unwrap_or(*run_color)), - thickness: run_underline.thickness, - squiggly: run_underline.squiggly, + color: Some( + style_run.underline.color.unwrap_or(style_run.color), + ), + thickness: style_run.underline.thickness, + squiggly: style_run.underline.squiggly, }, )); } - run_end += *run_len as usize; - color = *run_color; + run_end += style_run.len as usize; + color = style_run.color; } else { run_end = self.layout.len; finished_underline = underline.take(); @@ -405,8 +418,8 @@ impl Line { if glyph.index >= color_end { if let Some(next_run) = color_runs.next() { - color_end += next_run.0 as usize; - color = next_run.1; + color_end += next_run.len as usize; + color = next_run.color; } else { color_end = self.layout.len; color = Color::black(); From 1bbcff543b24e32f8a0a73f7882fd0200cf7044b Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 25 Apr 2023 17:23:55 -0700 Subject: [PATCH 02/14] Add API for adding mouse regions within Text --- crates/gpui/examples/text.rs | 137 ++++++++-------------------- crates/gpui/src/elements/text.rs | 152 +++++++++++++++++++++++++++++-- crates/gpui/src/text_layout.rs | 82 ++++++++++++++--- 3 files changed, 251 insertions(+), 120 deletions(-) diff --git a/crates/gpui/examples/text.rs b/crates/gpui/examples/text.rs index 10aa61fde5..f60e74deb1 100644 --- a/crates/gpui/examples/text.rs +++ b/crates/gpui/examples/text.rs @@ -1,13 +1,12 @@ use gpui::{ color::Color, - fonts::{Properties, Weight}, - text_layout::RunStyle, - AnyElement, Element, Quad, SceneBuilder, View, ViewContext, + elements::Text, + fonts::{HighlightStyle, TextStyle}, + platform::MouseButton, + AnyElement, Element, MouseRegion, }; use log::LevelFilter; -use pathfinder_geometry::rect::RectF; use simplelog::SimpleLogger; -use std::ops::Range; fn main() { SimpleLogger::init(LevelFilter::Info, Default::default()).expect("could not initialize logger"); @@ -19,7 +18,6 @@ fn main() { } struct TextView; -struct TextElement; impl gpui::Entity for TextView { type Event = (); @@ -30,104 +28,47 @@ impl gpui::View for TextView { "View" } - fn render(&mut self, _: &mut gpui::ViewContext) -> AnyElement { - TextElement.into_any() - } -} - -impl Element for TextElement { - type LayoutState = (); - - type PaintState = (); - - fn layout( - &mut self, - constraint: gpui::SizeConstraint, - _: &mut V, - _: &mut ViewContext, - ) -> (pathfinder_geometry::vector::Vector2F, Self::LayoutState) { - (constraint.max, ()) - } - - fn paint( - &mut self, - scene: &mut SceneBuilder, - bounds: RectF, - visible_bounds: RectF, - _: &mut Self::LayoutState, - _: &mut V, - cx: &mut ViewContext, - ) -> Self::PaintState { + fn render(&mut self, cx: &mut gpui::ViewContext) -> AnyElement { let font_size = 12.; let family = cx .font_cache - .load_family(&["SF Pro Display"], &Default::default()) + .load_family(&["Monaco"], &Default::default()) .unwrap(); - let normal = RunStyle { - font_id: cx - .font_cache - .select_font(family, &Default::default()) - .unwrap(), - color: Color::default(), - underline: Default::default(), - }; - let bold = RunStyle { - font_id: cx - .font_cache - .select_font( - family, - &Properties { - weight: Weight::BOLD, - ..Default::default() - }, - ) - .unwrap(), - color: Color::default(), - underline: Default::default(), - }; + let font_id = cx + .font_cache + .select_font(family, &Default::default()) + .unwrap(); + let view_id = cx.view_id(); - let text = "Hello world!"; - let line = cx.text_layout_cache().layout_str( - text, - font_size, - &[ - (1, normal), - (1, bold), - (1, normal), - (1, bold), - (text.len() - 4, normal), - ], - ); - - scene.push_quad(Quad { - bounds, - background: Some(Color::white()), + let underline = HighlightStyle { + underline: Some(gpui::fonts::Underline { + thickness: 1.0.into(), + ..Default::default() + }), ..Default::default() - }); - line.paint(scene, bounds.origin(), visible_bounds, bounds.height(), cx); - } + }; - fn rect_for_text_range( - &self, - _: Range, - _: RectF, - _: RectF, - _: &Self::LayoutState, - _: &Self::PaintState, - _: &V, - _: &ViewContext, - ) -> Option { - None - } - - fn debug( - &self, - _: RectF, - _: &Self::LayoutState, - _: &Self::PaintState, - _: &V, - _: &ViewContext, - ) -> gpui::json::Value { - todo!() + Text::new( + "The text:\nHello, beautiful world, hello!", + TextStyle { + font_id, + font_size, + color: Color::red(), + font_family_name: "".into(), + font_family_id: family, + underline: Default::default(), + font_properties: Default::default(), + }, + ) + .with_highlights(vec![(17..26, underline), (34..40, underline)]) + .with_mouse_regions(vec![(17..26), (34..40)], move |ix, bounds| { + MouseRegion::new::(view_id, ix, bounds).on_click::( + MouseButton::Left, + move |_, _, _| { + eprintln!("clicked link {ix}"); + }, + ) + }) + .into_any() } } diff --git a/crates/gpui/src/elements/text.rs b/crates/gpui/src/elements/text.rs index 3090a81c72..357bce9d0d 100644 --- a/crates/gpui/src/elements/text.rs +++ b/crates/gpui/src/elements/text.rs @@ -6,8 +6,10 @@ use crate::{ vector::{vec2f, Vector2F}, }, json::{ToJson, Value}, + platform::CursorStyle, text_layout::{Line, RunStyle, ShapedBoundary}, - Element, FontCache, SceneBuilder, SizeConstraint, TextLayoutCache, View, ViewContext, + CursorRegion, Element, FontCache, MouseRegion, SceneBuilder, SizeConstraint, TextLayoutCache, + View, ViewContext, }; use log::warn; use serde_json::json; @@ -17,7 +19,11 @@ pub struct Text { text: Cow<'static, str>, style: TextStyle, soft_wrap: bool, - highlights: Vec<(Range, HighlightStyle)>, + highlights: Option, HighlightStyle)]>>, + mouse_runs: Option<( + Box<[Range]>, + Box MouseRegion>, + )>, } pub struct LayoutState { @@ -32,7 +38,8 @@ impl Text { text: text.into(), style, soft_wrap: true, - highlights: Vec::new(), + highlights: None, + mouse_runs: None, } } @@ -41,8 +48,20 @@ impl Text { self } - pub fn with_highlights(mut self, runs: Vec<(Range, HighlightStyle)>) -> Self { - self.highlights = runs; + pub fn with_highlights( + mut self, + runs: impl Into, HighlightStyle)]>>, + ) -> Self { + self.highlights = Some(runs.into()); + self + } + + pub fn with_mouse_regions( + mut self, + runs: impl Into]>>, + build_mouse_region: impl 'static + FnMut(usize, RectF) -> MouseRegion, + ) -> Self { + self.mouse_runs = Some((runs.into(), Box::new(build_mouse_region))); self } @@ -65,7 +84,12 @@ impl Element for Text { // Convert the string and highlight ranges into an iterator of highlighted chunks. let mut offset = 0; - let mut highlight_ranges = self.highlights.iter().peekable(); + let mut highlight_ranges = self + .highlights + .as_ref() + .map_or(Default::default(), AsRef::as_ref) + .iter() + .peekable(); let chunks = std::iter::from_fn(|| { let result; if let Some((range, highlight_style)) = highlight_ranges.peek() { @@ -152,6 +176,19 @@ impl Element for Text { ) -> Self::PaintState { let mut origin = bounds.origin(); let empty = Vec::new(); + + let mouse_runs; + let mut build_mouse_region; + if let Some((runs, build_region)) = &mut self.mouse_runs { + mouse_runs = runs.iter(); + build_mouse_region = Some(build_region); + } else { + mouse_runs = [].iter(); + build_mouse_region = None; + } + let mut mouse_runs = mouse_runs.enumerate().peekable(); + + let mut offset = 0; for (ix, line) in layout.shaped_lines.iter().enumerate() { let wrap_boundaries = layout.wrap_boundaries.get(ix).unwrap_or(&empty); let boundaries = RectF::new( @@ -169,13 +206,114 @@ impl Element for Text { origin, visible_bounds, layout.line_height, - wrap_boundaries.iter().copied(), + wrap_boundaries, cx, ); } else { line.paint(scene, origin, visible_bounds, layout.line_height, cx); } } + + // Add the mouse regions + let end_offset = offset + line.len(); + if let Some((mut mouse_run_ix, mut mouse_run_range)) = mouse_runs.peek().cloned() { + if mouse_run_range.start < end_offset { + let mut current_mouse_run = None; + if mouse_run_range.start <= offset { + current_mouse_run = Some((mouse_run_ix, origin)); + } + + let mut glyph_origin = origin; + let mut prev_position = 0.; + let mut wrap_boundaries = wrap_boundaries.iter().copied().peekable(); + for (glyph_ix, glyph) in line + .runs() + .iter() + .flat_map(|run| run.glyphs().iter().enumerate()) + { + glyph_origin.set_x(glyph_origin.x() + glyph.position.x() - prev_position); + prev_position = glyph.position.x(); + + if wrap_boundaries + .peek() + .map_or(false, |b| b.glyph_ix == glyph_ix) + { + if let Some((mouse_run_ix, mouse_region_start)) = &mut current_mouse_run + { + let bounds = RectF::from_points( + *mouse_region_start, + glyph_origin + vec2f(0., layout.line_height), + ); + scene.push_cursor_region(CursorRegion { + bounds, + style: CursorStyle::PointingHand, + }); + scene.push_mouse_region((build_mouse_region.as_mut().unwrap())( + *mouse_run_ix, + bounds, + )); + *mouse_region_start = + vec2f(origin.x(), glyph_origin.y() + layout.line_height); + } + + wrap_boundaries.next(); + glyph_origin = vec2f(origin.x(), glyph_origin.y() + layout.line_height); + } + + if offset + glyph.index == mouse_run_range.start { + current_mouse_run = Some((mouse_run_ix, glyph_origin)); + } + if offset + glyph.index == mouse_run_range.end { + if let Some((mouse_run_ix, mouse_region_start)) = + current_mouse_run.take() + { + let bounds = RectF::from_points( + mouse_region_start, + glyph_origin + vec2f(0., layout.line_height), + ); + scene.push_cursor_region(CursorRegion { + bounds, + style: CursorStyle::PointingHand, + }); + scene.push_mouse_region((build_mouse_region.as_mut().unwrap())( + mouse_run_ix, + bounds, + )); + mouse_runs.next(); + } + + if let Some(next) = mouse_runs.peek() { + mouse_run_ix = next.0; + mouse_run_range = next.1; + if mouse_run_range.start >= end_offset { + break; + } + if mouse_run_range.start == offset + glyph.index { + current_mouse_run = Some((mouse_run_ix, glyph_origin)); + } + } + } + } + + if let Some((mouse_run_ix, mouse_region_start)) = current_mouse_run { + let line_end = glyph_origin + vec2f(line.width() - prev_position, 0.); + let bounds = RectF::from_points( + mouse_region_start, + line_end + vec2f(0., layout.line_height), + ); + scene.push_cursor_region(CursorRegion { + bounds, + style: CursorStyle::PointingHand, + }); + scene.push_mouse_region((build_mouse_region.as_mut().unwrap())( + mouse_run_ix, + bounds, + )); + } + } + } + + offset = end_offset + 1; origin.set_y(boundaries.max_y()); } } diff --git a/crates/gpui/src/text_layout.rs b/crates/gpui/src/text_layout.rs index b557afc319..3f2f7890b8 100644 --- a/crates/gpui/src/text_layout.rs +++ b/crates/gpui/src/text_layout.rs @@ -393,41 +393,82 @@ impl Line { origin: Vector2F, visible_bounds: RectF, line_height: f32, - boundaries: impl IntoIterator, + boundaries: &[ShapedBoundary], cx: &mut WindowContext, ) { let padding_top = (line_height - self.layout.ascent - self.layout.descent) / 2.; - let baseline_origin = vec2f(0., padding_top + self.layout.ascent); + let baseline_offset = vec2f(0., padding_top + self.layout.ascent); let mut boundaries = boundaries.into_iter().peekable(); let mut color_runs = self.style_runs.iter(); - let mut color_end = 0; + let mut style_run_end = 0; let mut color = Color::black(); + let mut underline: Option<(Vector2F, Underline)> = None; - let mut glyph_origin = vec2f(0., 0.); + let mut glyph_origin = origin; let mut prev_position = 0.; for run in &self.layout.runs { for (glyph_ix, glyph) in run.glyphs.iter().enumerate() { + glyph_origin.set_x(glyph_origin.x() + glyph.position.x() - prev_position); + if boundaries.peek().map_or(false, |b| b.glyph_ix == glyph_ix) { boundaries.next(); - glyph_origin = vec2f(0., glyph_origin.y() + line_height); - } else { - glyph_origin.set_x(glyph_origin.x() + glyph.position.x() - prev_position); + if let Some((underline_origin, underline_style)) = underline { + scene.push_underline(scene::Underline { + origin: underline_origin, + width: glyph_origin.x() - underline_origin.x(), + thickness: underline_style.thickness.into(), + color: underline_style.color.unwrap(), + squiggly: underline_style.squiggly, + }); + } + + glyph_origin = vec2f(origin.x(), glyph_origin.y() + line_height); } prev_position = glyph.position.x(); - if glyph.index >= color_end { - if let Some(next_run) = color_runs.next() { - color_end += next_run.len as usize; - color = next_run.color; + let mut finished_underline = None; + if glyph.index >= style_run_end { + if let Some(style_run) = color_runs.next() { + style_run_end += style_run.len as usize; + color = style_run.color; + if let Some((_, underline_style)) = underline { + if style_run.underline != underline_style { + finished_underline = underline.take(); + } + } + if style_run.underline.thickness.into_inner() > 0. { + underline.get_or_insert(( + glyph_origin + + vec2f(0., baseline_offset.y() + 0.618 * self.layout.descent), + Underline { + color: Some( + style_run.underline.color.unwrap_or(style_run.color), + ), + thickness: style_run.underline.thickness, + squiggly: style_run.underline.squiggly, + }, + )); + } } else { - color_end = self.layout.len; + style_run_end = self.layout.len; color = Color::black(); + finished_underline = underline.take(); } } + if let Some((underline_origin, underline_style)) = finished_underline { + scene.push_underline(scene::Underline { + origin: underline_origin, + width: glyph_origin.x() - underline_origin.x(), + thickness: underline_style.thickness.into(), + color: underline_style.color.unwrap(), + squiggly: underline_style.squiggly, + }); + } + let glyph_bounds = RectF::new( - origin + glyph_origin, + glyph_origin, cx.font_cache .bounding_box(run.font_id, self.layout.font_size), ); @@ -437,20 +478,31 @@ impl Line { font_id: run.font_id, font_size: self.layout.font_size, id: glyph.id, - origin: glyph_bounds.origin() + baseline_origin, + origin: glyph_bounds.origin() + baseline_offset, }); } else { scene.push_glyph(scene::Glyph { font_id: run.font_id, font_size: self.layout.font_size, id: glyph.id, - origin: glyph_bounds.origin() + baseline_origin, + origin: glyph_bounds.origin() + baseline_offset, color, }); } } } } + + if let Some((underline_origin, underline_style)) = underline.take() { + let line_end_x = glyph_origin.x() + self.layout.width - prev_position; + scene.push_underline(scene::Underline { + origin: underline_origin, + width: line_end_x - underline_origin.x(), + thickness: underline_style.thickness.into(), + color: underline_style.color.unwrap(), + squiggly: underline_style.squiggly, + }); + } } } From 7960067cf9f69d83677b88a285dafe694c8bf650 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 26 Apr 2023 15:32:19 -0700 Subject: [PATCH 03/14] Fix bug where Text element would wrap at the right glyph in the wrong run --- crates/gpui/src/elements/text.rs | 13 ++++++++----- crates/gpui/src/text_layout.rs | 7 +++++-- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/crates/gpui/src/elements/text.rs b/crates/gpui/src/elements/text.rs index 357bce9d0d..1d78ded4cd 100644 --- a/crates/gpui/src/elements/text.rs +++ b/crates/gpui/src/elements/text.rs @@ -226,17 +226,20 @@ impl Element for Text { let mut glyph_origin = origin; let mut prev_position = 0.; let mut wrap_boundaries = wrap_boundaries.iter().copied().peekable(); - for (glyph_ix, glyph) in line - .runs() - .iter() - .flat_map(|run| run.glyphs().iter().enumerate()) + for (run_ix, glyph_ix, glyph) in + line.runs().iter().enumerate().flat_map(|(run_ix, run)| { + run.glyphs() + .iter() + .enumerate() + .map(move |(ix, glyph)| (run_ix, ix, glyph)) + }) { glyph_origin.set_x(glyph_origin.x() + glyph.position.x() - prev_position); prev_position = glyph.position.x(); if wrap_boundaries .peek() - .map_or(false, |b| b.glyph_ix == glyph_ix) + .map_or(false, |b| b.run_ix == run_ix && b.glyph_ix == glyph_ix) { if let Some((mouse_run_ix, mouse_region_start)) = &mut current_mouse_run { diff --git a/crates/gpui/src/text_layout.rs b/crates/gpui/src/text_layout.rs index 3f2f7890b8..25b5c3f430 100644 --- a/crates/gpui/src/text_layout.rs +++ b/crates/gpui/src/text_layout.rs @@ -407,11 +407,14 @@ impl Line { let mut glyph_origin = origin; let mut prev_position = 0.; - for run in &self.layout.runs { + for (run_ix, run) in self.layout.runs.iter().enumerate() { for (glyph_ix, glyph) in run.glyphs.iter().enumerate() { glyph_origin.set_x(glyph_origin.x() + glyph.position.x() - prev_position); - if boundaries.peek().map_or(false, |b| b.glyph_ix == glyph_ix) { + if boundaries + .peek() + .map_or(false, |b| b.run_ix == run_ix && b.glyph_ix == glyph_ix) + { boundaries.next(); if let Some((underline_origin, underline_style)) = underline { scene.push_underline(scene::Underline { From d298ce3fd33e445e6aa6c83a5092287df08da7a3 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 26 Apr 2023 13:23:29 -0700 Subject: [PATCH 04/14] Render more markdown features in hover popover --- Cargo.lock | 2 +- crates/collab/src/tests/integration_tests.rs | 8 +- crates/editor/Cargo.toml | 1 + crates/editor/src/editor.rs | 4 + crates/editor/src/hover_popover.rs | 416 +++++++++++++++---- crates/project/Cargo.toml | 2 +- crates/project/src/lsp_command.rs | 122 +++--- crates/project/src/project.rs | 27 +- crates/rpc/proto/zed.proto | 1 + crates/theme/src/theme.rs | 2 + crates/theme/src/theme_registry.rs | 11 +- 11 files changed, 421 insertions(+), 175 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2fca74cd7c..34cdcf1e69 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1994,6 +1994,7 @@ dependencies = [ "parking_lot 0.11.2", "postage", "project", + "pulldown-cmark", "rand 0.8.5", "rpc", "serde", @@ -4727,7 +4728,6 @@ dependencies = [ "parking_lot 0.11.2", "postage", "pretty_assertions", - "pulldown-cmark", "rand 0.8.5", "regex", "rpc", diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index 5c19226960..7756f31efa 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -23,7 +23,7 @@ use language::{ }; use live_kit_client::MacOSDisplay; use lsp::LanguageServerId; -use project::{search::SearchQuery, DiagnosticSummary, Project, ProjectPath}; +use project::{search::SearchQuery, DiagnosticSummary, HoverBlockKind, Project, ProjectPath}; use rand::prelude::*; use serde_json::json; use settings::{Formatter, Settings}; @@ -4693,11 +4693,13 @@ async fn test_lsp_hover( vec![ project::HoverBlock { text: "Test hover content.".to_string(), - language: None, + kind: HoverBlockKind::Markdown, }, project::HoverBlock { text: "let foo = 42;".to_string(), - language: Some("Rust".to_string()), + kind: HoverBlockKind::Code { + language: "Rust".to_string() + }, } ] ); diff --git a/crates/editor/Cargo.toml b/crates/editor/Cargo.toml index 37b4dccb81..8835147ba6 100644 --- a/crates/editor/Cargo.toml +++ b/crates/editor/Cargo.toml @@ -55,6 +55,7 @@ log.workspace = true ordered-float.workspace = true parking_lot.workspace = true postage.workspace = true +pulldown-cmark = { version = "0.9.1", default-features = false } rand = { workspace = true, optional = true } serde.workspace = true serde_derive.workspace = true diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 563c0aa132..261c506967 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -463,6 +463,7 @@ pub struct EditorStyle { pub text: TextStyle, pub placeholder_text: Option, pub theme: theme::Editor, + pub theme_id: usize, } type CompletionId = usize; @@ -7319,6 +7320,7 @@ fn build_style( ) -> EditorStyle { let font_cache = cx.font_cache(); + let theme_id = settings.theme.meta.id; let mut theme = settings.theme.editor.clone(); let mut style = if let Some(get_field_editor_theme) = get_field_editor_theme { let field_editor_theme = get_field_editor_theme(&settings.theme); @@ -7332,6 +7334,7 @@ fn build_style( text: field_editor_theme.text, placeholder_text: field_editor_theme.placeholder_text, theme, + theme_id, } } else { let font_family_id = settings.buffer_font_family; @@ -7353,6 +7356,7 @@ fn build_style( }, placeholder_text: None, theme, + theme_id, } }; diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 7c62f71bda..70f9c8c88c 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -1,15 +1,17 @@ use futures::FutureExt; use gpui::{ actions, - elements::{Flex, MouseEventHandler, Padding, Text}, + color::Color, + elements::{Flex, MouseEventHandler, Padding, ParentElement, Text}, + fonts::{HighlightStyle, Underline, Weight}, impl_internal_actions, platform::{CursorStyle, MouseButton}, - AnyElement, AppContext, Axis, Element, ModelHandle, Task, ViewContext, + AnyElement, AppContext, Element, ModelHandle, MouseRegion, Task, ViewContext, }; -use language::{Bias, DiagnosticEntry, DiagnosticSeverity}; -use project::{HoverBlock, Project}; +use language::{Bias, DiagnosticEntry, DiagnosticSeverity, Language, LanguageRegistry}; +use project::{HoverBlock, HoverBlockKind, Project}; use settings::Settings; -use std::{ops::Range, time::Duration}; +use std::{ops::Range, sync::Arc, time::Duration}; use util::TryFutureExt; use crate::{ @@ -235,7 +237,8 @@ fn show_hover( Some(InfoPopover { project: project.clone(), symbol_range: range, - contents: hover_result.contents, + blocks: hover_result.contents, + rendered_content: None, }) }); @@ -264,6 +267,191 @@ fn show_hover( editor.hover_state.info_task = Some(task); } +fn render_blocks( + theme_id: usize, + blocks: &[HoverBlock], + language_registry: &Arc, + style: &EditorStyle, +) -> RenderedInfo { + let mut text = String::new(); + let mut highlights = Vec::new(); + let mut link_ranges = Vec::new(); + let mut link_urls = Vec::new(); + + for block in blocks { + match &block.kind { + HoverBlockKind::PlainText => { + new_paragraph(&mut text); + text.push_str(&block.text); + } + HoverBlockKind::Markdown => { + use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag}; + + let mut bold_depth = 0; + let mut italic_depth = 0; + let mut link_url = None; + let mut current_language = None; + let mut list_stack = Vec::new(); + + for event in Parser::new_ext(&block.text, Options::all()) { + let prev_len = text.len(); + match event { + Event::Text(t) => { + if let Some(language) = ¤t_language { + render_code( + &mut text, + &mut highlights, + t.as_ref(), + language, + style, + ); + } else { + text.push_str(t.as_ref()); + + let mut style = HighlightStyle::default(); + if bold_depth > 0 { + style.weight = Some(Weight::BOLD); + } + if italic_depth > 0 { + style.italic = Some(true); + } + if link_url.is_some() { + style.underline = Some(Underline { + thickness: 1.0.into(), + ..Default::default() + }); + } + + if style != HighlightStyle::default() { + let mut new_highlight = true; + if let Some((last_range, last_style)) = highlights.last_mut() { + if last_range.end == prev_len && last_style == &style { + last_range.end = text.len(); + new_highlight = false; + } + } + if new_highlight { + highlights.push((prev_len..text.len(), style)); + } + } + } + } + Event::Code(t) => { + text.push_str(t.as_ref()); + highlights.push(( + prev_len..text.len(), + HighlightStyle { + color: Some(Color::red()), + ..Default::default() + }, + )); + } + Event::Start(tag) => match tag { + Tag::Paragraph => new_paragraph(&mut text), + Tag::Heading(_, _, _) => { + new_paragraph(&mut text); + bold_depth += 1; + } + Tag::CodeBlock(kind) => { + new_paragraph(&mut text); + if let CodeBlockKind::Fenced(language) = kind { + current_language = language_registry + .language_for_name(language.as_ref()) + .now_or_never() + .and_then(Result::ok); + } + } + Tag::Emphasis => italic_depth += 1, + Tag::Strong => bold_depth += 1, + Tag::Link(_, url, _) => link_url = Some((prev_len, url)), + Tag::List(number) => list_stack.push(number), + Tag::Item => { + let len = list_stack.len(); + if let Some(list_state) = list_stack.last_mut() { + new_paragraph(&mut text); + for _ in 0..len - 1 { + text.push_str(" "); + } + if let Some(number) = list_state { + text.push_str(&format!("{}. ", number)); + *number += 1; + } else { + text.push_str("* "); + } + } + } + _ => {} + }, + Event::End(tag) => match tag { + Tag::Heading(_, _, _) => bold_depth -= 1, + Tag::CodeBlock(_) => current_language = None, + Tag::Emphasis => italic_depth -= 1, + Tag::Strong => bold_depth -= 1, + Tag::Link(_, _, _) => { + if let Some((start_offset, link_url)) = link_url.take() { + link_ranges.push(start_offset..text.len()); + link_urls.push(link_url.to_string()); + } + } + Tag::List(_) => { + list_stack.pop(); + } + _ => {} + }, + Event::HardBreak => text.push('\n'), + Event::SoftBreak => text.push(' '), + _ => {} + } + } + } + HoverBlockKind::Code { language } => { + if let Some(language) = language_registry + .language_for_name(language) + .now_or_never() + .and_then(Result::ok) + { + render_code(&mut text, &mut highlights, &block.text, &language, style); + } else { + text.push_str(&block.text); + } + } + } + } + + RenderedInfo { + theme_id, + text, + highlights, + link_ranges, + link_urls, + } +} + +fn render_code( + text: &mut String, + highlights: &mut Vec<(Range, HighlightStyle)>, + content: &str, + language: &Arc, + style: &EditorStyle, +) { + let prev_len = text.len(); + text.push_str(content); + for (range, highlight_id) in language.highlight_text(&content.into(), 0..content.len()) { + if let Some(style) = highlight_id.style(&style.syntax) { + highlights.push((prev_len + range.start..prev_len + range.end, style)); + } + } +} + +fn new_paragraph(text: &mut String) { + if !text.is_empty() { + if !text.ends_with('\n') { + text.push('\n'); + } + text.push('\n'); + } +} + #[derive(Default)] pub struct HoverState { pub info_popover: Option, @@ -278,7 +466,7 @@ impl HoverState { } pub fn render( - &self, + &mut self, snapshot: &EditorSnapshot, style: &EditorStyle, visible_rows: Range, @@ -307,7 +495,7 @@ impl HoverState { 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_ref() { + if let Some(info_popover) = self.info_popover.as_mut() { elements.push(info_popover.render(style, cx)); } @@ -319,44 +507,66 @@ impl HoverState { pub struct InfoPopover { pub project: ModelHandle, pub symbol_range: Range, - pub contents: Vec, + pub blocks: Vec, + rendered_content: Option, +} + +#[derive(Debug, Clone)] +struct RenderedInfo { + theme_id: usize, + text: String, + highlights: Vec<(Range, HighlightStyle)>, + link_ranges: Vec>, + link_urls: Vec, } impl InfoPopover { - pub fn render(&self, style: &EditorStyle, cx: &mut ViewContext) -> AnyElement { + pub fn render( + &mut self, + style: &EditorStyle, + cx: &mut ViewContext, + ) -> AnyElement { + if let Some(rendered) = &self.rendered_content { + if rendered.theme_id != style.theme_id { + self.rendered_content = None; + } + } + + let rendered_content = self.rendered_content.get_or_insert_with(|| { + render_blocks( + style.theme_id, + &self.blocks, + self.project.read(cx).languages(), + style, + ) + }); + MouseEventHandler::::new(0, cx, |_, cx| { - let mut flex = Flex::new(Axis::Vertical).scrollable::(1, None, cx); - flex.extend(self.contents.iter().map(|content| { - let languages = self.project.read(cx).languages(); - if let Some(language) = content.language.clone().and_then(|language| { - languages.language_for_name(&language).now_or_never()?.ok() - }) { - let runs = language - .highlight_text(&content.text.as_str().into(), 0..content.text.len()); + let mut region_id = 0; + let view_id = cx.view_id(); - Text::new(content.text.clone(), style.text.clone()) - .with_soft_wrap(true) - .with_highlights( - runs.iter() - .filter_map(|(range, id)| { - id.style(style.theme.syntax.as_ref()) - .map(|style| (range.clone(), style)) - }) - .collect(), + let link_urls = rendered_content.link_urls.clone(); + Flex::column() + .scrollable::(1, None, cx) + .with_child( + Text::new(rendered_content.text.clone(), style.text.clone()) + .with_highlights(rendered_content.highlights.clone()) + .with_mouse_regions( + rendered_content.link_ranges.clone(), + move |ix, bounds| { + region_id += 1; + let url = link_urls[ix].clone(); + MouseRegion::new::(view_id, region_id, bounds) + .on_click::(MouseButton::Left, move |_, _, cx| { + println!("clicked link {url}"); + cx.platform().open_url(&url); + }) + }, ) - .into_any() - } else { - let mut text_style = style.hover_popover.prose.clone(); - text_style.font_size = style.text.font_size; - - Text::new(content.text.clone(), text_style) - .with_soft_wrap(true) - .contained() - .with_style(style.hover_popover.block_style) - .into_any() - } - })); - flex.contained().with_style(style.hover_popover.container) + .with_soft_wrap(true), + ) + .contained() + .with_style(style.hover_popover.container) }) .on_move(|_, _, _| {}) // Consume move events so they don't reach regions underneath. .with_cursor_style(CursorStyle::Arrow) @@ -430,16 +640,15 @@ impl DiagnosticPopover { #[cfg(test)] mod tests { + use super::*; + use crate::test::editor_lsp_test_context::EditorLspTestContext; + use gpui::fonts::Weight; use indoc::indoc; - use language::{Diagnostic, DiagnosticSet}; use lsp::LanguageServerId; - use project::HoverBlock; + use project::{HoverBlock, HoverBlockKind}; use smol::stream::StreamExt; - - use crate::test::editor_lsp_test_context::EditorLspTestContext; - - use super::*; + use util::test::marked_text_ranges; #[gpui::test] async fn test_mouse_hover_info_popover(cx: &mut gpui::TestAppContext) { @@ -480,10 +689,7 @@ mod tests { Ok(Some(lsp::Hover { contents: lsp::HoverContents::Markup(lsp::MarkupContent { kind: lsp::MarkupKind::Markdown, - value: indoc! {" - # Some basic docs - Some test documentation"} - .to_string(), + value: "some basic docs".to_string(), }), range: Some(symbol_range), })) @@ -495,17 +701,11 @@ mod tests { cx.editor(|editor, _| { assert!(editor.hover_state.visible()); assert_eq!( - editor.hover_state.info_popover.clone().unwrap().contents, - vec![ - HoverBlock { - text: "Some basic docs".to_string(), - language: None - }, - HoverBlock { - text: "Some test documentation".to_string(), - language: None - } - ] + editor.hover_state.info_popover.clone().unwrap().blocks, + vec![HoverBlock { + text: "some basic docs".to_string(), + kind: HoverBlockKind::Markdown, + },] ) }); @@ -556,10 +756,7 @@ mod tests { Ok(Some(lsp::Hover { contents: lsp::HoverContents::Markup(lsp::MarkupContent { kind: lsp::MarkupKind::Markdown, - value: indoc! {" - # Some other basic docs - Some other test documentation"} - .to_string(), + value: "some other basic docs".to_string(), }), range: Some(symbol_range), })) @@ -570,17 +767,11 @@ mod tests { cx.condition(|editor, _| editor.hover_state.visible()).await; cx.editor(|editor, _| { assert_eq!( - editor.hover_state.info_popover.clone().unwrap().contents, - vec![ - HoverBlock { - text: "Some other basic docs".to_string(), - language: None - }, - HoverBlock { - text: "Some other test documentation".to_string(), - language: None - } - ] + editor.hover_state.info_popover.clone().unwrap().blocks, + vec![HoverBlock { + text: "some other basic docs".to_string(), + kind: HoverBlockKind::Markdown, + }] ) }); } @@ -637,10 +828,7 @@ mod tests { Ok(Some(lsp::Hover { contents: lsp::HoverContents::Markup(lsp::MarkupContent { kind: lsp::MarkupKind::Markdown, - value: indoc! {" - # Some other basic docs - Some other test documentation"} - .to_string(), + value: "some new docs".to_string(), }), range: Some(range), })) @@ -653,4 +841,72 @@ mod tests { hover_state.diagnostic_popover.is_some() && hover_state.info_task.is_some() }); } + + #[gpui::test] + fn test_render_blocks(cx: &mut gpui::TestAppContext) { + Settings::test_async(cx); + cx.add_window(|cx| { + let editor = Editor::single_line(None, cx); + let style = editor.style(cx); + + struct Row { + blocks: Vec, + expected_marked_text: &'static str, + expected_styles: Vec, + } + + let rows = &[ + Row { + blocks: vec![HoverBlock { + text: "one **two** three".to_string(), + kind: HoverBlockKind::Markdown, + }], + expected_marked_text: "one «two» three", + expected_styles: vec![HighlightStyle { + weight: Some(Weight::BOLD), + ..Default::default() + }], + }, + Row { + blocks: vec![HoverBlock { + text: "one [two](the-url) three".to_string(), + kind: HoverBlockKind::Markdown, + }], + expected_marked_text: "one «two» three", + 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 + { + let rendered = render_blocks(0, &blocks, &Default::default(), &style); + + 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:?}" + ); + assert_eq!( + rendered.highlights, expected_highlights, + "wrong highlights for input {blocks:?}" + ); + } + + editor + }); + } } diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml index a0e6b31ec8..56803bb062 100644 --- a/crates/project/Cargo.toml +++ b/crates/project/Cargo.toml @@ -37,6 +37,7 @@ settings = { path = "../settings" } sum_tree = { path = "../sum_tree" } terminal = { path = "../terminal" } util = { path = "../util" } + aho-corasick = "0.7" anyhow.workspace = true async-trait.workspace = true @@ -47,7 +48,6 @@ lazy_static.workspace = true log.workspace = true parking_lot.workspace = true postage.workspace = true -pulldown-cmark = { version = "0.9.1", default-features = false } rand.workspace = true regex.workspace = true serde.workspace = true diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index b26987694e..ddae9b59ae 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -1,5 +1,6 @@ use crate::{ - DocumentHighlight, Hover, HoverBlock, Location, LocationLink, Project, ProjectTransaction, + DocumentHighlight, Hover, HoverBlock, HoverBlockKind, Location, LocationLink, Project, + ProjectTransaction, }; use anyhow::{anyhow, Result}; use async_trait::async_trait; @@ -13,7 +14,6 @@ use language::{ Completion, OffsetRangeExt, PointUtf16, ToOffset, ToPointUtf16, Unclipped, }; use lsp::{DocumentHighlightKind, LanguageServer, LanguageServerId, ServerCapabilities}; -use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag}; use std::{cmp::Reverse, ops::Range, path::Path, sync::Arc}; #[async_trait(?Send)] @@ -1092,76 +1092,49 @@ impl LspCommand for GetHover { }) }); - let contents = cx.read(|_| match hover.contents { - lsp::HoverContents::Scalar(marked_string) => { - HoverBlock::try_new(marked_string).map(|contents| vec![contents]) - } - lsp::HoverContents::Array(marked_strings) => { - let content: Vec = marked_strings - .into_iter() - .filter_map(HoverBlock::try_new) - .collect(); - if content.is_empty() { - None - } else { - Some(content) - } - } - lsp::HoverContents::Markup(markup_content) => { - let mut contents = Vec::new(); - let mut language = None; - let mut current_text = String::new(); - for event in Parser::new_ext(&markup_content.value, Options::all()) { - match event { - Event::SoftBreak => { - current_text.push(' '); - } - Event::Text(text) | Event::Code(text) => { - current_text.push_str(&text.to_string()); - } - Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(new_language))) => { - if !current_text.is_empty() { - let text = std::mem::take(&mut current_text).trim().to_string(); - contents.push(HoverBlock { text, language }); - } - - language = if new_language.is_empty() { - None - } else { - Some(new_language.to_string()) - }; - } - Event::End(Tag::CodeBlock(_)) - | Event::End(Tag::Paragraph) - | Event::End(Tag::Heading(_, _, _)) - | Event::End(Tag::BlockQuote) - | Event::HardBreak => { - if !current_text.is_empty() { - let text = std::mem::take(&mut current_text).trim().to_string(); - contents.push(HoverBlock { text, language }); - } - language = None; - } - _ => {} + fn hover_blocks_from_marked_string( + marked_string: lsp::MarkedString, + ) -> Option { + let block = match marked_string { + lsp::MarkedString::String(content) => HoverBlock { + text: content, + kind: HoverBlockKind::Markdown, + }, + lsp::MarkedString::LanguageString(lsp::LanguageString { language, value }) => { + HoverBlock { + text: value, + kind: HoverBlockKind::Code { language }, } } - - if !current_text.trim().is_empty() { - contents.push(HoverBlock { - text: current_text, - language, - }); - } - - if contents.is_empty() { - None - } else { - Some(contents) - } + }; + if block.text.is_empty() { + None + } else { + Some(block) } + } + + let contents = cx.read(|_| match hover.contents { + lsp::HoverContents::Scalar(marked_string) => { + hover_blocks_from_marked_string(marked_string) + .into_iter() + .collect() + } + lsp::HoverContents::Array(marked_strings) => marked_strings + .into_iter() + .filter_map(hover_blocks_from_marked_string) + .collect(), + lsp::HoverContents::Markup(markup_content) => vec![HoverBlock { + text: markup_content.value, + kind: if markup_content.kind == lsp::MarkupKind::Markdown { + HoverBlockKind::Markdown + } else { + HoverBlockKind::PlainText + }, + }], }); - contents.map(|contents| Hover { contents, range }) + Some(Hover { contents, range }) })) } @@ -1218,7 +1191,12 @@ impl LspCommand for GetHover { .into_iter() .map(|block| proto::HoverBlock { text: block.text, - language: block.language, + is_markdown: block.kind == HoverBlockKind::Markdown, + language: if let HoverBlockKind::Code { language } = block.kind { + Some(language) + } else { + None + }, }) .collect(); @@ -1255,7 +1233,13 @@ impl LspCommand for GetHover { .into_iter() .map(|block| HoverBlock { text: block.text, - language: block.language, + kind: if let Some(language) = block.language { + HoverBlockKind::Code { language } + } else if block.is_markdown { + HoverBlockKind::Markdown + } else { + HoverBlockKind::PlainText + }, }) .collect(); diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index c82855b03c..51d29d7296 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -36,7 +36,7 @@ use language::{ }; use lsp::{ DiagnosticSeverity, DiagnosticTag, DidChangeWatchedFilesRegistrationOptions, - DocumentHighlightKind, LanguageServer, LanguageServerId, LanguageString, MarkedString, + DocumentHighlightKind, LanguageServer, LanguageServerId, }; use lsp_command::*; use lsp_glob_set::LspGlobSet; @@ -287,27 +287,14 @@ pub struct Symbol { #[derive(Clone, Debug, PartialEq)] pub struct HoverBlock { pub text: String, - pub language: Option, + pub kind: HoverBlockKind, } -impl HoverBlock { - fn try_new(marked_string: MarkedString) -> Option { - let result = match marked_string { - MarkedString::LanguageString(LanguageString { language, value }) => HoverBlock { - text: value, - language: Some(language), - }, - MarkedString::String(text) => HoverBlock { - text, - language: None, - }, - }; - if result.text.is_empty() { - None - } else { - Some(result) - } - } +#[derive(Clone, Debug, PartialEq)] +pub enum HoverBlockKind { + PlainText, + Markdown, + Code { language: String }, } #[derive(Debug)] diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 599d80e2ba..86f8f38ff3 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -633,6 +633,7 @@ message GetHoverResponse { message HoverBlock { string text = 1; optional string language = 2; + bool is_markdown = 3; } message ApplyCodeAction { diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index b98dd5483e..fbf4ea6b70 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -46,6 +46,8 @@ pub struct Theme { #[derive(Deserialize, Default, Clone)] pub struct ThemeMeta { + #[serde(skip_deserializing)] + pub id: usize, pub name: String, pub is_light: bool, } diff --git a/crates/theme/src/theme_registry.rs b/crates/theme/src/theme_registry.rs index a82ede59f7..f9f89b7adc 100644 --- a/crates/theme/src/theme_registry.rs +++ b/crates/theme/src/theme_registry.rs @@ -4,13 +4,20 @@ use gpui::{fonts, AssetSource, FontCache}; use parking_lot::Mutex; use serde::Deserialize; use serde_json::Value; -use std::{collections::HashMap, sync::Arc}; +use std::{ + collections::HashMap, + sync::{ + atomic::{AtomicUsize, Ordering::SeqCst}, + Arc, + }, +}; pub struct ThemeRegistry { assets: Box, themes: Mutex>>, theme_data: Mutex>>, font_cache: Arc, + next_theme_id: AtomicUsize, } impl ThemeRegistry { @@ -19,6 +26,7 @@ impl ThemeRegistry { assets: Box::new(source), themes: Default::default(), theme_data: Default::default(), + next_theme_id: Default::default(), font_cache, }) } @@ -66,6 +74,7 @@ impl ThemeRegistry { // Reset name to be the file path, so that we can use it to access the stored themes theme.meta.name = name.into(); + theme.meta.id = self.next_theme_id.fetch_add(1, SeqCst); let theme: Arc = theme.into(); self.themes.lock().insert(name.to_string(), theme.clone()); Ok(theme) From c6abb0db3a87245b626b86edc64ca67030c62d74 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 26 Apr 2023 17:09:20 -0700 Subject: [PATCH 05/14] Improve rendering of multi-paragraph list items in hover markdown --- crates/editor/src/hover_popover.rs | 36 +++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 70f9c8c88c..141559740e 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -281,7 +281,7 @@ fn render_blocks( for block in blocks { match &block.kind { HoverBlockKind::PlainText => { - new_paragraph(&mut text); + new_paragraph(&mut text, &mut Vec::new()); text.push_str(&block.text); } HoverBlockKind::Markdown => { @@ -347,13 +347,13 @@ fn render_blocks( )); } Event::Start(tag) => match tag { - Tag::Paragraph => new_paragraph(&mut text), + Tag::Paragraph => new_paragraph(&mut text, &mut list_stack), Tag::Heading(_, _, _) => { - new_paragraph(&mut text); + new_paragraph(&mut text, &mut list_stack); bold_depth += 1; } Tag::CodeBlock(kind) => { - new_paragraph(&mut text); + new_paragraph(&mut text, &mut list_stack); if let CodeBlockKind::Fenced(language) = kind { current_language = language_registry .language_for_name(language.as_ref()) @@ -364,19 +364,25 @@ fn render_blocks( Tag::Emphasis => italic_depth += 1, Tag::Strong => bold_depth += 1, Tag::Link(_, url, _) => link_url = Some((prev_len, url)), - Tag::List(number) => list_stack.push(number), + Tag::List(number) => { + list_stack.push((number, false)); + } Tag::Item => { let len = list_stack.len(); - if let Some(list_state) = list_stack.last_mut() { - new_paragraph(&mut text); + if let Some((list_number, has_content)) = list_stack.last_mut() { + *has_content = false; + if !text.is_empty() && !text.ends_with('\n') { + text.push('\n'); + } for _ in 0..len - 1 { text.push_str(" "); } - if let Some(number) = list_state { + if let Some(number) = list_number { text.push_str(&format!("{}. ", number)); *number += 1; + *has_content = false; } else { - text.push_str("* "); + text.push_str("• "); } } } @@ -443,13 +449,23 @@ fn render_code( } } -fn new_paragraph(text: &mut String) { +fn new_paragraph(text: &mut String, list_stack: &mut Vec<(Option, bool)>) { + if let Some((_, has_content)) = list_stack.last_mut() { + if !*has_content { + *has_content = true; + return; + } + } + if !text.is_empty() { if !text.ends_with('\n') { text.push('\n'); } text.push('\n'); } + for _ in 0..list_stack.len().saturating_sub(1) { + text.push_str(" "); + } } #[derive(Default)] From 30f20024c0a7e578d7030089977805fbc669aa24 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 27 Apr 2023 10:43:09 -0700 Subject: [PATCH 06/14] Fix vim mode crash when active editor changes in inactive window Co-authored-by: Antonio Scandurra --- crates/vim/src/editor_events.rs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/crates/vim/src/editor_events.rs b/crates/vim/src/editor_events.rs index fadfdd3b0f..4324bc6054 100644 --- a/crates/vim/src/editor_events.rs +++ b/crates/vim/src/editor_events.rs @@ -9,11 +9,18 @@ pub fn init(cx: &mut AppContext) { } fn focused(EditorFocused(editor): &EditorFocused, cx: &mut AppContext) { + if let Some(previously_active_editor) = Vim::read(cx).active_editor.clone() { + cx.update_window(previously_active_editor.window_id(), |cx| { + Vim::update(cx, |vim, cx| { + vim.update_active_editor(cx, |previously_active_editor, cx| { + Vim::unhook_vim_settings(previously_active_editor, cx); + }); + }); + }); + } + cx.update_window(editor.window_id(), |cx| { Vim::update(cx, |vim, cx| { - vim.update_active_editor(cx, |previously_active_editor, cx| { - Vim::unhook_vim_settings(previously_active_editor, cx); - }); vim.set_active_editor(editor.clone(), cx); }); }); From 3f7533a0b482eb19682cc4049b3b897073c02c01 Mon Sep 17 00:00:00 2001 From: Julia Date: Wed, 26 Apr 2023 15:46:20 -0400 Subject: [PATCH 07/14] Show source of diagnostic hovers --- crates/editor/src/hover_popover.rs | 18 +++++++++++++++--- crates/language/src/buffer.rs | 2 ++ crates/language/src/proto.rs | 2 ++ crates/project/src/project.rs | 2 ++ crates/rpc/proto/zed.proto | 17 +++++++++-------- crates/rpc/src/rpc.rs | 2 +- crates/theme/src/theme.rs | 1 + crates/zed/src/languages.rs | 23 +++++++++++++---------- styles/src/styleTree/hoverPopover.ts | 5 +++-- 9 files changed, 48 insertions(+), 24 deletions(-) diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 43c93cf33b..df2570a8ef 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -1,7 +1,7 @@ use futures::FutureExt; use gpui::{ actions, - elements::{Flex, MouseEventHandler, Padding, Text}, + elements::{Flex, MouseEventHandler, Padding, ParentElement, Text}, impl_internal_actions, platform::{CursorStyle, MouseButton}, AnyElement, AppContext, Axis, Element, ModelHandle, Task, ViewContext, @@ -378,6 +378,8 @@ impl DiagnosticPopover { let mut text_style = style.hover_popover.prose.clone(); text_style.font_size = style.text.font_size; + let mut diagnostic_source_style = style.hover_popover.diagnostic_source.clone(); + diagnostic_source_style.font_size = style.text.font_size; let container_style = match self.local_diagnostic.diagnostic.severity { DiagnosticSeverity::HINT => style.hover_popover.info_container, @@ -390,8 +392,18 @@ impl DiagnosticPopover { let tooltip_style = cx.global::().theme.tooltip.clone(); MouseEventHandler::::new(0, cx, |_, _| { - Text::new(self.local_diagnostic.diagnostic.message.clone(), text_style) - .with_soft_wrap(true) + Flex::row() + .with_children( + self.local_diagnostic + .diagnostic + .source + .as_ref() + .map(|source| Text::new(format!("{source}: "), diagnostic_source_style)), + ) + .with_child( + Text::new(self.local_diagnostic.diagnostic.message.clone(), text_style) + .with_soft_wrap(true), + ) .contained() .with_style(container_style) }) diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 65e4d3b8b6..32cbc260ec 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -138,6 +138,7 @@ pub struct GroupId { #[derive(Clone, Debug, PartialEq, Eq)] pub struct Diagnostic { + pub source: Option, pub code: Option, pub severity: DiagnosticSeverity, pub message: String, @@ -2881,6 +2882,7 @@ impl operation_queue::Operation for Operation { impl Default for Diagnostic { fn default() -> Self { Self { + source: Default::default(), code: None, severity: DiagnosticSeverity::ERROR, message: Default::default(), diff --git a/crates/language/src/proto.rs b/crates/language/src/proto.rs index bf1d1dd273..0de3f704c7 100644 --- a/crates/language/src/proto.rs +++ b/crates/language/src/proto.rs @@ -173,6 +173,7 @@ pub fn serialize_diagnostics<'a>( diagnostics .into_iter() .map(|entry| proto::Diagnostic { + source: entry.diagnostic.source.clone(), start: Some(serialize_anchor(&entry.range.start)), end: Some(serialize_anchor(&entry.range.end)), message: entry.diagnostic.message.clone(), @@ -359,6 +360,7 @@ pub fn deserialize_diagnostics( Some(DiagnosticEntry { range: deserialize_anchor(diagnostic.start?)?..deserialize_anchor(diagnostic.end?)?, diagnostic: Diagnostic { + source: diagnostic.source, severity: match proto::diagnostic::Severity::from_i32(diagnostic.severity)? { proto::diagnostic::Severity::Error => DiagnosticSeverity::ERROR, proto::diagnostic::Severity::Warning => DiagnosticSeverity::WARNING, diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index d543f225b2..3aa332696b 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -2954,6 +2954,7 @@ impl Project { diagnostics.push(DiagnosticEntry { range, diagnostic: Diagnostic { + source: diagnostic.source.clone(), code: code.clone(), severity: diagnostic.severity.unwrap_or(DiagnosticSeverity::ERROR), message: diagnostic.message.clone(), @@ -2971,6 +2972,7 @@ impl Project { diagnostics.push(DiagnosticEntry { range, diagnostic: Diagnostic { + source: diagnostic.source.clone(), code: code.clone(), severity: DiagnosticSeverity::INFORMATION, message: info.message.clone(), diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 599d80e2ba..ffd7709015 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -1049,14 +1049,15 @@ enum Bias { message Diagnostic { Anchor start = 1; Anchor end = 2; - Severity severity = 3; - string message = 4; - optional string code = 5; - uint64 group_id = 6; - bool is_primary = 7; - bool is_valid = 8; - bool is_disk_based = 9; - bool is_unnecessary = 10; + optional string source = 3; + Severity severity = 4; + string message = 5; + optional string code = 6; + uint64 group_id = 7; + bool is_primary = 8; + bool is_valid = 9; + bool is_disk_based = 10; + bool is_unnecessary = 11; enum Severity { None = 0; diff --git a/crates/rpc/src/rpc.rs b/crates/rpc/src/rpc.rs index f64e6bea4c..b7cb59266c 100644 --- a/crates/rpc/src/rpc.rs +++ b/crates/rpc/src/rpc.rs @@ -6,4 +6,4 @@ pub use conn::Connection; pub use peer::*; mod macros; -pub const PROTOCOL_VERSION: u32 = 52; +pub const PROTOCOL_VERSION: u32 = 53; diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index b98dd5483e..6edf69d054 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -887,6 +887,7 @@ pub struct HoverPopover { pub error_container: ContainerStyle, pub block_style: ContainerStyle, pub prose: TextStyle, + pub diagnostic_source: TextStyle, pub highlight: Color, } diff --git a/crates/zed/src/languages.rs b/crates/zed/src/languages.rs index 9ab6e1d778..91b58f634b 100644 --- a/crates/zed/src/languages.rs +++ b/crates/zed/src/languages.rs @@ -89,23 +89,26 @@ pub fn init( ( "tsx", tree_sitter_typescript::language_tsx(), - vec![adapter_arc(typescript::TypeScriptLspAdapter::new( - node_runtime.clone(), - ))], + vec![ + adapter_arc(typescript::TypeScriptLspAdapter::new(node_runtime.clone())), + adapter_arc(typescript::EsLintLspAdapter::new(node_runtime.clone())), + ], ), ( "typescript", tree_sitter_typescript::language_typescript(), - vec![adapter_arc(typescript::TypeScriptLspAdapter::new( - node_runtime.clone(), - ))], + vec![ + adapter_arc(typescript::TypeScriptLspAdapter::new(node_runtime.clone())), + adapter_arc(typescript::EsLintLspAdapter::new(node_runtime.clone())), + ], ), ( "javascript", tree_sitter_typescript::language_tsx(), - vec![adapter_arc(typescript::TypeScriptLspAdapter::new( - node_runtime.clone(), - ))], + vec![ + adapter_arc(typescript::TypeScriptLspAdapter::new(node_runtime.clone())), + adapter_arc(typescript::EsLintLspAdapter::new(node_runtime.clone())), + ], ), ( "html", @@ -132,7 +135,7 @@ pub fn init( ( "yaml", tree_sitter_yaml::language(), - vec![adapter_arc(yaml::YamlLspAdapter::new(node_runtime.clone()))], + vec![adapter_arc(yaml::YamlLspAdapter::new(node_runtime))], ), ]; diff --git a/styles/src/styleTree/hoverPopover.ts b/styles/src/styleTree/hoverPopover.ts index 032c53112b..31e1d80778 100644 --- a/styles/src/styleTree/hoverPopover.ts +++ b/styles/src/styleTree/hoverPopover.ts @@ -1,5 +1,5 @@ import { ColorScheme } from "../themes/common/colorScheme" -import { background, border, text } from "./components" +import { background, border, foreground, text } from "./components" export default function HoverPopover(colorScheme: ColorScheme) { let layer = colorScheme.middle @@ -36,10 +36,11 @@ export default function HoverPopover(colorScheme: ColorScheme) { background: background(layer, "negative"), border: border(layer, "negative"), }, - block_style: { + blockStyle: { padding: { top: 4 }, }, prose: text(layer, "sans", { size: "sm" }), + diagnosticSource: text(layer, "sans", { size: "sm", underline: true, color: foreground(layer, "accent") }), highlight: colorScheme.ramps.neutral(0.5).alpha(0.2).hex(), // TODO: blend was used here. Replace with something better } } From 678c188de0e3572e7d9f0e7ed388455ed057a1c4 Mon Sep 17 00:00:00 2001 From: Julia Date: Wed, 26 Apr 2023 13:21:19 -0400 Subject: [PATCH 08/14] Re-allow diagnostics hovers to soft wrap Co-Authored-By: Max Brunsfeld --- crates/editor/src/hover_popover.rs | 28 +++++++++++++--------------- crates/theme/src/theme.rs | 2 +- styles/src/styleTree/hoverPopover.ts | 2 +- 3 files changed, 15 insertions(+), 17 deletions(-) diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index df2570a8ef..b45cdb6b01 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -1,7 +1,7 @@ use futures::FutureExt; use gpui::{ actions, - elements::{Flex, MouseEventHandler, Padding, ParentElement, Text}, + elements::{Flex, MouseEventHandler, Padding, Text}, impl_internal_actions, platform::{CursorStyle, MouseButton}, AnyElement, AppContext, Axis, Element, ModelHandle, Task, ViewContext, @@ -378,8 +378,17 @@ impl DiagnosticPopover { let mut text_style = style.hover_popover.prose.clone(); text_style.font_size = style.text.font_size; - let mut diagnostic_source_style = style.hover_popover.diagnostic_source.clone(); - diagnostic_source_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, @@ -392,18 +401,7 @@ impl DiagnosticPopover { let tooltip_style = cx.global::().theme.tooltip.clone(); MouseEventHandler::::new(0, cx, |_, _| { - Flex::row() - .with_children( - self.local_diagnostic - .diagnostic - .source - .as_ref() - .map(|source| Text::new(format!("{source}: "), diagnostic_source_style)), - ) - .with_child( - Text::new(self.local_diagnostic.diagnostic.message.clone(), text_style) - .with_soft_wrap(true), - ) + text.with_soft_wrap(true) .contained() .with_style(container_style) }) diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 6edf69d054..5f0e89b5bf 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -887,7 +887,7 @@ pub struct HoverPopover { pub error_container: ContainerStyle, pub block_style: ContainerStyle, pub prose: TextStyle, - pub diagnostic_source: TextStyle, + pub diagnostic_source_highlight: HighlightStyle, pub highlight: Color, } diff --git a/styles/src/styleTree/hoverPopover.ts b/styles/src/styleTree/hoverPopover.ts index 31e1d80778..fadd62db1d 100644 --- a/styles/src/styleTree/hoverPopover.ts +++ b/styles/src/styleTree/hoverPopover.ts @@ -40,7 +40,7 @@ export default function HoverPopover(colorScheme: ColorScheme) { padding: { top: 4 }, }, prose: text(layer, "sans", { size: "sm" }), - diagnosticSource: text(layer, "sans", { size: "sm", underline: true, color: foreground(layer, "accent") }), + diagnosticSourceHighlight: { underline: true, color: foreground(layer, "accent") }, highlight: colorScheme.ramps.neutral(0.5).alpha(0.2).hex(), // TODO: blend was used here. Replace with something better } } From a284fae515519d5d200dbe106c77713dff50f5e7 Mon Sep 17 00:00:00 2001 From: Julia Date: Wed, 26 Apr 2023 13:23:38 -0400 Subject: [PATCH 09/14] Don't hardcode `workspaceFolder` for ESLint adapter --- crates/zed/src/languages/typescript.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/crates/zed/src/languages/typescript.rs b/crates/zed/src/languages/typescript.rs index bfd6c11a27..e4a540dcd8 100644 --- a/crates/zed/src/languages/typescript.rs +++ b/crates/zed/src/languages/typescript.rs @@ -206,10 +206,6 @@ impl LspAdapter for EsLintLspAdapter { "shortenToSingleLine": false }, "nodePath": null, - "workspaceFolder": { - "name": "testing_ts", - "uri": "file:///Users/julia/Stuff/testing_ts" - }, "codeAction": { "disableRuleComment": { "enable": true, From 66d4cb8c14300cf4b14b81cb7e2146b0f654f28f Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 27 Apr 2023 11:39:34 -0700 Subject: [PATCH 10/14] Tweak rendering of multi-paragraph list items in markdown --- crates/editor/src/hover_popover.rs | 97 +++++++++++++++++++++++++++--- 1 file changed, 90 insertions(+), 7 deletions(-) diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 141559740e..9de9344ae6 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -382,7 +382,7 @@ fn render_blocks( *number += 1; *has_content = false; } else { - text.push_str("• "); + text.push_str("- "); } } } @@ -424,6 +424,10 @@ fn render_blocks( } } + if !text.is_empty() && !text.ends_with('\n') { + text.push('\n'); + } + RenderedInfo { theme_id, text, @@ -450,8 +454,11 @@ fn render_code( } fn new_paragraph(text: &mut String, list_stack: &mut Vec<(Option, bool)>) { + let mut is_subsequent_paragraph_of_list = false; if let Some((_, has_content)) = list_stack.last_mut() { - if !*has_content { + if *has_content { + is_subsequent_paragraph_of_list = true; + } else { *has_content = true; return; } @@ -466,6 +473,9 @@ fn new_paragraph(text: &mut String, list_stack: &mut Vec<(Option, bool)>) { for _ in 0..list_stack.len().saturating_sub(1) { text.push_str(" "); } + if is_subsequent_paragraph_of_list { + text.push_str(" "); + } } #[derive(Default)] @@ -664,6 +674,7 @@ mod tests { use lsp::LanguageServerId; use project::{HoverBlock, HoverBlockKind}; use smol::stream::StreamExt; + use unindent::Unindent; use util::test::marked_text_ranges; #[gpui::test] @@ -867,28 +878,99 @@ mod tests { struct Row { blocks: Vec, - expected_marked_text: &'static str, + 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", + expected_marked_text: "one «two» three\n".to_string(), expected_styles: vec![HighlightStyle { weight: Some(Weight::BOLD), ..Default::default() }], }, + // Links Row { blocks: vec![HoverBlock { text: "one [two](the-url) three".to_string(), kind: HoverBlockKind::Markdown, }], - expected_marked_text: "one «two» three", + expected_marked_text: "one «two» three\n".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](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(), @@ -903,7 +985,7 @@ mod tests { blocks, expected_marked_text, expected_styles, - } in rows + } in &rows[0..] { let rendered = render_blocks(0, &blocks, &Default::default(), &style); @@ -913,7 +995,8 @@ mod tests { .zip(expected_styles.iter().cloned()) .collect::>(); assert_eq!( - rendered.text, expected_text, + rendered.text, + dbg!(expected_text), "wrong text for input {blocks:?}" ); assert_eq!( From 87539e7b82da7dbabdfb2a48df88fc3a09095f00 Mon Sep 17 00:00:00 2001 From: Julia Date: Thu, 27 Apr 2023 15:04:48 -0400 Subject: [PATCH 11/14] Update test to not fail due to absence of diagnostic source --- crates/project/src/project_tests.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 5d062d42ce..fc530b5122 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -1190,6 +1190,7 @@ async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) { DiagnosticEntry { range: Point::new(3, 9)..Point::new(3, 11), diagnostic: Diagnostic { + source: Some("disk".into()), severity: DiagnosticSeverity::ERROR, message: "undefined variable 'BB'".to_string(), is_disk_based: true, @@ -1201,6 +1202,7 @@ async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) { DiagnosticEntry { range: Point::new(4, 9)..Point::new(4, 12), diagnostic: Diagnostic { + source: Some("disk".into()), severity: DiagnosticSeverity::ERROR, message: "undefined variable 'CCC'".to_string(), is_disk_based: true, @@ -1266,6 +1268,7 @@ async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) { DiagnosticEntry { range: Point::new(2, 9)..Point::new(2, 12), diagnostic: Diagnostic { + source: Some("disk".into()), severity: DiagnosticSeverity::WARNING, message: "unreachable statement".to_string(), is_disk_based: true, @@ -1277,6 +1280,7 @@ async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) { DiagnosticEntry { range: Point::new(2, 9)..Point::new(2, 10), diagnostic: Diagnostic { + source: Some("disk".into()), severity: DiagnosticSeverity::ERROR, message: "undefined variable 'A'".to_string(), is_disk_based: true, @@ -1356,6 +1360,7 @@ async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) { DiagnosticEntry { range: Point::new(2, 21)..Point::new(2, 22), diagnostic: Diagnostic { + source: Some("disk".into()), severity: DiagnosticSeverity::WARNING, message: "undefined variable 'A'".to_string(), is_disk_based: true, @@ -1367,6 +1372,7 @@ async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) { DiagnosticEntry { range: Point::new(3, 9)..Point::new(3, 14), diagnostic: Diagnostic { + source: Some("disk".into()), severity: DiagnosticSeverity::ERROR, message: "undefined variable 'BB'".to_string(), is_disk_based: true, From 8eb9c6563af552480eb4c4ea6eca961e7f62798a Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 27 Apr 2023 13:58:06 -0700 Subject: [PATCH 12/14] Generalize Text element to let you add arbitrary scene primitives for runs of text --- crates/gpui/examples/text.rs | 24 ++++--- crates/gpui/src/elements/text.rs | 118 ++++++++++++++----------------- 2 files changed, 67 insertions(+), 75 deletions(-) diff --git a/crates/gpui/examples/text.rs b/crates/gpui/examples/text.rs index f60e74deb1..a269706cc5 100644 --- a/crates/gpui/examples/text.rs +++ b/crates/gpui/examples/text.rs @@ -2,8 +2,8 @@ use gpui::{ color::Color, elements::Text, fonts::{HighlightStyle, TextStyle}, - platform::MouseButton, - AnyElement, Element, MouseRegion, + platform::{CursorStyle, MouseButton}, + AnyElement, CursorRegion, Element, MouseRegion, }; use log::LevelFilter; use simplelog::SimpleLogger; @@ -61,13 +61,19 @@ impl gpui::View for TextView { }, ) .with_highlights(vec![(17..26, underline), (34..40, underline)]) - .with_mouse_regions(vec![(17..26), (34..40)], move |ix, bounds| { - MouseRegion::new::(view_id, ix, bounds).on_click::( - MouseButton::Left, - move |_, _, _| { - eprintln!("clicked link {ix}"); - }, - ) + .with_custom_runs(vec![(17..26), (34..40)], move |ix, bounds, scene, _| { + scene.push_cursor_region(CursorRegion { + bounds, + style: CursorStyle::PointingHand, + }); + scene.push_mouse_region( + MouseRegion::new::(view_id, ix, bounds).on_click::( + MouseButton::Left, + move |_, _, _| { + eprintln!("clicked link {ix}"); + }, + ), + ); }) .into_any() } diff --git a/crates/gpui/src/elements/text.rs b/crates/gpui/src/elements/text.rs index 1d78ded4cd..a92581f6c8 100644 --- a/crates/gpui/src/elements/text.rs +++ b/crates/gpui/src/elements/text.rs @@ -6,10 +6,9 @@ use crate::{ vector::{vec2f, Vector2F}, }, json::{ToJson, Value}, - platform::CursorStyle, text_layout::{Line, RunStyle, ShapedBoundary}, - CursorRegion, Element, FontCache, MouseRegion, SceneBuilder, SizeConstraint, TextLayoutCache, - View, ViewContext, + AppContext, Element, FontCache, SceneBuilder, SizeConstraint, TextLayoutCache, View, + ViewContext, }; use log::warn; use serde_json::json; @@ -20,9 +19,9 @@ pub struct Text { style: TextStyle, soft_wrap: bool, highlights: Option, HighlightStyle)]>>, - mouse_runs: Option<( + custom_runs: Option<( Box<[Range]>, - Box MouseRegion>, + Box, )>, } @@ -39,7 +38,7 @@ impl Text { style, soft_wrap: true, highlights: None, - mouse_runs: None, + custom_runs: None, } } @@ -56,12 +55,12 @@ impl Text { self } - pub fn with_mouse_regions( + pub fn with_custom_runs( mut self, runs: impl Into]>>, - build_mouse_region: impl 'static + FnMut(usize, RectF) -> MouseRegion, + callback: impl 'static + FnMut(usize, RectF, &mut SceneBuilder, &mut AppContext), ) -> Self { - self.mouse_runs = Some((runs.into(), Box::new(build_mouse_region))); + self.custom_runs = Some((runs.into(), Box::new(callback))); self } @@ -176,17 +175,18 @@ impl Element for Text { ) -> Self::PaintState { let mut origin = bounds.origin(); let empty = Vec::new(); + let mut callback = |_, _, _: &mut SceneBuilder, _: &mut AppContext| {}; let mouse_runs; - let mut build_mouse_region; - if let Some((runs, build_region)) = &mut self.mouse_runs { + let custom_run_callback; + if let Some((runs, build_region)) = &mut self.custom_runs { mouse_runs = runs.iter(); - build_mouse_region = Some(build_region); + custom_run_callback = build_region.as_mut(); } else { mouse_runs = [].iter(); - build_mouse_region = None; + custom_run_callback = &mut callback; } - let mut mouse_runs = mouse_runs.enumerate().peekable(); + let mut custom_runs = mouse_runs.enumerate().peekable(); let mut offset = 0; for (ix, line) in layout.shaped_lines.iter().enumerate() { @@ -214,13 +214,13 @@ impl Element for Text { } } - // Add the mouse regions + // Paint any custom runs that intersect this line. let end_offset = offset + line.len(); - if let Some((mut mouse_run_ix, mut mouse_run_range)) = mouse_runs.peek().cloned() { - if mouse_run_range.start < end_offset { - let mut current_mouse_run = None; - if mouse_run_range.start <= offset { - current_mouse_run = Some((mouse_run_ix, origin)); + if let Some((custom_run_ix, custom_run_range)) = custom_runs.peek().cloned() { + if custom_run_range.start < end_offset { + let mut current_custom_run = None; + if custom_run_range.start <= offset { + current_custom_run = Some((custom_run_ix, custom_run_range.end, origin)); } let mut glyph_origin = origin; @@ -237,81 +237,67 @@ impl Element for Text { glyph_origin.set_x(glyph_origin.x() + glyph.position.x() - prev_position); prev_position = glyph.position.x(); + // If we've reached a soft wrap position, move down one line. If there + // is a custom run in-progress, paint it. if wrap_boundaries .peek() .map_or(false, |b| b.run_ix == run_ix && b.glyph_ix == glyph_ix) { - if let Some((mouse_run_ix, mouse_region_start)) = &mut current_mouse_run - { + if let Some((run_ix, _, run_origin)) = &mut current_custom_run { let bounds = RectF::from_points( - *mouse_region_start, + *run_origin, glyph_origin + vec2f(0., layout.line_height), ); - scene.push_cursor_region(CursorRegion { - bounds, - style: CursorStyle::PointingHand, - }); - scene.push_mouse_region((build_mouse_region.as_mut().unwrap())( - *mouse_run_ix, - bounds, - )); - *mouse_region_start = + custom_run_callback(*run_ix, bounds, scene, cx); + *run_origin = vec2f(origin.x(), glyph_origin.y() + layout.line_height); } - wrap_boundaries.next(); glyph_origin = vec2f(origin.x(), glyph_origin.y() + layout.line_height); } - if offset + glyph.index == mouse_run_range.start { - current_mouse_run = Some((mouse_run_ix, glyph_origin)); - } - if offset + glyph.index == mouse_run_range.end { - if let Some((mouse_run_ix, mouse_region_start)) = - current_mouse_run.take() - { + // If we've reached the end of the current custom run, paint it. + if let Some((run_ix, run_end_offset, run_origin)) = current_custom_run { + if offset + glyph.index == run_end_offset { + current_custom_run.take(); let bounds = RectF::from_points( - mouse_region_start, + run_origin, glyph_origin + vec2f(0., layout.line_height), ); - scene.push_cursor_region(CursorRegion { - bounds, - style: CursorStyle::PointingHand, - }); - scene.push_mouse_region((build_mouse_region.as_mut().unwrap())( - mouse_run_ix, - bounds, - )); - mouse_runs.next(); + custom_run_callback(run_ix, bounds, scene, cx); + custom_runs.next(); } - if let Some(next) = mouse_runs.peek() { - mouse_run_ix = next.0; - mouse_run_range = next.1; - if mouse_run_range.start >= end_offset { + if let Some((_, run_range)) = custom_runs.peek() { + if run_range.start >= end_offset { break; } - if mouse_run_range.start == offset + glyph.index { - current_mouse_run = Some((mouse_run_ix, glyph_origin)); + if run_range.start == offset + glyph.index { + current_custom_run = + Some((run_ix, run_range.end, glyph_origin)); } } } + + // If we've reached the start of a new custom run, start tracking it. + if let Some((run_ix, run_range)) = custom_runs.peek() { + if offset + glyph.index == run_range.start { + current_custom_run = Some((*run_ix, run_range.end, glyph_origin)); + } + } } - if let Some((mouse_run_ix, mouse_region_start)) = current_mouse_run { + // If a custom run extends beyond the end of the line, paint it. + if let Some((run_ix, run_end_offset, run_origin)) = current_custom_run { let line_end = glyph_origin + vec2f(line.width() - prev_position, 0.); let bounds = RectF::from_points( - mouse_region_start, + run_origin, line_end + vec2f(0., layout.line_height), ); - scene.push_cursor_region(CursorRegion { - bounds, - style: CursorStyle::PointingHand, - }); - scene.push_mouse_region((build_mouse_region.as_mut().unwrap())( - mouse_run_ix, - bounds, - )); + custom_run_callback(run_ix, bounds, scene, cx); + if end_offset == run_end_offset { + custom_runs.next(); + } } } } From 6042df393bf709205a197fdd611da0cafae4d018 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 27 Apr 2023 13:58:30 -0700 Subject: [PATCH 13/14] Give code spans in markdown a background highlight --- crates/editor/Cargo.toml | 2 +- crates/editor/src/hover_popover.rs | 105 +++++++++++++++++++---------- 2 files changed, 70 insertions(+), 37 deletions(-) diff --git a/crates/editor/Cargo.toml b/crates/editor/Cargo.toml index 8835147ba6..feb55e1b2f 100644 --- a/crates/editor/Cargo.toml +++ b/crates/editor/Cargo.toml @@ -55,7 +55,7 @@ log.workspace = true ordered-float.workspace = true parking_lot.workspace = true postage.workspace = true -pulldown-cmark = { version = "0.9.1", default-features = false } +pulldown-cmark = { version = "0.9.2", default-features = false } rand = { workspace = true, optional = true } serde.workspace = true serde_derive.workspace = true diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 9de9344ae6..80a1bb19a3 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -1,12 +1,11 @@ use futures::FutureExt; use gpui::{ actions, - color::Color, elements::{Flex, MouseEventHandler, Padding, ParentElement, Text}, fonts::{HighlightStyle, Underline, Weight}, impl_internal_actions, platform::{CursorStyle, MouseButton}, - AnyElement, AppContext, Element, ModelHandle, MouseRegion, Task, ViewContext, + AnyElement, AppContext, CursorRegion, Element, ModelHandle, MouseRegion, Task, ViewContext, }; use language::{Bias, DiagnosticEntry, DiagnosticSeverity, Language, LanguageRegistry}; use project::{HoverBlock, HoverBlockKind, Project}; @@ -275,8 +274,8 @@ fn render_blocks( ) -> RenderedInfo { let mut text = String::new(); let mut highlights = Vec::new(); - let mut link_ranges = Vec::new(); - let mut link_urls = Vec::new(); + let mut region_ranges = Vec::new(); + let mut regions = Vec::new(); for block in blocks { match &block.kind { @@ -315,7 +314,12 @@ fn render_blocks( if italic_depth > 0 { style.italic = Some(true); } - if link_url.is_some() { + if let Some(link_url) = link_url.clone() { + region_ranges.push(prev_len..text.len()); + regions.push(RenderedRegion { + link_url: Some(link_url), + code: false, + }); style.underline = Some(Underline { thickness: 1.0.into(), ..Default::default() @@ -338,13 +342,23 @@ fn render_blocks( } Event::Code(t) => { text.push_str(t.as_ref()); - highlights.push(( - prev_len..text.len(), - HighlightStyle { - color: Some(Color::red()), - ..Default::default() - }, - )); + region_ranges.push(prev_len..text.len()); + if link_url.is_some() { + highlights.push(( + prev_len..text.len(), + HighlightStyle { + underline: Some(Underline { + thickness: 1.0.into(), + ..Default::default() + }), + ..Default::default() + }, + )); + } + regions.push(RenderedRegion { + code: true, + link_url: link_url.clone(), + }); } Event::Start(tag) => match tag { Tag::Paragraph => new_paragraph(&mut text, &mut list_stack), @@ -363,7 +377,7 @@ fn render_blocks( } Tag::Emphasis => italic_depth += 1, Tag::Strong => bold_depth += 1, - Tag::Link(_, url, _) => link_url = Some((prev_len, url)), + Tag::Link(_, url, _) => link_url = Some(url.to_string()), Tag::List(number) => { list_stack.push((number, false)); } @@ -393,15 +407,8 @@ fn render_blocks( Tag::CodeBlock(_) => current_language = None, Tag::Emphasis => italic_depth -= 1, Tag::Strong => bold_depth -= 1, - Tag::Link(_, _, _) => { - if let Some((start_offset, link_url)) = link_url.take() { - link_ranges.push(start_offset..text.len()); - link_urls.push(link_url.to_string()); - } - } - Tag::List(_) => { - list_stack.pop(); - } + Tag::Link(_, _, _) => link_url = None, + Tag::List(_) => drop(list_stack.pop()), _ => {} }, Event::HardBreak => text.push('\n'), @@ -432,8 +439,8 @@ fn render_blocks( theme_id, text, highlights, - link_ranges, - link_urls, + region_ranges, + regions, } } @@ -542,8 +549,14 @@ struct RenderedInfo { theme_id: usize, text: String, highlights: Vec<(Range, HighlightStyle)>, - link_ranges: Vec>, - link_urls: Vec, + region_ranges: Vec>, + regions: Vec, +} + +#[derive(Debug, Clone)] +struct RenderedRegion { + code: bool, + link_url: Option, } impl InfoPopover { @@ -571,22 +584,42 @@ impl InfoPopover { let mut region_id = 0; let view_id = cx.view_id(); - let link_urls = rendered_content.link_urls.clone(); + let code_span_background_color = style.document_highlight_read_background; + let regions = rendered_content.regions.clone(); Flex::column() .scrollable::(1, None, cx) .with_child( Text::new(rendered_content.text.clone(), style.text.clone()) .with_highlights(rendered_content.highlights.clone()) - .with_mouse_regions( - rendered_content.link_ranges.clone(), - move |ix, bounds| { + .with_custom_runs( + rendered_content.region_ranges.clone(), + move |ix, bounds, scene, _| { region_id += 1; - let url = link_urls[ix].clone(); - MouseRegion::new::(view_id, region_id, bounds) - .on_click::(MouseButton::Left, move |_, _, cx| { - println!("clicked link {url}"); - cx.platform().open_url(&url); - }) + let region = regions[ix].clone(); + if let Some(url) = region.link_url { + scene.push_cursor_region(CursorRegion { + bounds, + style: CursorStyle::PointingHand, + }); + scene.push_mouse_region( + MouseRegion::new::(view_id, region_id, bounds) + .on_click::( + MouseButton::Left, + move |_, _, cx| { + println!("clicked link {url}"); + cx.platform().open_url(&url); + }, + ), + ); + } + if region.code { + scene.push_quad(gpui::Quad { + bounds, + background: Some(code_span_background_color), + border: Default::default(), + corner_radius: 2.0, + }); + } }, ) .with_soft_wrap(true), From 1533c17cd772f89487cf2502827780d4fad611b6 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 27 Apr 2023 14:39:00 -0700 Subject: [PATCH 14/14] Shutdown copilot server when quitting zed --- crates/copilot/src/copilot.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index 13aa904b5c..a5d26f8254 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -24,6 +24,7 @@ use std::{ mem, ops::Range, path::{Path, PathBuf}, + pin::Pin, sync::Arc, }; use util::{ @@ -271,6 +272,20 @@ pub struct Copilot { impl Entity for Copilot { type Event = (); + + fn app_will_quit( + &mut self, + _: &mut AppContext, + ) -> Option>>> { + match mem::replace(&mut self.server, CopilotServer::Disabled) { + CopilotServer::Running(server) => Some(Box::pin(async move { + if let Some(shutdown) = server.lsp.shutdown() { + shutdown.await; + } + })), + _ => None, + } + } } impl Copilot {