diff --git a/crates/diagnostics/src/diagnostic_renderer.rs b/crates/diagnostics/src/diagnostic_renderer.rs index 039ec57b9a..538fdb0e4b 100644 --- a/crates/diagnostics/src/diagnostic_renderer.rs +++ b/crates/diagnostics/src/diagnostic_renderer.rs @@ -28,6 +28,7 @@ impl DiagnosticRenderer { diagnostic_group: Vec>, buffer_id: BufferId, diagnostics_editor: Option>, + merge_same_row: bool, cx: &mut App, ) -> Vec { let Some(primary_ix) = diagnostic_group @@ -45,7 +46,7 @@ impl DiagnosticRenderer { if entry.diagnostic.is_primary { continue; } - if entry.range.start.row == primary.range.start.row { + if entry.range.start.row == primary.range.start.row && merge_same_row { same_row.push(entry) } else if entry.range.start.row.abs_diff(primary.range.start.row) < 5 { close.push(entry) @@ -54,28 +55,48 @@ impl DiagnosticRenderer { } } - let mut markdown = - Markdown::escape(&if let Some(source) = primary.diagnostic.source.as_ref() { - format!("{}: {}", source, primary.diagnostic.message) - } else { - primary.diagnostic.message - }) - .to_string(); + let mut markdown = String::new(); + let diagnostic = &primary.diagnostic; + markdown.push_str(&Markdown::escape(&diagnostic.message)); for entry in same_row { markdown.push_str("\n- hint: "); markdown.push_str(&Markdown::escape(&entry.diagnostic.message)) } + if diagnostic.source.is_some() || diagnostic.code.is_some() { + markdown.push_str(" ("); + } + if let Some(source) = diagnostic.source.as_ref() { + markdown.push_str(&Markdown::escape(&source)); + } + if diagnostic.source.is_some() && diagnostic.code.is_some() { + markdown.push(' '); + } + if let Some(code) = diagnostic.code.as_ref() { + if let Some(description) = diagnostic.code_description.as_ref() { + markdown.push('['); + markdown.push_str(&Markdown::escape(&code.to_string())); + markdown.push_str("]("); + markdown.push_str(&Markdown::escape(description.as_ref())); + markdown.push(')'); + } else { + markdown.push_str(&Markdown::escape(&code.to_string())); + } + } + if diagnostic.source.is_some() || diagnostic.code.is_some() { + markdown.push(')'); + } for (ix, entry) in &distant { markdown.push_str("\n- hint: ["); markdown.push_str(&Markdown::escape(&entry.diagnostic.message)); - markdown.push_str(&format!("](file://#diagnostic-{group_id}-{ix})\n",)) + markdown.push_str(&format!( + "](file://#diagnostic-{buffer_id}-{group_id}-{ix})\n", + )) } let mut results = vec![DiagnosticBlock { initial_range: primary.range, severity: primary.diagnostic.severity, - buffer_id, diagnostics_editor: diagnostics_editor.clone(), markdown: cx.new(|cx| Markdown::new(markdown.into(), None, None, cx)), }]; @@ -91,7 +112,6 @@ impl DiagnosticRenderer { results.push(DiagnosticBlock { initial_range: entry.range, severity: entry.diagnostic.severity, - buffer_id, diagnostics_editor: diagnostics_editor.clone(), markdown: cx.new(|cx| Markdown::new(markdown.into(), None, None, cx)), }); @@ -105,15 +125,12 @@ impl DiagnosticRenderer { }; let mut markdown = Markdown::escape(&markdown).to_string(); markdown.push_str(&format!( - " ([back](file://#diagnostic-{group_id}-{primary_ix}))" + " ([back](file://#diagnostic-{buffer_id}-{group_id}-{primary_ix}))" )); - // problem: group-id changes... - // - only an issue in diagnostics because caching results.push(DiagnosticBlock { initial_range: entry.range, severity: entry.diagnostic.severity, - buffer_id, diagnostics_editor: diagnostics_editor.clone(), markdown: cx.new(|cx| Markdown::new(markdown.into(), None, None, cx)), }); @@ -132,7 +149,7 @@ impl editor::DiagnosticRenderer for DiagnosticRenderer { editor: WeakEntity, cx: &mut App, ) -> Vec> { - let blocks = Self::diagnostic_blocks_for_group(diagnostic_group, buffer_id, None, cx); + let blocks = Self::diagnostic_blocks_for_group(diagnostic_group, buffer_id, None, true, cx); blocks .into_iter() .map(|block| { @@ -151,13 +168,40 @@ impl editor::DiagnosticRenderer for DiagnosticRenderer { }) .collect() } + + fn render_hover( + &self, + diagnostic_group: Vec>, + range: Range, + buffer_id: BufferId, + cx: &mut App, + ) -> Option> { + let blocks = + Self::diagnostic_blocks_for_group(diagnostic_group, buffer_id, None, false, cx); + blocks.into_iter().find_map(|block| { + if block.initial_range == range { + Some(block.markdown) + } else { + None + } + }) + } + + fn open_link( + &self, + editor: &mut Editor, + link: SharedString, + window: &mut Window, + cx: &mut Context, + ) { + DiagnosticBlock::open_link(editor, &None, link, window, cx); + } } #[derive(Clone)] pub(crate) struct DiagnosticBlock { pub(crate) initial_range: Range, pub(crate) severity: DiagnosticSeverity, - pub(crate) buffer_id: BufferId, pub(crate) markdown: Entity, pub(crate) diagnostics_editor: Option>, } @@ -181,7 +225,6 @@ impl DiagnosticBlock { let settings = ThemeSettings::get_global(cx); let editor_line_height = (settings.line_height() * settings.buffer_font_size(cx)).round(); let line_height = editor_line_height; - let buffer_id = self.buffer_id; let diagnostics_editor = self.diagnostics_editor.clone(); div() @@ -195,14 +238,11 @@ impl DiagnosticBlock { MarkdownElement::new(self.markdown.clone(), hover_markdown_style(bcx.window, cx)) .on_url_click({ move |link, window, cx| { - Self::open_link( - editor.clone(), - &diagnostics_editor, - link, - window, - buffer_id, - cx, - ) + editor + .update(cx, |editor, cx| { + Self::open_link(editor, &diagnostics_editor, link, window, cx) + }) + .ok(); } }), ) @@ -210,79 +250,71 @@ impl DiagnosticBlock { } pub fn open_link( - editor: WeakEntity, + editor: &mut Editor, diagnostics_editor: &Option>, link: SharedString, window: &mut Window, - buffer_id: BufferId, - cx: &mut App, + cx: &mut Context, ) { - editor - .update(cx, |editor, cx| { - let Some(diagnostic_link) = link.strip_prefix("file://#diagnostic-") else { - editor::hover_popover::open_markdown_url(link, window, cx); - return; - }; - let Some((group_id, ix)) = maybe!({ - let (group_id, ix) = diagnostic_link.split_once('-')?; - let group_id: usize = group_id.parse().ok()?; - let ix: usize = ix.parse().ok()?; - Some((group_id, ix)) - }) else { - return; - }; + let Some(diagnostic_link) = link.strip_prefix("file://#diagnostic-") else { + editor::hover_popover::open_markdown_url(link, window, cx); + return; + }; + let Some((buffer_id, group_id, ix)) = maybe!({ + let mut parts = diagnostic_link.split('-'); + let buffer_id: u64 = parts.next()?.parse().ok()?; + let group_id: usize = parts.next()?.parse().ok()?; + let ix: usize = parts.next()?.parse().ok()?; + Some((BufferId::new(buffer_id).ok()?, group_id, ix)) + }) else { + return; + }; - if let Some(diagnostics_editor) = diagnostics_editor { - if let Some(diagnostic) = diagnostics_editor - .update(cx, |diagnostics, _| { - diagnostics - .diagnostics - .get(&buffer_id) - .cloned() - .unwrap_or_default() - .into_iter() - .filter(|d| d.diagnostic.group_id == group_id) - .nth(ix) - }) - .ok() - .flatten() - { - let multibuffer = editor.buffer().read(cx); - let Some(snapshot) = multibuffer - .buffer(buffer_id) - .map(|entity| entity.read(cx).snapshot()) - else { - return; - }; - - for (excerpt_id, range) in multibuffer.excerpts_for_buffer(buffer_id, cx) { - if range.context.overlaps(&diagnostic.range, &snapshot) { - Self::jump_to( - editor, - Anchor::range_in_buffer( - excerpt_id, - buffer_id, - diagnostic.range, - ), - window, - cx, - ); - return; - } - } - } - } else { - if let Some(diagnostic) = editor - .snapshot(window, cx) - .buffer_snapshot - .diagnostic_group(buffer_id, group_id) + if let Some(diagnostics_editor) = diagnostics_editor { + if let Some(diagnostic) = diagnostics_editor + .update(cx, |diagnostics, _| { + diagnostics + .diagnostics + .get(&buffer_id) + .cloned() + .unwrap_or_default() + .into_iter() + .filter(|d| d.diagnostic.group_id == group_id) .nth(ix) - { - Self::jump_to(editor, diagnostic.range, window, cx) - } + }) + .ok() + .flatten() + { + let multibuffer = editor.buffer().read(cx); + let Some(snapshot) = multibuffer + .buffer(buffer_id) + .map(|entity| entity.read(cx).snapshot()) + else { + return; }; - }) - .ok(); + + for (excerpt_id, range) in multibuffer.excerpts_for_buffer(buffer_id, cx) { + if range.context.overlaps(&diagnostic.range, &snapshot) { + Self::jump_to( + editor, + Anchor::range_in_buffer(excerpt_id, buffer_id, diagnostic.range), + window, + cx, + ); + return; + } + } + } + } else { + if let Some(diagnostic) = editor + .snapshot(window, cx) + .buffer_snapshot + .diagnostic_group(buffer_id, group_id) + .nth(ix) + { + Self::jump_to(editor, diagnostic.range, window, cx) + } + }; } fn jump_to( diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index fc4dcba3c3..18602356c5 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -416,6 +416,7 @@ impl ProjectDiagnosticsEditor { group, buffer_snapshot.remote_id(), Some(this.clone()), + true, cx, ) })?; diff --git a/crates/diagnostics/src/diagnostics_tests.rs b/crates/diagnostics/src/diagnostics_tests.rs index 8e455d8523..f22104ae53 100644 --- a/crates/diagnostics/src/diagnostics_tests.rs +++ b/crates/diagnostics/src/diagnostics_tests.rs @@ -1,10 +1,13 @@ use super::*; use collections::{HashMap, HashSet}; use editor::{ - DisplayPoint, InlayId, - actions::{GoToDiagnostic, GoToPreviousDiagnostic, MoveToBeginning}, + DisplayPoint, EditorSettings, InlayId, + actions::{GoToDiagnostic, GoToPreviousDiagnostic, Hover, MoveToBeginning}, display_map::{DisplayRow, Inlay}, - test::{editor_content_with_blocks, editor_test_context::EditorTestContext}, + test::{ + editor_content_with_blocks, editor_lsp_test_context::EditorLspTestContext, + editor_test_context::EditorTestContext, + }, }; use gpui::{TestAppContext, VisualTestContext}; use indoc::indoc; @@ -134,11 +137,13 @@ async fn test_diagnostics(cx: &mut TestAppContext) { // comment 1 // comment 2 c(y); - § use of moved value value used here after move + § use of moved value + § value used here after move § hint: move occurs because `y` has type `Vec`, which does not § implement the `Copy` trait d(x); - § use of moved value value used here after move + § use of moved value + § value used here after move § hint: move occurs because `x` has type `Vec`, which does not § implement the `Copy` trait § hint: value moved here @@ -168,7 +173,7 @@ async fn test_diagnostics(cx: &mut TestAppContext) { lsp::Position::new(0, 15), ), severity: Some(lsp::DiagnosticSeverity::ERROR), - message: "mismatched types\nexpected `usize`, found `char`".to_string(), + message: "mismatched types expected `usize`, found `char`".to_string(), ..Default::default() }], version: None, @@ -206,11 +211,13 @@ async fn test_diagnostics(cx: &mut TestAppContext) { // comment 1 // comment 2 c(y); - § use of moved value value used here after move + § use of moved value + § value used here after move § hint: move occurs because `y` has type `Vec`, which does not § implement the `Copy` trait d(x); - § use of moved value value used here after move + § use of moved value + § value used here after move § hint: move occurs because `x` has type `Vec`, which does not § implement the `Copy` trait § hint: value moved here @@ -241,7 +248,7 @@ async fn test_diagnostics(cx: &mut TestAppContext) { lsp::Position::new(0, 15), ), severity: Some(lsp::DiagnosticSeverity::ERROR), - message: "mismatched types\nexpected `usize`, found `char`".to_string(), + message: "mismatched types expected `usize`, found `char`".to_string(), ..Default::default() }, lsp::Diagnostic { @@ -289,11 +296,13 @@ async fn test_diagnostics(cx: &mut TestAppContext) { // comment 1 // comment 2 c(y); - § use of moved value value used here after move + § use of moved value + § value used here after move § hint: move occurs because `y` has type `Vec`, which does not § implement the `Copy` trait d(x); - § use of moved value value used here after move + § use of moved value + § value used here after move § hint: move occurs because `x` has type `Vec`, which does not § implement the `Copy` trait § hint: value moved here @@ -1192,8 +1201,219 @@ async fn cycle_through_same_place_diagnostics(cx: &mut TestAppContext) { "}); } +#[gpui::test] +async fn test_diagnostics_with_links(cx: &mut TestAppContext) { + init_test(cx); + + let mut cx = EditorTestContext::new(cx).await; + + cx.set_state(indoc! {" + fn func(abˇc def: i32) -> u32 { + } + "}); + let lsp_store = + cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store()); + + cx.update(|_, cx| { + lsp_store.update(cx, |lsp_store, cx| { + lsp_store.update_diagnostics( + LanguageServerId(0), + lsp::PublishDiagnosticsParams { + uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(), + version: None, + diagnostics: vec![lsp::Diagnostic { + range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 12)), + severity: Some(lsp::DiagnosticSeverity::ERROR), + message: "we've had problems with , and is broken".to_string(), + ..Default::default() + }], + }, + &[], + cx, + ) + }) + }).unwrap(); + cx.run_until_parked(); + cx.update_editor(|editor, window, cx| { + editor::hover_popover::hover(editor, &Default::default(), window, cx) + }); + cx.run_until_parked(); + cx.update_editor(|editor, _, _| assert!(editor.hover_state.diagnostic_popover.is_some())) +} + +#[gpui::test] +async fn test_hover_diagnostic_and_info_popovers(cx: &mut gpui::TestAppContext) { + init_test(cx); + + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), + ..Default::default() + }, + cx, + ) + .await; + + // Hover with just diagnostic, pops DiagnosticPopover immediately and then + // info popover once request completes + cx.set_state(indoc! {" + fn teˇst() { println!(); } + "}); + // Send diagnostic to client + let range = cx.lsp_range(indoc! {" + fn «test»() { println!(); } + "}); + let lsp_store = + cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store()); + cx.update(|_, cx| { + lsp_store.update(cx, |lsp_store, cx| { + lsp_store.update_diagnostics( + LanguageServerId(0), + lsp::PublishDiagnosticsParams { + uri: lsp::Url::from_file_path(path!("/root/dir/file.rs")).unwrap(), + version: None, + diagnostics: vec![lsp::Diagnostic { + range, + severity: Some(lsp::DiagnosticSeverity::ERROR), + message: "A test diagnostic message.".to_string(), + ..Default::default() + }], + }, + &[], + cx, + ) + }) + }) + .unwrap(); + cx.run_until_parked(); + + // Hover pops diagnostic immediately + cx.update_editor(|editor, window, cx| editor::hover_popover::hover(editor, &Hover, window, cx)); + cx.background_executor.run_until_parked(); + + cx.editor(|Editor { hover_state, .. }, _, _| { + assert!(hover_state.diagnostic_popover.is_some()); + assert!(hover_state.info_popovers.is_empty()); + }); + + // Info Popover shows after request responded to + let range = cx.lsp_range(indoc! {" + fn «test»() { println!(); } + "}); + cx.set_request_handler::(move |_, _, _| async move { + Ok(Some(lsp::Hover { + contents: lsp::HoverContents::Markup(lsp::MarkupContent { + kind: lsp::MarkupKind::Markdown, + value: "some new docs".to_string(), + }), + range: Some(range), + })) + }); + let delay = cx.update(|_, cx| EditorSettings::get_global(cx).hover_popover_delay + 1); + cx.background_executor + .advance_clock(Duration::from_millis(delay)); + + cx.background_executor.run_until_parked(); + cx.editor(|Editor { hover_state, .. }, _, _| { + hover_state.diagnostic_popover.is_some() && hover_state.info_task.is_some() + }); +} +#[gpui::test] +async fn test_diagnostics_with_code(cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + path!("/root"), + json!({ + "main.js": " + function test() { + const x = 10; + const y = 20; + return 1; + } + test(); + " + .unindent(), + }), + ) + .await; + + let language_server_id = LanguageServerId(0); + let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; + let lsp_store = project.read_with(cx, |project, _| project.lsp_store()); + let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let cx = &mut VisualTestContext::from_window(*window, cx); + let workspace = window.root(cx).unwrap(); + let uri = lsp::Url::from_file_path(path!("/root/main.js")).unwrap(); + + // Create diagnostics with code fields + lsp_store.update(cx, |lsp_store, cx| { + lsp_store + .update_diagnostics( + language_server_id, + lsp::PublishDiagnosticsParams { + uri: uri.clone(), + diagnostics: vec![ + lsp::Diagnostic { + range: lsp::Range::new( + lsp::Position::new(1, 4), + lsp::Position::new(1, 14), + ), + severity: Some(lsp::DiagnosticSeverity::WARNING), + code: Some(lsp::NumberOrString::String("no-unused-vars".to_string())), + source: Some("eslint".to_string()), + message: "'x' is assigned a value but never used".to_string(), + ..Default::default() + }, + lsp::Diagnostic { + range: lsp::Range::new( + lsp::Position::new(2, 4), + lsp::Position::new(2, 14), + ), + severity: Some(lsp::DiagnosticSeverity::WARNING), + code: Some(lsp::NumberOrString::String("no-unused-vars".to_string())), + source: Some("eslint".to_string()), + message: "'y' is assigned a value but never used".to_string(), + ..Default::default() + }, + ], + version: None, + }, + &[], + cx, + ) + .unwrap(); + }); + + // Open the project diagnostics view + let diagnostics = window.build_entity(cx, |window, cx| { + ProjectDiagnosticsEditor::new(true, project.clone(), workspace.downgrade(), window, cx) + }); + let editor = diagnostics.update(cx, |diagnostics, _| diagnostics.editor.clone()); + + diagnostics + .next_notification(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10), cx) + .await; + + // Verify that the diagnostic codes are displayed correctly + pretty_assertions::assert_eq!( + editor_content_with_blocks(&editor, cx), + indoc::indoc! { + "§ main.js + § ----- + function test() { + const x = 10; § 'x' is assigned a value but never used (eslint no-unused-vars) + const y = 20; § 'y' is assigned a value but never used (eslint no-unused-vars) + return 1; + }" + } + ); +} + fn init_test(cx: &mut TestAppContext) { cx.update(|cx| { + env_logger::try_init().ok(); let settings = SettingsStore::test(cx); cx.set_global(settings); theme::init(theme::LoadThemes::JustBase, cx); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index aae7a6a859..47adc19d43 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -394,10 +394,32 @@ pub trait DiagnosticRenderer { editor: WeakEntity, cx: &mut App, ) -> Vec>; + + fn render_hover( + &self, + diagnostic_group: Vec>, + range: Range, + buffer_id: BufferId, + cx: &mut App, + ) -> Option>; + + fn open_link( + &self, + editor: &mut Editor, + link: SharedString, + window: &mut Window, + cx: &mut Context, + ); } pub(crate) struct GlobalDiagnosticRenderer(pub Arc); +impl GlobalDiagnosticRenderer { + fn global(cx: &App) -> Option> { + cx.try_global::().map(|g| g.0.clone()) + } +} + impl gpui::Global for GlobalDiagnosticRenderer {} pub fn set_diagnostic_renderer(renderer: impl DiagnosticRenderer + 'static, cx: &mut App) { cx.set_global(GlobalDiagnosticRenderer(Arc::new(renderer))); @@ -867,7 +889,7 @@ pub struct Editor { read_only: bool, leader_peer_id: Option, remote_id: Option, - hover_state: HoverState, + pub hover_state: HoverState, pending_mouse_down: Option>>>, gutter_hovered: bool, hovered_link_state: Option, @@ -14788,25 +14810,17 @@ impl Editor { } self.dismiss_diagnostics(cx); let snapshot = self.snapshot(window, cx); - let Some(diagnostic_renderer) = cx - .try_global::() - .map(|g| g.0.clone()) - else { + let buffer = self.buffer.read(cx).snapshot(cx); + let Some(renderer) = GlobalDiagnosticRenderer::global(cx) else { return; }; - let buffer = self.buffer.read(cx).snapshot(cx); let diagnostic_group = buffer .diagnostic_group(buffer_id, diagnostic.diagnostic.group_id) .collect::>(); - let blocks = diagnostic_renderer.render_group( - diagnostic_group, - buffer_id, - snapshot, - cx.weak_entity(), - cx, - ); + let blocks = + renderer.render_group(diagnostic_group, buffer_id, snapshot, cx.weak_entity(), cx); let blocks = self.display_map.update(cx, |display_map, cx| { display_map.insert_blocks(blocks, cx).into_iter().collect() diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 6de271a85c..07bcbb40e1 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -12855,46 +12855,6 @@ async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mu "}); } -#[gpui::test] -async fn test_diagnostics_with_links(cx: &mut TestAppContext) { - init_test(cx, |_| {}); - - let mut cx = EditorTestContext::new(cx).await; - - cx.set_state(indoc! {" - fn func(abˇc def: i32) -> u32 { - } - "}); - let lsp_store = - cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store()); - - cx.update(|_, cx| { - lsp_store.update(cx, |lsp_store, cx| { - lsp_store.update_diagnostics( - LanguageServerId(0), - lsp::PublishDiagnosticsParams { - uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(), - version: None, - diagnostics: vec![lsp::Diagnostic { - range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 12)), - severity: Some(lsp::DiagnosticSeverity::ERROR), - message: "we've had problems with , and is broken".to_string(), - ..Default::default() - }], - }, - &[], - cx, - ) - }) - }).unwrap(); - cx.run_until_parked(); - cx.update_editor(|editor, window, cx| { - hover_popover::hover(editor, &Default::default(), window, cx) - }); - cx.run_until_parked(); - cx.update_editor(|editor, _, _| assert!(editor.hover_state.diagnostic_popover.is_some())) -} - #[gpui::test] async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) { init_test(cx, |_| {}); diff --git a/crates/editor/src/hover_links.rs b/crates/editor/src/hover_links.rs index bb3b12fb89..42a8fe70fc 100644 --- a/crates/editor/src/hover_links.rs +++ b/crates/editor/src/hover_links.rs @@ -71,7 +71,7 @@ pub enum HoverLink { } #[derive(Debug, Clone, PartialEq, Eq)] -pub(crate) struct InlayHighlight { +pub struct InlayHighlight { pub inlay: InlayId, pub inlay_position: Anchor, pub range: Range, diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index e86070a205..586aa7bb02 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -1,6 +1,6 @@ use crate::{ ActiveDiagnostic, Anchor, AnchorRangeExt, DisplayPoint, DisplayRow, Editor, EditorSettings, - EditorSnapshot, Hover, + EditorSnapshot, GlobalDiagnosticRenderer, Hover, display_map::{InlayOffset, ToDisplayPoint, invisibles::is_invisible}, hover_links::{InlayHighlight, RangeInEditor}, scroll::{Autoscroll, ScrollAmount}, @@ -15,7 +15,7 @@ use itertools::Itertools; use language::{DiagnosticEntry, Language, LanguageRegistry}; use lsp::DiagnosticSeverity; use markdown::{Markdown, MarkdownElement, MarkdownStyle}; -use multi_buffer::{MultiOrSingleBufferOffsetRange, ToOffset}; +use multi_buffer::{MultiOrSingleBufferOffsetRange, ToOffset, ToPoint}; use project::{HoverBlock, HoverBlockKind, InlayHintLabelPart}; use settings::Settings; use std::{borrow::Cow, cell::RefCell}; @@ -81,7 +81,7 @@ pub fn show_keyboard_hover( .as_ref() .and_then(|d| { if *d.keyboard_grace.borrow() { - d.anchor + Some(d.anchor) } else { None } @@ -283,6 +283,7 @@ fn show_hover( None }; + let renderer = GlobalDiagnosticRenderer::global(cx); let task = cx.spawn_in(window, async move |this, cx| { async move { // If we need to delay, delay a set amount initially before making the lsp request @@ -313,28 +314,35 @@ fn show_hover( } else { snapshot .buffer_snapshot - .diagnostics_in_range::(offset..offset) - .filter(|diagnostic| Some(diagnostic.diagnostic.group_id) != active_group_id) + .diagnostics_with_buffer_ids_in_range::(offset..offset) + .filter(|(_, diagnostic)| { + Some(diagnostic.diagnostic.group_id) != active_group_id + }) // Find the entry with the most specific range - .min_by_key(|entry| entry.range.len()) + .min_by_key(|(_, entry)| entry.range.len()) }; - let diagnostic_popover = if let Some(local_diagnostic) = local_diagnostic { - let text = match local_diagnostic.diagnostic.source { - Some(ref source) => { - format!("{source}: {}", local_diagnostic.diagnostic.message) - } - None => local_diagnostic.diagnostic.message.clone(), - }; - let local_diagnostic = DiagnosticEntry { - diagnostic: local_diagnostic.diagnostic, - range: snapshot - .buffer_snapshot - .anchor_before(local_diagnostic.range.start) - ..snapshot - .buffer_snapshot - .anchor_after(local_diagnostic.range.end), - }; + let diagnostic_popover = if let Some((buffer_id, local_diagnostic)) = local_diagnostic { + let group = snapshot + .buffer_snapshot + .diagnostic_group(buffer_id, local_diagnostic.diagnostic.group_id) + .collect::>(); + let point_range = local_diagnostic + .range + .start + .to_point(&snapshot.buffer_snapshot) + ..local_diagnostic + .range + .end + .to_point(&snapshot.buffer_snapshot); + let markdown = cx.update(|_, cx| { + renderer + .as_ref() + .and_then(|renderer| { + renderer.render_hover(group, point_range, buffer_id, cx) + }) + .ok_or_else(|| anyhow::anyhow!("no rendered diagnostic")) + })??; let (background_color, border_color) = cx.update(|_, cx| { let status_colors = cx.theme().status(); @@ -359,28 +367,26 @@ fn show_hover( } })?; - let parsed_content = cx - .new(|cx| Markdown::new_text(SharedString::new(text), cx)) - .ok(); + let subscription = + this.update(cx, |_, cx| cx.observe(&markdown, |_, _, cx| cx.notify()))?; - let subscription = this - .update(cx, |_, cx| { - if let Some(parsed_content) = &parsed_content { - Some(cx.observe(parsed_content, |_, _, cx| cx.notify())) - } else { - None - } - }) - .ok() - .flatten(); + let local_diagnostic = DiagnosticEntry { + diagnostic: local_diagnostic.diagnostic, + range: snapshot + .buffer_snapshot + .anchor_before(local_diagnostic.range.start) + ..snapshot + .buffer_snapshot + .anchor_after(local_diagnostic.range.end), + }; Some(DiagnosticPopover { local_diagnostic, - parsed_content, + markdown, border_color, background_color, keyboard_grace: Rc::new(RefCell::new(ignore_timeout)), - anchor: Some(anchor), + anchor, _subscription: subscription, }) } else { @@ -719,7 +725,7 @@ pub fn open_markdown_url(link: SharedString, window: &mut Window, cx: &mut App) #[derive(Default)] pub struct HoverState { - pub(crate) info_popovers: Vec, + pub info_popovers: Vec, pub diagnostic_popover: Option, pub triggered_from: Option, pub info_task: Option>>, @@ -789,23 +795,25 @@ impl HoverState { } } if let Some(diagnostic_popover) = &self.diagnostic_popover { - if let Some(markdown_view) = &diagnostic_popover.parsed_content { - if markdown_view.focus_handle(cx).is_focused(window) { - hover_popover_is_focused = true; - } + if diagnostic_popover + .markdown + .focus_handle(cx) + .is_focused(window) + { + hover_popover_is_focused = true; } } hover_popover_is_focused } } -pub(crate) struct InfoPopover { - pub(crate) symbol_range: RangeInEditor, - pub(crate) parsed_content: Option>, - pub(crate) scroll_handle: ScrollHandle, - pub(crate) scrollbar_state: ScrollbarState, - pub(crate) keyboard_grace: Rc>, - pub(crate) anchor: Option, +pub struct InfoPopover { + pub symbol_range: RangeInEditor, + pub parsed_content: Option>, + pub scroll_handle: ScrollHandle, + pub scrollbar_state: ScrollbarState, + pub keyboard_grace: Rc>, + pub anchor: Option, _subscription: Option, } @@ -897,12 +905,12 @@ impl InfoPopover { pub struct DiagnosticPopover { pub(crate) local_diagnostic: DiagnosticEntry, - parsed_content: Option>, + markdown: Entity, border_color: Hsla, background_color: Hsla, pub keyboard_grace: Rc>, - pub anchor: Option, - _subscription: Option, + pub anchor: Anchor, + _subscription: Subscription, } impl DiagnosticPopover { @@ -913,6 +921,7 @@ impl DiagnosticPopover { cx: &mut Context, ) -> AnyElement { let keyboard_grace = Rc::clone(&self.keyboard_grace); + let this = cx.entity().downgrade(); div() .id("diagnostic") .block() @@ -935,51 +944,29 @@ impl DiagnosticPopover { *keyboard_grace = false; cx.stop_propagation(); }) - .when_some(self.parsed_content.clone(), |this, markdown| { - this.child( - div() - .py_1() - .px_2() - .child( - MarkdownElement::new(markdown, { - let settings = ThemeSettings::get_global(cx); - let mut base_text_style = window.text_style(); - base_text_style.refine(&TextStyleRefinement { - font_family: Some(settings.ui_font.family.clone()), - font_fallbacks: settings.ui_font.fallbacks.clone(), - font_size: Some(settings.ui_font_size(cx).into()), - color: Some(cx.theme().colors().editor_foreground), - background_color: Some(gpui::transparent_black()), - ..Default::default() - }); - MarkdownStyle { - base_text_style, - selection_background_color: { - cx.theme().players().local().selection - }, - link: TextStyleRefinement { - underline: Some(gpui::UnderlineStyle { - thickness: px(1.), - color: Some(cx.theme().colors().editor_foreground), - wavy: false, - }), - ..Default::default() - }, - ..Default::default() - } - }) - .code_block_renderer(markdown::CodeBlockRenderer::Default { - copy_button: false, - border: false, - }) - .on_url_click(open_markdown_url), + .child( + div() + .py_1() + .px_2() + .child( + MarkdownElement::new( + self.markdown.clone(), + hover_markdown_style(window, cx), ) - .bg(self.background_color) - .border_1() - .border_color(self.border_color) - .rounded_lg(), - ) - }) + .on_url_click(move |link, window, cx| { + if let Some(renderer) = GlobalDiagnosticRenderer::global(cx) { + this.update(cx, |this, cx| { + renderer.as_ref().open_link(this, link, window, cx); + }) + .ok(); + } + }), + ) + .bg(self.background_color) + .border_1() + .border_color(self.border_color) + .rounded_lg(), + ) .into_any_element() } } @@ -998,8 +985,7 @@ mod tests { use collections::BTreeSet; use gpui::App; use indoc::indoc; - use language::{Diagnostic, DiagnosticSet, language_settings::InlayHintSettings}; - use lsp::LanguageServerId; + use language::language_settings::InlayHintSettings; use markdown::parser::MarkdownEvent; use smol::stream::StreamExt; use std::sync::atomic; @@ -1484,76 +1470,6 @@ mod tests { }); } - #[gpui::test] - async fn test_hover_diagnostic_and_info_popovers(cx: &mut gpui::TestAppContext) { - init_test(cx, |_| {}); - - let mut cx = EditorLspTestContext::new_rust( - lsp::ServerCapabilities { - hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), - ..Default::default() - }, - cx, - ) - .await; - - // Hover with just diagnostic, pops DiagnosticPopover immediately and then - // info popover once request completes - cx.set_state(indoc! {" - fn teˇst() { println!(); } - "}); - - // Send diagnostic to client - let range = cx.text_anchor_range(indoc! {" - fn «test»() { println!(); } - "}); - cx.update_buffer(|buffer, cx| { - let snapshot = buffer.text_snapshot(); - let set = DiagnosticSet::from_sorted_entries( - vec![DiagnosticEntry { - range, - diagnostic: Diagnostic { - message: "A test diagnostic message.".to_string(), - ..Default::default() - }, - }], - &snapshot, - ); - buffer.update_diagnostics(LanguageServerId(0), set, cx); - }); - - // Hover pops diagnostic immediately - cx.update_editor(|editor, window, cx| hover(editor, &Hover, window, cx)); - cx.background_executor.run_until_parked(); - - cx.editor(|Editor { hover_state, .. }, _, _| { - assert!( - hover_state.diagnostic_popover.is_some() && hover_state.info_popovers.is_empty() - ) - }); - - // Info Popover shows after request responded to - let range = cx.lsp_range(indoc! {" - fn «test»() { println!(); } - "}); - cx.set_request_handler::(move |_, _, _| async move { - Ok(Some(lsp::Hover { - contents: lsp::HoverContents::Markup(lsp::MarkupContent { - kind: lsp::MarkupKind::Markdown, - value: "some new docs".to_string(), - }), - range: Some(range), - })) - }); - cx.background_executor - .advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100)); - - cx.background_executor.run_until_parked(); - cx.editor(|Editor { hover_state, .. }, _, _| { - hover_state.diagnostic_popover.is_some() && hover_state.info_task.is_some() - }); - } - #[gpui::test] // https://github.com/zed-industries/zed/issues/15498 async fn test_info_hover_with_hrs(cx: &mut gpui::TestAppContext) { diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index aac6df01b2..ce6405514f 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -208,6 +208,7 @@ pub struct Diagnostic { pub source: Option, /// A machine-readable code that identifies this diagnostic. pub code: Option, + pub code_description: Option, /// Whether this diagnostic is a hint, warning, or error. pub severity: DiagnosticSeverity, /// The human-readable message associated with this diagnostic. @@ -4612,6 +4613,7 @@ impl Default for Diagnostic { Self { source: Default::default(), code: None, + code_description: None, severity: DiagnosticSeverity::ERROR, message: Default::default(), group_id: 0, diff --git a/crates/language/src/proto.rs b/crates/language/src/proto.rs index 8069418072..d918b86dd7 100644 --- a/crates/language/src/proto.rs +++ b/crates/language/src/proto.rs @@ -213,6 +213,11 @@ pub fn serialize_diagnostics<'a>( group_id: entry.diagnostic.group_id as u64, is_primary: entry.diagnostic.is_primary, code: entry.diagnostic.code.as_ref().map(|s| s.to_string()), + code_description: entry + .diagnostic + .code_description + .as_ref() + .map(|s| s.to_string()), is_disk_based: entry.diagnostic.is_disk_based, is_unnecessary: entry.diagnostic.is_unnecessary, data: entry.diagnostic.data.as_ref().map(|data| data.to_string()), @@ -419,6 +424,9 @@ pub fn deserialize_diagnostics( message: diagnostic.message, group_id: diagnostic.group_id as usize, code: diagnostic.code.map(lsp::NumberOrString::from_string), + code_description: diagnostic + .code_description + .and_then(|s| lsp::Url::parse(&s).ok()), is_primary: diagnostic.is_primary, is_disk_based: diagnostic.is_disk_based, is_unnecessary: diagnostic.is_unnecessary, diff --git a/crates/markdown/src/markdown.rs b/crates/markdown/src/markdown.rs index 5fad779da1..a6b7914d65 100644 --- a/crates/markdown/src/markdown.rs +++ b/crates/markdown/src/markdown.rs @@ -215,11 +215,16 @@ impl Markdown { } pub fn escape(s: &str) -> Cow { - let count = s.bytes().filter(|c| c.is_ascii_punctuation()).count(); + let count = s + .bytes() + .filter(|c| *c == b'\n' || c.is_ascii_punctuation()) + .count(); if count > 0 { let mut output = String::with_capacity(s.len() + count); for c in s.chars() { - if c.is_ascii_punctuation() { + if c == '\n' { + output.push('\n') + } else if c.is_ascii_punctuation() { output.push('\\') } output.push(c) diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index fa763b81b8..06a461bb5b 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -5950,6 +5950,29 @@ impl MultiBufferSnapshot { .map(|(range, diagnostic, _)| DiagnosticEntry { diagnostic, range }) } + pub fn diagnostics_with_buffer_ids_in_range<'a, T>( + &'a self, + range: Range, + ) -> impl Iterator)> + 'a + where + T: 'a + + text::ToOffset + + text::FromAnchor + + TextDimension + + Ord + + Sub + + fmt::Debug, + { + self.lift_buffer_metadata(range, move |buffer, buffer_range| { + Some( + buffer + .diagnostics_in_range(buffer_range.start..buffer_range.end, false) + .map(|entry| (entry.range, entry.diagnostic)), + ) + }) + .map(|(range, diagnostic, b)| (b.buffer_id, DiagnosticEntry { diagnostic, range })) + } + pub fn syntax_ancestor( &self, range: Range, diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 4b39b9496d..0584b55513 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -8613,6 +8613,10 @@ impl LspStore { diagnostic: Diagnostic { source: diagnostic.source.clone(), code: diagnostic.code.clone(), + code_description: diagnostic + .code_description + .as_ref() + .map(|d| d.href.clone()), severity: diagnostic.severity.unwrap_or(DiagnosticSeverity::ERROR), message: diagnostic.message.trim().to_string(), group_id, @@ -8631,6 +8635,10 @@ impl LspStore { diagnostic: Diagnostic { source: diagnostic.source.clone(), code: diagnostic.code.clone(), + code_description: diagnostic + .code_description + .as_ref() + .map(|c| c.href.clone()), severity: DiagnosticSeverity::INFORMATION, message: info.message.trim().to_string(), group_id, diff --git a/crates/proto/proto/buffer.proto b/crates/proto/proto/buffer.proto index e5b610ee55..5bb18e54d4 100644 --- a/crates/proto/proto/buffer.proto +++ b/crates/proto/proto/buffer.proto @@ -270,6 +270,7 @@ message Diagnostic { Hint = 4; } optional string data = 12; + optional string code_description = 13; } message SearchQuery {