From 1da60a8518d5f94304264a17870e34df4de70b50 Mon Sep 17 00:00:00 2001 From: Mikhail Pertsev Date: Tue, 5 May 2026 17:52:45 +0200 Subject: [PATCH] editor: Extract Diagnostics code out of `editor.rs` (#55747) cc @SomeoneToIgnore ## Summary Follow-up to https://github.com/zed-industries/zed/discussions/55352, where the conclusion was to split `editor.rs` incrementally by topic instead of all at once. This mechanically extracts diagnostics-related editor code into `crates/editor/src/editor/diagnostics.rs` while preserving the existing public API via re-exports. ## Testing - `cargo check -p editor --lib` - `cargo check -p diagnostics --lib` - `cargo check -p diagnostics --tests` Self-Review Checklist: - [x] I've reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments - [x] The content is consistent with the [UI/UX checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) - [x] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable Release Notes: - N/A --- crates/diagnostics/src/diagnostics_tests.rs | 8 +- crates/editor/src/display_map.rs | 16 +- crates/editor/src/editor.rs | 513 +------------------ crates/editor/src/editor/diagnostics.rs | 519 ++++++++++++++++++++ crates/editor/src/element.rs | 21 +- crates/editor/src/hover_popover.rs | 12 +- 6 files changed, 553 insertions(+), 536 deletions(-) create mode 100644 crates/editor/src/editor/diagnostics.rs diff --git a/crates/diagnostics/src/diagnostics_tests.rs b/crates/diagnostics/src/diagnostics_tests.rs index 527f5b5bfcb..c587e61c4f4 100644 --- a/crates/diagnostics/src/diagnostics_tests.rs +++ b/crates/diagnostics/src/diagnostics_tests.rs @@ -1037,9 +1037,7 @@ async fn active_diagnostics_dismiss_after_invalidation(cx: &mut TestAppContext) cx.update_editor(|editor, window, cx| { editor.go_to_diagnostic(&GoToDiagnostic::default(), window, cx); assert_eq!( - editor - .active_diagnostic_group() - .map(|diagnostics_group| diagnostics_group.active_message.as_str()), + editor.active_diagnostic_message(), Some(message), "Should have a diagnostics group activated" ); @@ -1069,7 +1067,7 @@ async fn active_diagnostics_dismiss_after_invalidation(cx: &mut TestAppContext) }); cx.run_until_parked(); cx.update_editor(|editor, _, _| { - assert_eq!(editor.active_diagnostic_group(), None); + assert_eq!(editor.active_diagnostic_message(), None); }); cx.assert_editor_state(indoc! {" fn func(abcˇ def: i32) -> u32 { @@ -1078,7 +1076,7 @@ async fn active_diagnostics_dismiss_after_invalidation(cx: &mut TestAppContext) cx.update_editor(|editor, window, cx| { editor.go_to_diagnostic(&GoToDiagnostic::default(), window, cx); - assert_eq!(editor.active_diagnostic_group(), None); + assert_eq!(editor.active_diagnostic_message(), None); }); cx.assert_editor_state(indoc! {" fn func(abcˇ def: i32) -> u32 { diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 552eca261e9..db01bbb1786 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -94,7 +94,7 @@ pub use wrap_map::{WrapPoint, WrapRow, WrapSnapshot}; use collections::{HashMap, HashSet, IndexSet}; use gpui::{ - App, Context, Entity, EntityId, Font, HighlightStyle, LineLayout, Pixels, UnderlineStyle, + App, Context, Entity, EntityId, Font, HighlightStyle, Hsla, LineLayout, Pixels, UnderlineStyle, WeakEntity, }; use language::{ @@ -113,6 +113,7 @@ use settings::Settings; use smallvec::SmallVec; use sum_tree::{Bias, TreeMap}; use text::{BufferId, LineIndent, Patch}; +use theme::StatusColors; use ui::{SharedString, px}; use unicode_segmentation::UnicodeSegmentation; use ztracing::instrument; @@ -1848,8 +1849,7 @@ impl DisplaySnapshot { && editor_style.show_underlines && !(chunk.is_unnecessary && severity > lsp::DiagnosticSeverity::WARNING)) .then(|| { - let diagnostic_color = - super::diagnostic_style(severity, &editor_style.status); + let diagnostic_color = diagnostic_style(severity, &editor_style.status); UnderlineStyle { color: Some(diagnostic_color), thickness: 1.0.into(), @@ -2414,6 +2414,16 @@ impl DisplaySnapshot { } } +fn diagnostic_style(severity: lsp::DiagnosticSeverity, colors: &StatusColors) -> Hsla { + match severity { + lsp::DiagnosticSeverity::ERROR => colors.error, + lsp::DiagnosticSeverity::WARNING => colors.warning, + lsp::DiagnosticSeverity::INFORMATION => colors.info, + lsp::DiagnosticSeverity::HINT => colors.hint, + _ => colors.ignored, + } +} + impl std::ops::Deref for DisplaySnapshot { type Target = BlockSnapshot; diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index a57b7058560..649ffbfae8a 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -57,7 +57,12 @@ mod signature_help; #[cfg(any(test, feature = "test-support"))] pub mod test; +#[path = "editor/diagnostics.rs"] +mod diagnostics; + pub(crate) use actions::*; +use diagnostics::{ActiveDiagnostic, GlobalDiagnosticRenderer, InlineDiagnostic}; +pub use diagnostics::{DiagnosticRenderer, set_diagnostic_renderer}; pub use display_map::{ ChunkRenderer, ChunkRendererContext, DisplayPoint, FoldPlaceholder, HighlightKey, NavigationOverlayKey, SemanticTokenHighlight, @@ -390,48 +395,6 @@ pub fn set_blame_renderer(renderer: impl BlameRenderer + 'static, cx: &mut App) cx.set_global(GlobalBlameRenderer(Arc::new(renderer))); } -pub trait DiagnosticRenderer { - fn render_group( - &self, - diagnostic_group: Vec>, - buffer_id: BufferId, - snapshot: EditorSnapshot, - editor: WeakEntity, - language_registry: Option>, - cx: &mut App, - ) -> Vec>; - - fn render_hover( - &self, - diagnostic_group: Vec>, - range: Range, - buffer_id: BufferId, - language_registry: Option>, - 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))); -} - pub struct SearchWithinRange; trait InvalidationRegion { @@ -678,15 +641,6 @@ enum EditPredictionSettings { }, } -#[derive(Debug, Clone)] -struct InlineDiagnostic { - message: SharedString, - group_id: usize, - is_primary: bool, - start: Point, - severity: lsp::DiagnosticSeverity, -} - pub enum MenuEditPredictionsPolicy { Never, ByProvider, @@ -1771,22 +1725,6 @@ struct RegisteredEditPredictionDelegate { _subscription: Subscription, } -#[derive(Debug, PartialEq, Eq)] -pub struct ActiveDiagnosticGroup { - pub active_range: Range, - pub active_message: String, - pub group_id: usize, - pub blocks: HashSet, -} - -#[derive(Debug, PartialEq, Eq)] - -pub(crate) enum ActiveDiagnostic { - None, - All, - Group(ActiveDiagnosticGroup), -} - #[derive(Serialize, Deserialize, Clone, Debug)] pub struct ClipboardSelection { /// The number of bytes in this selection. @@ -4713,7 +4651,7 @@ impl Editor { dismissed = true; } - if self.mode.is_full() && matches!(self.active_diagnostics, ActiveDiagnostic::Group(_)) { + if self.mode.is_full() && self.has_active_diagnostic_group() { self.dismiss_diagnostics(cx); dismissed = true; } @@ -18249,123 +18187,6 @@ impl Editor { ); } - pub fn go_to_diagnostic( - &mut self, - action: &GoToDiagnostic, - window: &mut Window, - cx: &mut Context, - ) { - if !self.diagnostics_enabled() { - return; - } - self.go_to_diagnostic_impl(Direction::Next, action.severity, window, cx) - } - - pub fn go_to_prev_diagnostic( - &mut self, - action: &GoToPreviousDiagnostic, - window: &mut Window, - cx: &mut Context, - ) { - if !self.diagnostics_enabled() { - return; - } - self.go_to_diagnostic_impl(Direction::Prev, action.severity, window, cx) - } - - pub fn go_to_diagnostic_impl( - &mut self, - direction: Direction, - severity: GoToDiagnosticSeverityFilter, - window: &mut Window, - cx: &mut Context, - ) { - let buffer = self.buffer.read(cx).snapshot(cx); - let selection = self - .selections - .newest::(&self.display_snapshot(cx)); - - let mut active_group_id = None; - if let ActiveDiagnostic::Group(active_group) = &self.active_diagnostics - && active_group.active_range.start.to_offset(&buffer) == selection.start - { - active_group_id = Some(active_group.group_id); - } - - fn filtered<'a>( - severity: GoToDiagnosticSeverityFilter, - diagnostics: impl Iterator>, - ) -> impl Iterator> { - diagnostics - .filter(move |entry| severity.matches(entry.diagnostic.severity)) - .filter(|entry| entry.range.start != entry.range.end) - .filter(|entry| !entry.diagnostic.is_unnecessary) - } - - let before = filtered( - severity, - buffer - .diagnostics_in_range(MultiBufferOffset(0)..selection.start) - .filter(|entry| entry.range.start <= selection.start), - ); - let after = filtered( - severity, - buffer - .diagnostics_in_range(selection.start..buffer.len()) - .filter(|entry| entry.range.start >= selection.start), - ); - - let mut found: Option> = None; - if direction == Direction::Prev { - 'outer: for prev_diagnostics in [before.collect::>(), after.collect::>()] - { - for diagnostic in prev_diagnostics.into_iter().rev() { - if diagnostic.range.start != selection.start - || active_group_id - .is_some_and(|active| diagnostic.diagnostic.group_id < active) - { - found = Some(diagnostic); - break 'outer; - } - } - } - } else { - for diagnostic in after.chain(before) { - if diagnostic.range.start != selection.start - || active_group_id.is_some_and(|active| diagnostic.diagnostic.group_id > active) - { - found = Some(diagnostic); - break; - } - } - } - let Some(next_diagnostic) = found else { - return; - }; - - let next_diagnostic_start = buffer.anchor_after(next_diagnostic.range.start); - let Some((buffer_anchor, _)) = buffer.anchor_to_buffer_anchor(next_diagnostic_start) else { - return; - }; - let buffer_id = buffer_anchor.buffer_id; - let snapshot = self.snapshot(window, cx); - if snapshot.intersects_fold(next_diagnostic.range.start) { - self.unfold_ranges( - std::slice::from_ref(&next_diagnostic.range), - true, - false, - cx, - ); - } - self.change_selections(Default::default(), window, cx, |s| { - s.select_ranges(vec![ - next_diagnostic.range.start..next_diagnostic.range.start, - ]) - }); - self.activate_diagnostics(buffer_id, next_diagnostic, window, cx); - self.refresh_edit_prediction(false, true, window, cx); - } - pub fn go_to_next_hunk(&mut self, _: &GoToHunk, window: &mut Window, cx: &mut Context) { let snapshot = self.snapshot(window, cx); let selection = self.selections.newest::(&self.display_snapshot(cx)); @@ -20303,183 +20124,10 @@ impl Editor { window.show_character_palette(); } - fn refresh_active_diagnostics(&mut self, cx: &mut Context) { - if !self.diagnostics_enabled() { - return; - } - - if let ActiveDiagnostic::Group(active_diagnostics) = &mut self.active_diagnostics { - let buffer = self.buffer.read(cx).snapshot(cx); - let primary_range_start = active_diagnostics.active_range.start.to_offset(&buffer); - let primary_range_end = active_diagnostics.active_range.end.to_offset(&buffer); - let is_valid = buffer - .diagnostics_in_range::(primary_range_start..primary_range_end) - .any(|entry| { - entry.diagnostic.is_primary - && !entry.range.is_empty() - && entry.range.start == primary_range_start - && entry.diagnostic.message == active_diagnostics.active_message - }); - - if !is_valid { - self.dismiss_diagnostics(cx); - } - } - } - - pub fn active_diagnostic_group(&self) -> Option<&ActiveDiagnosticGroup> { - match &self.active_diagnostics { - ActiveDiagnostic::Group(group) => Some(group), - _ => None, - } - } - - pub fn set_all_diagnostics_active(&mut self, cx: &mut Context) { - if !self.diagnostics_enabled() { - return; - } - self.dismiss_diagnostics(cx); - self.active_diagnostics = ActiveDiagnostic::All; - } - - fn activate_diagnostics( - &mut self, - buffer_id: BufferId, - diagnostic: DiagnosticEntryRef<'_, MultiBufferOffset>, - window: &mut Window, - cx: &mut Context, - ) { - if !self.diagnostics_enabled() || matches!(self.active_diagnostics, ActiveDiagnostic::All) { - return; - } - self.dismiss_diagnostics(cx); - let snapshot = self.snapshot(window, cx); - let buffer = self.buffer.read(cx).snapshot(cx); - let Some(renderer) = GlobalDiagnosticRenderer::global(cx) else { - return; - }; - - let diagnostic_group = buffer - .diagnostic_group(buffer_id, diagnostic.diagnostic.group_id) - .collect::>(); - - let language_registry = self - .project() - .map(|project| project.read(cx).languages().clone()); - - let blocks = renderer.render_group( - diagnostic_group, - buffer_id, - snapshot, - cx.weak_entity(), - language_registry, - cx, - ); - - let blocks = self.display_map.update(cx, |display_map, cx| { - display_map.insert_blocks(blocks, cx).into_iter().collect() - }); - self.active_diagnostics = ActiveDiagnostic::Group(ActiveDiagnosticGroup { - active_range: buffer.anchor_before(diagnostic.range.start) - ..buffer.anchor_after(diagnostic.range.end), - active_message: diagnostic.diagnostic.message.clone(), - group_id: diagnostic.diagnostic.group_id, - blocks, - }); - cx.notify(); - } - - fn dismiss_diagnostics(&mut self, cx: &mut Context) { - if matches!(self.active_diagnostics, ActiveDiagnostic::All) { - return; - }; - - let prev = mem::replace(&mut self.active_diagnostics, ActiveDiagnostic::None); - if let ActiveDiagnostic::Group(group) = prev { - self.display_map.update(cx, |display_map, cx| { - display_map.remove_blocks(group.blocks, cx); - }); - cx.notify(); - } - } - - /// Disable inline diagnostics rendering for this editor. - pub fn disable_inline_diagnostics(&mut self) { - self.inline_diagnostics_enabled = false; - self.inline_diagnostics_update = Task::ready(()); - self.inline_diagnostics.clear(); - } - - pub fn disable_diagnostics(&mut self, cx: &mut Context) { - self.diagnostics_enabled = false; - self.dismiss_diagnostics(cx); - self.inline_diagnostics_update = Task::ready(()); - self.inline_diagnostics.clear(); - } - pub fn disable_word_completions(&mut self) { self.word_completions_enabled = false; } - pub fn diagnostics_enabled(&self) -> bool { - self.diagnostics_enabled && self.lsp_data_enabled() - } - - pub fn inline_diagnostics_enabled(&self) -> bool { - self.inline_diagnostics_enabled && self.diagnostics_enabled() - } - - pub fn show_inline_diagnostics(&self) -> bool { - self.show_inline_diagnostics - } - - pub fn toggle_inline_diagnostics( - &mut self, - _: &ToggleInlineDiagnostics, - window: &mut Window, - cx: &mut Context, - ) { - self.show_inline_diagnostics = !self.show_inline_diagnostics; - self.refresh_inline_diagnostics(false, window, cx); - } - - pub fn set_max_diagnostics_severity(&mut self, severity: DiagnosticSeverity, cx: &mut App) { - self.diagnostics_max_severity = severity; - self.display_map.update(cx, |display_map, _| { - display_map.diagnostics_max_severity = self.diagnostics_max_severity; - }); - } - - pub fn toggle_diagnostics( - &mut self, - _: &ToggleDiagnostics, - window: &mut Window, - cx: &mut Context, - ) { - let diagnostics_enabled = - self.diagnostics_enabled() && self.diagnostics_max_severity != DiagnosticSeverity::Off; - self.diagnostics_enabled = !diagnostics_enabled; - - let new_severity = if self.diagnostics_enabled { - EditorSettings::get_global(cx) - .diagnostics_max_severity - .filter(|severity| severity != &DiagnosticSeverity::Off) - .unwrap_or(DiagnosticSeverity::Hint) - } else { - DiagnosticSeverity::Off - }; - self.set_max_diagnostics_severity(new_severity, cx); - if self.diagnostics_enabled { - self.active_diagnostics = ActiveDiagnostic::None; - self.inline_diagnostics_update = Task::ready(()); - self.inline_diagnostics.clear(); - } else { - self.refresh_inline_diagnostics(false, window, cx); - } - - cx.notify(); - } - pub fn toggle_minimap( &mut self, _: &ToggleMinimap, @@ -20491,135 +20139,6 @@ impl Editor { } } - fn refresh_inline_diagnostics( - &mut self, - debounce: bool, - window: &mut Window, - cx: &mut Context, - ) { - let max_severity = ProjectSettings::get_global(cx) - .diagnostics - .inline - .max_severity - .unwrap_or(self.diagnostics_max_severity); - - if !self.inline_diagnostics_enabled() - || !self.diagnostics_enabled() - || !self.show_inline_diagnostics - || max_severity == DiagnosticSeverity::Off - { - self.inline_diagnostics_update = Task::ready(()); - self.inline_diagnostics.clear(); - return; - } - - let debounce_ms = ProjectSettings::get_global(cx) - .diagnostics - .inline - .update_debounce_ms; - let debounce = if debounce && debounce_ms > 0 { - Some(Duration::from_millis(debounce_ms)) - } else { - None - }; - self.inline_diagnostics_update = cx.spawn_in(window, async move |editor, cx| { - if let Some(debounce) = debounce { - cx.background_executor().timer(debounce).await; - } - let Some(snapshot) = editor.upgrade().map(|editor| { - editor.update(cx, |editor, cx| editor.buffer().read(cx).snapshot(cx)) - }) else { - return; - }; - - let new_inline_diagnostics = cx - .background_spawn(async move { - let mut inline_diagnostics = Vec::<(Anchor, InlineDiagnostic)>::new(); - for diagnostic_entry in - snapshot.diagnostics_in_range(MultiBufferOffset(0)..snapshot.len()) - { - let message = diagnostic_entry - .diagnostic - .message - .split_once('\n') - .map(|(line, _)| line) - .map(SharedString::new) - .unwrap_or_else(|| { - SharedString::new(&*diagnostic_entry.diagnostic.message) - }); - let start_anchor = snapshot.anchor_before(diagnostic_entry.range.start); - let (Ok(i) | Err(i)) = inline_diagnostics - .binary_search_by(|(probe, _)| probe.cmp(&start_anchor, &snapshot)); - inline_diagnostics.insert( - i, - ( - start_anchor, - InlineDiagnostic { - message, - group_id: diagnostic_entry.diagnostic.group_id, - start: diagnostic_entry.range.start.to_point(&snapshot), - is_primary: diagnostic_entry.diagnostic.is_primary, - severity: diagnostic_entry.diagnostic.severity, - }, - ), - ); - } - inline_diagnostics - }) - .await; - - editor - .update(cx, |editor, cx| { - editor.inline_diagnostics = new_inline_diagnostics; - cx.notify(); - }) - .ok(); - }); - } - - fn pull_diagnostics( - &mut self, - buffer_id: BufferId, - _window: &Window, - cx: &mut Context, - ) -> Option<()> { - // `ActiveDiagnostic::All` is a special mode where editor's diagnostics are managed by the external view, - // skip any LSP updates for it. - - if self.active_diagnostics == ActiveDiagnostic::All || !self.diagnostics_enabled() { - return None; - } - let pull_diagnostics_settings = ProjectSettings::get_global(cx) - .diagnostics - .lsp_pull_diagnostics; - if !pull_diagnostics_settings.enabled { - return None; - } - let debounce = Duration::from_millis(pull_diagnostics_settings.debounce_ms); - let project = self.project()?.downgrade(); - let buffer = self.buffer().read(cx).buffer(buffer_id)?; - - self.pull_diagnostics_task = cx.spawn(async move |_, cx| { - cx.background_executor().timer(debounce).await; - if let Ok(task) = project.update(cx, |project, cx| { - project.lsp_store().update(cx, |lsp_store, cx| { - lsp_store.pull_diagnostics_for_buffer(buffer, cx) - }) - }) { - task.await.log_err(); - } - project - .update(cx, |project, cx| { - project.lsp_store().update(cx, |lsp_store, cx| { - lsp_store.pull_document_diagnostics_for_buffer_edit(buffer_id, cx); - }) - }) - .log_err(); - }); - - Some(()) - } - pub fn set_selections_from_remote( &mut self, selections: Vec>, @@ -25098,16 +24617,6 @@ impl Editor { }; } - fn update_diagnostics_state(&mut self, window: &mut Window, cx: &mut Context<'_, Editor>) { - if !self.diagnostics_enabled() { - return; - } - self.refresh_active_diagnostics(cx); - self.refresh_inline_diagnostics(true, window, cx); - self.scrollbar_marker_state.dirty = true; - cx.notify(); - } - pub fn start_temporary_diff_override(&mut self) { self.load_diff_task.take(); self.temporary_diff_override = true; @@ -29637,16 +29146,6 @@ fn edit_prediction_fallback_text(edits: &[(Range, Arc)], cx: &App) } } -pub fn diagnostic_style(severity: lsp::DiagnosticSeverity, colors: &StatusColors) -> Hsla { - match severity { - lsp::DiagnosticSeverity::ERROR => colors.error, - lsp::DiagnosticSeverity::WARNING => colors.warning, - lsp::DiagnosticSeverity::INFORMATION => colors.info, - lsp::DiagnosticSeverity::HINT => colors.hint, - _ => colors.ignored, - } -} - pub fn styled_runs_for_code_label<'a>( label: &'a CodeLabel, syntax_theme: &'a theme::SyntaxTheme, diff --git a/crates/editor/src/editor/diagnostics.rs b/crates/editor/src/editor/diagnostics.rs new file mode 100644 index 00000000000..b13b4b699f5 --- /dev/null +++ b/crates/editor/src/editor/diagnostics.rs @@ -0,0 +1,519 @@ +use super::*; + +pub trait DiagnosticRenderer { + fn render_group( + &self, + diagnostic_group: Vec>, + buffer_id: BufferId, + snapshot: EditorSnapshot, + editor: WeakEntity, + language_registry: Option>, + cx: &mut App, + ) -> Vec>; + + fn render_hover( + &self, + diagnostic_group: Vec>, + range: Range, + buffer_id: BufferId, + language_registry: Option>, + cx: &mut App, + ) -> Option>; + + fn open_link( + &self, + editor: &mut Editor, + link: SharedString, + window: &mut Window, + cx: &mut Context, + ); +} + +pub fn set_diagnostic_renderer(renderer: impl DiagnosticRenderer + 'static, cx: &mut App) { + cx.set_global(GlobalDiagnosticRenderer(Arc::new(renderer))); +} + +pub(super) struct GlobalDiagnosticRenderer(Arc); + +impl GlobalDiagnosticRenderer { + pub(super) fn global(cx: &App) -> Option> { + cx.try_global::().map(|g| g.0.clone()) + } +} + +impl gpui::Global for GlobalDiagnosticRenderer {} + +#[derive(Debug, Clone)] +pub(super) struct InlineDiagnostic { + pub(super) message: SharedString, + pub(super) group_id: usize, + pub(super) is_primary: bool, + pub(super) start: Point, + pub(super) severity: lsp::DiagnosticSeverity, +} + +#[derive(Debug, PartialEq, Eq)] +pub(super) struct ActiveDiagnosticGroup { + active_range: Range, + active_message: String, + group_id: usize, + blocks: HashSet, +} + +#[derive(Debug, PartialEq, Eq)] +pub(super) enum ActiveDiagnostic { + None, + All, + Group(ActiveDiagnosticGroup), +} + +impl Editor { + pub fn go_to_diagnostic( + &mut self, + action: &GoToDiagnostic, + window: &mut Window, + cx: &mut Context, + ) { + if !self.diagnostics_enabled() { + return; + } + self.go_to_diagnostic_impl(Direction::Next, action.severity, window, cx) + } + + pub fn go_to_prev_diagnostic( + &mut self, + action: &GoToPreviousDiagnostic, + window: &mut Window, + cx: &mut Context, + ) { + if !self.diagnostics_enabled() { + return; + } + self.go_to_diagnostic_impl(Direction::Prev, action.severity, window, cx) + } + + pub fn go_to_diagnostic_impl( + &mut self, + direction: Direction, + severity: GoToDiagnosticSeverityFilter, + window: &mut Window, + cx: &mut Context, + ) { + let buffer = self.buffer.read(cx).snapshot(cx); + let selection = self + .selections + .newest::(&self.display_snapshot(cx)); + + let mut active_group_id = None; + if let ActiveDiagnostic::Group(active_group) = &self.active_diagnostics + && active_group.active_range.start.to_offset(&buffer) == selection.start + { + active_group_id = Some(active_group.group_id); + } + + fn filtered<'a>( + severity: GoToDiagnosticSeverityFilter, + diagnostics: impl Iterator>, + ) -> impl Iterator> { + diagnostics + .filter(move |entry| severity.matches(entry.diagnostic.severity)) + .filter(|entry| entry.range.start != entry.range.end) + .filter(|entry| !entry.diagnostic.is_unnecessary) + } + + let before = filtered( + severity, + buffer + .diagnostics_in_range(MultiBufferOffset(0)..selection.start) + .filter(|entry| entry.range.start <= selection.start), + ); + let after = filtered( + severity, + buffer + .diagnostics_in_range(selection.start..buffer.len()) + .filter(|entry| entry.range.start >= selection.start), + ); + + let mut found: Option> = None; + if direction == Direction::Prev { + 'outer: for prev_diagnostics in [before.collect::>(), after.collect::>()] + { + for diagnostic in prev_diagnostics.into_iter().rev() { + if diagnostic.range.start != selection.start + || active_group_id + .is_some_and(|active| diagnostic.diagnostic.group_id < active) + { + found = Some(diagnostic); + break 'outer; + } + } + } + } else { + for diagnostic in after.chain(before) { + if diagnostic.range.start != selection.start + || active_group_id.is_some_and(|active| diagnostic.diagnostic.group_id > active) + { + found = Some(diagnostic); + break; + } + } + } + let Some(next_diagnostic) = found else { + return; + }; + + let next_diagnostic_start = buffer.anchor_after(next_diagnostic.range.start); + let Some((buffer_anchor, _)) = buffer.anchor_to_buffer_anchor(next_diagnostic_start) else { + return; + }; + let buffer_id = buffer_anchor.buffer_id; + let snapshot = self.snapshot(window, cx); + if snapshot.intersects_fold(next_diagnostic.range.start) { + self.unfold_ranges( + std::slice::from_ref(&next_diagnostic.range), + true, + false, + cx, + ); + } + self.change_selections(Default::default(), window, cx, |s| { + s.select_ranges(vec![ + next_diagnostic.range.start..next_diagnostic.range.start, + ]) + }); + self.activate_diagnostics(buffer_id, next_diagnostic, window, cx); + self.refresh_edit_prediction(false, true, window, cx); + } + + #[cfg(any(test, feature = "test-support"))] + pub fn active_diagnostic_message(&self) -> Option<&str> { + match &self.active_diagnostics { + ActiveDiagnostic::Group(group) => Some(group.active_message.as_str()), + _ => None, + } + } + + pub fn set_all_diagnostics_active(&mut self, cx: &mut Context) { + if !self.diagnostics_enabled() { + return; + } + self.dismiss_diagnostics(cx); + self.active_diagnostics = ActiveDiagnostic::All; + } + + /// Disable inline diagnostics rendering for this editor. + pub fn disable_inline_diagnostics(&mut self) { + self.inline_diagnostics_enabled = false; + self.inline_diagnostics_update = Task::ready(()); + self.inline_diagnostics.clear(); + } + + pub fn disable_diagnostics(&mut self, cx: &mut Context) { + self.diagnostics_enabled = false; + self.dismiss_diagnostics(cx); + self.inline_diagnostics_update = Task::ready(()); + self.inline_diagnostics.clear(); + } + + pub fn diagnostics_enabled(&self) -> bool { + self.diagnostics_enabled && self.lsp_data_enabled() + } + + pub fn inline_diagnostics_enabled(&self) -> bool { + self.inline_diagnostics_enabled && self.diagnostics_enabled() + } + + pub fn show_inline_diagnostics(&self) -> bool { + self.show_inline_diagnostics + } + + pub fn toggle_inline_diagnostics( + &mut self, + _: &ToggleInlineDiagnostics, + window: &mut Window, + cx: &mut Context, + ) { + self.show_inline_diagnostics = !self.show_inline_diagnostics; + self.refresh_inline_diagnostics(false, window, cx); + } + + pub fn set_max_diagnostics_severity(&mut self, severity: DiagnosticSeverity, cx: &mut App) { + self.diagnostics_max_severity = severity; + self.display_map.update(cx, |display_map, _| { + display_map.diagnostics_max_severity = self.diagnostics_max_severity; + }); + } + + pub fn toggle_diagnostics( + &mut self, + _: &ToggleDiagnostics, + window: &mut Window, + cx: &mut Context, + ) { + let diagnostics_enabled = + self.diagnostics_enabled() && self.diagnostics_max_severity != DiagnosticSeverity::Off; + self.diagnostics_enabled = !diagnostics_enabled; + + let new_severity = if self.diagnostics_enabled { + EditorSettings::get_global(cx) + .diagnostics_max_severity + .filter(|severity| severity != &DiagnosticSeverity::Off) + .unwrap_or(DiagnosticSeverity::Hint) + } else { + DiagnosticSeverity::Off + }; + self.set_max_diagnostics_severity(new_severity, cx); + if self.diagnostics_enabled { + self.active_diagnostics = ActiveDiagnostic::None; + self.inline_diagnostics_update = Task::ready(()); + self.inline_diagnostics.clear(); + } else { + self.refresh_inline_diagnostics(false, window, cx); + } + + cx.notify(); + } + + pub(super) fn all_diagnostics_active(&self) -> bool { + self.active_diagnostics == ActiveDiagnostic::All + } + + pub(super) fn active_diagnostic_group_id(&self) -> Option { + match &self.active_diagnostics { + ActiveDiagnostic::Group(group) => Some(group.group_id), + _ => None, + } + } + + pub(super) fn has_active_diagnostic_group(&self) -> bool { + matches!(self.active_diagnostics, ActiveDiagnostic::Group(_)) + } + + pub(super) fn refresh_active_diagnostics(&mut self, cx: &mut Context) { + if !self.diagnostics_enabled() { + return; + } + + if let ActiveDiagnostic::Group(active_diagnostics) = &mut self.active_diagnostics { + let buffer = self.buffer.read(cx).snapshot(cx); + let primary_range_start = active_diagnostics.active_range.start.to_offset(&buffer); + let primary_range_end = active_diagnostics.active_range.end.to_offset(&buffer); + let is_valid = buffer + .diagnostics_in_range::(primary_range_start..primary_range_end) + .any(|entry| { + entry.diagnostic.is_primary + && !entry.range.is_empty() + && entry.range.start == primary_range_start + && entry.diagnostic.message == active_diagnostics.active_message + }); + + if !is_valid { + self.dismiss_diagnostics(cx); + } + } + } + + pub(super) fn activate_diagnostics( + &mut self, + buffer_id: BufferId, + diagnostic: DiagnosticEntryRef<'_, MultiBufferOffset>, + window: &mut Window, + cx: &mut Context, + ) { + if !self.diagnostics_enabled() || matches!(self.active_diagnostics, ActiveDiagnostic::All) { + return; + } + self.dismiss_diagnostics(cx); + let snapshot = self.snapshot(window, cx); + let buffer = self.buffer.read(cx).snapshot(cx); + let Some(renderer) = GlobalDiagnosticRenderer::global(cx) else { + return; + }; + + let diagnostic_group = buffer + .diagnostic_group(buffer_id, diagnostic.diagnostic.group_id) + .collect::>(); + + let language_registry = self + .project() + .map(|project| project.read(cx).languages().clone()); + + let blocks = renderer.render_group( + diagnostic_group, + buffer_id, + snapshot, + cx.weak_entity(), + language_registry, + cx, + ); + + let blocks = self.display_map.update(cx, |display_map, cx| { + display_map.insert_blocks(blocks, cx).into_iter().collect() + }); + self.active_diagnostics = ActiveDiagnostic::Group(ActiveDiagnosticGroup { + active_range: buffer.anchor_before(diagnostic.range.start) + ..buffer.anchor_after(diagnostic.range.end), + active_message: diagnostic.diagnostic.message.clone(), + group_id: diagnostic.diagnostic.group_id, + blocks, + }); + cx.notify(); + } + + pub(super) fn dismiss_diagnostics(&mut self, cx: &mut Context) { + if matches!(self.active_diagnostics, ActiveDiagnostic::All) { + return; + }; + + let prev = mem::replace(&mut self.active_diagnostics, ActiveDiagnostic::None); + if let ActiveDiagnostic::Group(group) = prev { + self.display_map.update(cx, |display_map, cx| { + display_map.remove_blocks(group.blocks, cx); + }); + cx.notify(); + } + } + + pub(super) fn refresh_inline_diagnostics( + &mut self, + debounce: bool, + window: &mut Window, + cx: &mut Context, + ) { + let max_severity = ProjectSettings::get_global(cx) + .diagnostics + .inline + .max_severity + .unwrap_or(self.diagnostics_max_severity); + + if !self.inline_diagnostics_enabled() + || !self.diagnostics_enabled() + || !self.show_inline_diagnostics + || max_severity == DiagnosticSeverity::Off + { + self.inline_diagnostics_update = Task::ready(()); + self.inline_diagnostics.clear(); + return; + } + + let debounce_ms = ProjectSettings::get_global(cx) + .diagnostics + .inline + .update_debounce_ms; + let debounce = if debounce && debounce_ms > 0 { + Some(Duration::from_millis(debounce_ms)) + } else { + None + }; + self.inline_diagnostics_update = cx.spawn_in(window, async move |editor, cx| { + if let Some(debounce) = debounce { + cx.background_executor().timer(debounce).await; + } + let Some(snapshot) = editor.upgrade().map(|editor| { + editor.update(cx, |editor, cx| editor.buffer().read(cx).snapshot(cx)) + }) else { + return; + }; + + let new_inline_diagnostics = cx + .background_spawn(async move { + let mut inline_diagnostics = Vec::<(Anchor, InlineDiagnostic)>::new(); + for diagnostic_entry in + snapshot.diagnostics_in_range(MultiBufferOffset(0)..snapshot.len()) + { + let message = diagnostic_entry + .diagnostic + .message + .split_once('\n') + .map(|(line, _)| line) + .map(SharedString::new) + .unwrap_or_else(|| { + SharedString::new(&*diagnostic_entry.diagnostic.message) + }); + let start_anchor = snapshot.anchor_before(diagnostic_entry.range.start); + let (Ok(i) | Err(i)) = inline_diagnostics + .binary_search_by(|(probe, _)| probe.cmp(&start_anchor, &snapshot)); + inline_diagnostics.insert( + i, + ( + start_anchor, + InlineDiagnostic { + message, + group_id: diagnostic_entry.diagnostic.group_id, + start: diagnostic_entry.range.start.to_point(&snapshot), + is_primary: diagnostic_entry.diagnostic.is_primary, + severity: diagnostic_entry.diagnostic.severity, + }, + ), + ); + } + inline_diagnostics + }) + .await; + + editor + .update(cx, |editor, cx| { + editor.inline_diagnostics = new_inline_diagnostics; + cx.notify(); + }) + .ok(); + }); + } + + pub(super) fn pull_diagnostics( + &mut self, + buffer_id: BufferId, + _window: &Window, + cx: &mut Context, + ) -> Option<()> { + // `ActiveDiagnostic::All` is a special mode where editor's diagnostics are managed by the external view, + // skip any LSP updates for it. + + if self.active_diagnostics == ActiveDiagnostic::All || !self.diagnostics_enabled() { + return None; + } + let pull_diagnostics_settings = ProjectSettings::get_global(cx) + .diagnostics + .lsp_pull_diagnostics; + if !pull_diagnostics_settings.enabled { + return None; + } + let debounce = Duration::from_millis(pull_diagnostics_settings.debounce_ms); + let project = self.project()?.downgrade(); + let buffer = self.buffer().read(cx).buffer(buffer_id)?; + + self.pull_diagnostics_task = cx.spawn(async move |_, cx| { + cx.background_executor().timer(debounce).await; + if let Ok(task) = project.update(cx, |project, cx| { + project.lsp_store().update(cx, |lsp_store, cx| { + lsp_store.pull_diagnostics_for_buffer(buffer, cx) + }) + }) { + task.await.log_err(); + } + project + .update(cx, |project, cx| { + project.lsp_store().update(cx, |lsp_store, cx| { + lsp_store.pull_document_diagnostics_for_buffer_edit(buffer_id, cx); + }) + }) + .log_err(); + }); + + Some(()) + } + + pub(super) fn update_diagnostics_state( + &mut self, + window: &mut Window, + cx: &mut Context<'_, Editor>, + ) { + if !self.diagnostics_enabled() { + return; + } + self.refresh_active_diagnostics(cx); + self.refresh_inline_diagnostics(true, window, cx); + self.scrollbar_marker_state.dirty = true; + cx.notify(); + } +} diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 9db33bb9ba7..7e751535910 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1,11 +1,11 @@ use crate::{ - ActiveDiagnostic, BUFFER_HEADER_PADDING, BlockId, CURSORS_VISIBLE_FOR, ChunkRendererContext, - ChunkReplacement, CodeActionSource, ColumnarMode, ConflictsOurs, ConflictsOursMarker, - ConflictsOuter, ConflictsTheirs, ConflictsTheirsMarker, ContextMenuPlacement, CursorShape, - CustomBlockId, DisplayDiffHunk, DisplayPoint, DisplayRow, EditDisplayMode, EditPrediction, - Editor, EditorMode, EditorSettings, EditorSnapshot, EditorStyle, FILE_HEADER_HEIGHT, - FocusedBlock, GutterDimensions, GutterHoverButton, HalfPageDown, HalfPageUp, HandleInput, - HoveredCursor, InlayHintRefreshReason, JumpData, LineDown, LineHighlight, LineUp, MAX_LINE_LEN, + BUFFER_HEADER_PADDING, BlockId, CURSORS_VISIBLE_FOR, ChunkRendererContext, ChunkReplacement, + CodeActionSource, ColumnarMode, ConflictsOurs, ConflictsOursMarker, ConflictsOuter, + ConflictsTheirs, ConflictsTheirsMarker, ContextMenuPlacement, CursorShape, CustomBlockId, + DisplayDiffHunk, DisplayPoint, DisplayRow, EditDisplayMode, EditPrediction, Editor, EditorMode, + EditorSettings, EditorSnapshot, EditorStyle, FILE_HEADER_HEIGHT, FocusedBlock, + GutterDimensions, GutterHoverButton, HalfPageDown, HalfPageUp, HandleInput, HoveredCursor, + InlayHintRefreshReason, JumpData, LineDown, LineHighlight, LineUp, MAX_LINE_LEN, MINIMAP_FONT_SIZE, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, OpenExcerpts, PageDown, PageUp, PhantomDiffReviewIndicator, Point, RowExt, RowRangeExt, SelectPhase, Selection, SelectionDragState, SelectionEffects, SizingBehavior, SoftWrap, StickyHeaderExcerpt, ToPoint, @@ -2498,12 +2498,7 @@ impl EditorElement { None => return HashMap::default(), }; - let active_diagnostics_group = - if let ActiveDiagnostic::Group(group) = &self.editor.read(cx).active_diagnostics { - Some(group.group_id) - } else { - None - }; + let active_diagnostics_group = self.editor.read(cx).active_diagnostic_group_id(); let diagnostics_by_rows = self.editor.update(cx, |editor, cx| { let snapshot = editor.snapshot(window, cx); diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index e43ae09c0d6..cfa7284127e 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, GlobalDiagnosticRenderer, HighlightKey, Hover, + Anchor, AnchorRangeExt, DisplayPoint, DisplayRow, Editor, EditorSettings, EditorSnapshot, + GlobalDiagnosticRenderer, HighlightKey, Hover, display_map::{InlayOffset, ToDisplayPoint, is_invisible}, editor_settings::EditorSettingsScrollbarProxy, hover_links::{InlayHighlight, RangeInEditor}, @@ -319,12 +319,8 @@ fn show_hover( } let hover_popover_delay = EditorSettings::get_global(cx).hover_popover_delay.0; - let all_diagnostics_active = editor.active_diagnostics == ActiveDiagnostic::All; - let active_group_id = if let ActiveDiagnostic::Group(group) = &editor.active_diagnostics { - Some(group.group_id) - } else { - None - }; + let all_diagnostics_active = editor.all_diagnostics_active(); + let active_group_id = editor.active_diagnostic_group_id(); let renderer = GlobalDiagnosticRenderer::global(cx); let task = cx.spawn_in(window, async move |this, cx| {