From 5ae93ce68dfdf51406e7335f5c177dc4ee25b2ff Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 21 Feb 2025 01:39:47 +0200 Subject: [PATCH] Add initial inline diagnostics support (#25297) https://github.com/user-attachments/assets/eb881707-e575-47ef-9ae0-67d8085d8065 Closes https://github.com/zed-industries/zed/pull/22668 Closes https://github.com/zed-industries/zed/issues/4901 Takes https://github.com/zed-industries/zed/pull/22668 and fixes all review items on top. Inline diagnostics are disabled by default, but can be enabled via settings permanently, or temporarily toggled with the `editor: ToggleInlineDiagnostics` action and the corresponding editor menu item image Inline diagnostics does not show currently active diagnostics group, as it gets inline into the editor too, inside the text. Inline git blame takes precedence and is shown instead of the diagnostics, edit predictions dim the diagnostics if located on the same line. One notable drawback of the implementation is the inability to wrap, making inline diagnostics cut off the right side: ![image](https://github.com/user-attachments/assets/6e87268a-b51a-4a2b-8b8d-01d932c62fea) (same as inline git blame and other elements to the right of the text) Given that it's disabled by default and go to next/prev diagnostics will show them better, seems fine to leave in the first iteration. Release Notes: - Added initial inline diagnostics support --------- Co-authored-by: Paul J. Davis Co-authored-by: Danilo Leal --- Cargo.lock | 1 - assets/settings/default.json | 20 +- crates/diagnostics/Cargo.toml | 1 - crates/diagnostics/src/diagnostics.rs | 8 +- .../src/project_diagnostics_settings.rs | 28 -- crates/editor/src/actions.rs | 1 + crates/editor/src/editor.rs | 124 +++++++++ crates/editor/src/element.rs | 261 +++++++++++++++--- crates/project/src/project_settings.rs | 81 +++++- crates/zed/src/zed/quick_action_bar.rs | 29 ++ docs/src/configuring-zed.md | 106 +++++++ 11 files changed, 579 insertions(+), 81 deletions(-) delete mode 100644 crates/diagnostics/src/project_diagnostics_settings.rs diff --git a/Cargo.lock b/Cargo.lock index a4aacdbeb290ef..85907443ea35f8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3851,7 +3851,6 @@ dependencies = [ "pretty_assertions", "project", "rand 0.8.5", - "schemars", "serde", "serde_json", "settings", diff --git a/assets/settings/default.json b/assets/settings/default.json index 2f4b2646399f3f..d8c60e89848ec5 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -760,7 +760,25 @@ // Diagnostics configuration. "diagnostics": { // Whether to show warnings or not by default. - "include_warnings": true + "include_warnings": true, + // Settings for inline diagnostics + "inline": { + // Whether to show diagnostics inline or not + "enabled": false, + // The delay in milliseconds to show inline diagnostics after the + // last diagnostic update. + "update_debounce_ms": 150, + // The amount of padding between the end of the source line and the start + // of the inline diagnostic in units of em widths. + "padding": 4, + // The minimum column to display inline diagnostics. This setting can be + // used to horizontally align inline diagnostics at some column. Lines + // longer than this value will still push diagnostics further to the right. + "min_column": 0, + // The minimum severity of the diagnostics to show inline. + // Shows all diagnostics when not specified. + "max_severity": null + } }, // Files or globs of files that will be excluded by Zed entirely. They will be skipped during file // scans, file searches, and not be displayed in the project file tree. Takes precedence over `file_scan_inclusions`. diff --git a/crates/diagnostics/Cargo.toml b/crates/diagnostics/Cargo.toml index 7888c405793c81..cb815324438edf 100644 --- a/crates/diagnostics/Cargo.toml +++ b/crates/diagnostics/Cargo.toml @@ -24,7 +24,6 @@ log.workspace = true lsp.workspace = true project.workspace = true rand.workspace = true -schemars.workspace = true serde.workspace = true settings.workspace = true theme.workspace = true diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 6f936f2c8c4536..c83c9df735bc70 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -1,5 +1,4 @@ pub mod items; -mod project_diagnostics_settings; mod toolbar_controls; #[cfg(test)] @@ -24,8 +23,7 @@ use language::{ Point, Selection, SelectionGoal, ToTreeSitterPoint, }; use lsp::LanguageServerId; -use project::{DiagnosticSummary, Project, ProjectPath}; -use project_diagnostics_settings::ProjectDiagnosticsSettings; +use project::{project_settings::ProjectSettings, DiagnosticSummary, Project, ProjectPath}; use settings::Settings; use std::{ any::{Any, TypeId}, @@ -52,7 +50,6 @@ struct IncludeWarnings(bool); impl Global for IncludeWarnings {} pub fn init(cx: &mut App) { - ProjectDiagnosticsSettings::register(cx); cx.observe_new(ProjectDiagnosticsEditor::register).detach(); } @@ -178,6 +175,7 @@ impl ProjectDiagnosticsEditor { cx, ); editor.set_vertical_scroll_margin(5, cx); + editor.disable_inline_diagnostics(); editor }); cx.subscribe_in( @@ -287,7 +285,7 @@ impl ProjectDiagnosticsEditor { let include_warnings = match cx.try_global::() { Some(include_warnings) => include_warnings.0, - None => ProjectDiagnosticsSettings::get_global(cx).include_warnings, + None => ProjectSettings::get_global(cx).diagnostics.include_warnings, }; let diagnostics = cx.new(|cx| { diff --git a/crates/diagnostics/src/project_diagnostics_settings.rs b/crates/diagnostics/src/project_diagnostics_settings.rs deleted file mode 100644 index 50d0949b737c08..00000000000000 --- a/crates/diagnostics/src/project_diagnostics_settings.rs +++ /dev/null @@ -1,28 +0,0 @@ -use anyhow::Result; -use gpui::App; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsSources}; - -#[derive(Deserialize, Debug)] -pub struct ProjectDiagnosticsSettings { - pub include_warnings: bool, -} - -/// Diagnostics configuration. -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)] -pub struct ProjectDiagnosticsSettingsContent { - /// Whether to show warnings or not by default. - /// - /// Default: true - include_warnings: Option, -} - -impl Settings for ProjectDiagnosticsSettings { - const KEY: Option<&'static str> = Some("diagnostics"); - type FileContent = ProjectDiagnosticsSettingsContent; - - fn load(sources: SettingsSources, _: &mut App) -> Result { - sources.json_merge() - } -} diff --git a/crates/editor/src/actions.rs b/crates/editor/src/actions.rs index 71cffc315bd22d..2c61e8521de17f 100644 --- a/crates/editor/src/actions.rs +++ b/crates/editor/src/actions.rs @@ -397,6 +397,7 @@ gpui::actions!( ToggleGitBlameInline, ToggleIndentGuides, ToggleInlayHints, + ToggleInlineDiagnostics, ToggleEditPrediction, ToggleLineNumbers, SwapSelectionEnds, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 2f80d80d267d03..a17831e763a441 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -464,6 +464,15 @@ enum EditPredictionSettings { enum InlineCompletionHighlight {} +#[derive(Debug, Clone)] +struct InlineDiagnostic { + message: SharedString, + group_id: usize, + is_primary: bool, + start: Point, + severity: DiagnosticSeverity, +} + pub enum MenuInlineCompletionsPolicy { Never, ByProvider, @@ -594,6 +603,10 @@ pub struct Editor { select_larger_syntax_node_stack: Vec]>>, ime_transaction: Option, active_diagnostics: Option, + show_inline_diagnostics: bool, + inline_diagnostics_update: Task<()>, + inline_diagnostics_enabled: bool, + inline_diagnostics: Vec<(Anchor, InlineDiagnostic)>, soft_wrap_mode_override: Option, // TODO: make this a access method @@ -1304,6 +1317,9 @@ impl Editor { select_larger_syntax_node_stack: Vec::new(), ime_transaction: Default::default(), active_diagnostics: None, + show_inline_diagnostics: ProjectSettings::get_global(cx).diagnostics.inline.enabled, + inline_diagnostics_update: Task::ready(()), + inline_diagnostics: Vec::new(), soft_wrap_mode_override, completion_provider: project.clone().map(|project| Box::new(project) as _), semantics_provider: project.clone().map(|project| Rc::new(project) as _), @@ -1368,6 +1384,7 @@ impl Editor { active_inline_completion: None, stale_inline_completion_in_menu: None, edit_prediction_preview: EditPredictionPreview::Inactive, + inline_diagnostics_enabled: mode == EditorMode::Full, inlay_hint_cache: InlayHintCache::new(inlay_hint_settings), gutter_hovered: false, @@ -11868,6 +11885,106 @@ impl Editor { } } + /// 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 inline_diagnostics_enabled(&self) -> bool { + self.inline_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<'_, Editor>, + ) { + self.show_inline_diagnostics = !self.show_inline_diagnostics; + self.refresh_inline_diagnostics(false, window, cx); + } + + fn refresh_inline_diagnostics( + &mut self, + debounce: bool, + window: &mut Window, + cx: &mut Context, + ) { + if !self.inline_diagnostics_enabled || !self.show_inline_diagnostics { + 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, |editor, mut cx| async move { + if let Some(debounce) = debounce { + cx.background_executor().timer(debounce).await; + } + let Some(snapshot) = editor + .update(&mut cx, |editor, cx| editor.buffer().read(cx).snapshot(cx)) + .ok() + 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(0..snapshot.len()) { + let message = diagnostic_entry + .diagnostic + .message + .split_once('\n') + .map(|(line, _)| line) + .map(SharedString::new) + .unwrap_or_else(|| { + SharedString::from(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(&mut cx, |editor, cx| { + editor.inline_diagnostics = new_inline_diagnostics; + cx.notify(); + }) + .ok(); + }); + } + pub fn set_selections_from_remote( &mut self, selections: Vec>, @@ -14333,6 +14450,7 @@ impl Editor { multi_buffer::Event::Closed => cx.emit(EditorEvent::Closed), multi_buffer::Event::DiagnosticsUpdated => { self.refresh_active_diagnostics(cx); + self.refresh_inline_diagnostics(true, window, cx); self.scrollbar_marker_state.dirty = true; cx.notify(); } @@ -14383,7 +14501,13 @@ impl Editor { self.serialize_dirty_buffers = project_settings.session.restore_unsaved_buffers; if self.mode == EditorMode::Full { + let show_inline_diagnostics = project_settings.diagnostics.inline.enabled; let inline_blame_enabled = project_settings.git.inline_blame_enabled(); + if self.show_inline_diagnostics != show_inline_diagnostics { + self.show_inline_diagnostics = show_inline_diagnostics; + self.refresh_inline_diagnostics(false, window, cx); + } + if self.git_blame_inline_enabled != inline_blame_enabled { self.toggle_git_blame_inline_internal(false, window, cx); } diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 89f830b646341b..a14d2137376f71 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -52,7 +52,7 @@ use multi_buffer::{ Anchor, ExcerptId, ExcerptInfo, ExpandExcerptDirection, MultiBufferPoint, MultiBufferRow, RowInfo, ToOffset, }; -use project::project_settings::{GitGutterSetting, ProjectSettings}; +use project::project_settings::{self, GitGutterSetting, ProjectSettings}; use settings::Settings; use smallvec::{smallvec, SmallVec}; use std::{ @@ -403,6 +403,7 @@ impl EditorElement { register_action(editor, window, Editor::toggle_indent_guides); register_action(editor, window, Editor::toggle_inlay_hints); register_action(editor, window, Editor::toggle_inline_completions); + register_action(editor, window, Editor::toggle_inline_diagnostics); register_action(editor, window, hover_popover::hover); register_action(editor, window, Editor::reveal_in_finder); register_action(editor, window, Editor::copy_path); @@ -1610,6 +1611,157 @@ impl EditorElement { display_hunks } + #[allow(clippy::too_many_arguments)] + fn layout_inline_diagnostics( + &self, + line_layouts: &[LineWithInvisibles], + crease_trailers: &[Option], + content_origin: gpui::Point, + scroll_pixel_position: gpui::Point, + inline_completion_popover_origin: Option>, + start_row: DisplayRow, + end_row: DisplayRow, + line_height: Pixels, + em_width: Pixels, + style: &EditorStyle, + window: &mut Window, + cx: &mut App, + ) -> HashMap { + let max_severity = ProjectSettings::get_global(cx) + .diagnostics + .inline + .max_severity + .map_or(DiagnosticSeverity::HINT, |severity| match severity { + project_settings::DiagnosticSeverity::Error => DiagnosticSeverity::ERROR, + project_settings::DiagnosticSeverity::Warning => DiagnosticSeverity::WARNING, + project_settings::DiagnosticSeverity::Info => DiagnosticSeverity::INFORMATION, + project_settings::DiagnosticSeverity::Hint => DiagnosticSeverity::HINT, + }); + + let active_diagnostics_group = self + .editor + .read(cx) + .active_diagnostics + .as_ref() + .map(|active_diagnostics| active_diagnostics.group_id); + + let diagnostics_by_rows = self.editor.update(cx, |editor, cx| { + let snapshot = editor.snapshot(window, cx); + editor + .inline_diagnostics + .iter() + .filter(|(_, diagnostic)| diagnostic.severity <= max_severity) + .filter(|(_, diagnostic)| match active_diagnostics_group { + Some(active_diagnostics_group) => { + // Active diagnostics are all shown in the editor already, no need to display them inline + diagnostic.group_id != active_diagnostics_group + } + None => true, + }) + .map(|(point, diag)| (point.to_display_point(&snapshot), diag.clone())) + .skip_while(|(point, _)| point.row() < start_row) + .take_while(|(point, _)| point.row() < end_row) + .fold(HashMap::default(), |mut acc, (point, diagnostic)| { + acc.entry(point.row()) + .or_insert_with(Vec::new) + .push(diagnostic); + acc + }) + }); + + if diagnostics_by_rows.is_empty() { + return HashMap::default(); + } + + let severity_to_color = |sev: &DiagnosticSeverity| match sev { + &DiagnosticSeverity::ERROR => Color::Error, + &DiagnosticSeverity::WARNING => Color::Warning, + &DiagnosticSeverity::INFORMATION => Color::Info, + &DiagnosticSeverity::HINT => Color::Hint, + _ => Color::Error, + }; + + let padding = ProjectSettings::get_global(cx).diagnostics.inline.padding as f32 * em_width; + let min_x = ProjectSettings::get_global(cx) + .diagnostics + .inline + .min_column as f32 + * em_width; + + let mut elements = HashMap::default(); + for (row, mut diagnostics) in diagnostics_by_rows { + diagnostics.sort_by_key(|diagnostic| { + ( + diagnostic.severity, + std::cmp::Reverse(diagnostic.is_primary), + diagnostic.start.row, + diagnostic.start.column, + ) + }); + + let Some(diagnostic_to_render) = diagnostics + .iter() + .find(|diagnostic| diagnostic.is_primary) + .or_else(|| diagnostics.first()) + else { + continue; + }; + + let pos_y = content_origin.y + + line_height * (row.0 as f32 - scroll_pixel_position.y / line_height); + + let window_ix = row.minus(start_row) as usize; + let pos_x = { + let crease_trailer_layout = &crease_trailers[window_ix]; + let line_layout = &line_layouts[window_ix]; + + let line_end = if let Some(crease_trailer) = crease_trailer_layout { + crease_trailer.bounds.right() + } else { + content_origin.x - scroll_pixel_position.x + line_layout.width + }; + + let padded_line = line_end + padding; + let min_start = content_origin.x - scroll_pixel_position.x + min_x; + + cmp::max(padded_line, min_start) + }; + + let behind_inline_completion_popover = inline_completion_popover_origin + .as_ref() + .map_or(false, |inline_completion_popover_origin| { + (pos_y..pos_y + line_height).contains(&inline_completion_popover_origin.y) + }); + let opacity = if behind_inline_completion_popover { + 0.5 + } else { + 1.0 + }; + + let mut element = h_flex() + .id(("diagnostic", row.0)) + .h(line_height) + .w_full() + .px_1() + .rounded_sm() + .opacity(opacity) + .bg(severity_to_color(&diagnostic_to_render.severity) + .color(cx) + .opacity(0.05)) + .text_color(severity_to_color(&diagnostic_to_render.severity).color(cx)) + .text_sm() + .font_family(style.text.font().family) + .child(diagnostic_to_render.message.clone()) + .into_any(); + + element.prepaint_as_root(point(pos_x, pos_y), AvailableSpace::min_size(), window, cx); + + elements.insert(row, element); + } + + elements + } + #[allow(clippy::too_many_arguments)] fn layout_inline_blame( &self, @@ -3573,7 +3725,7 @@ impl EditorElement { style: &EditorStyle, window: &mut Window, cx: &mut App, - ) -> Option { + ) -> Option<(AnyElement, gpui::Point)> { const PADDING_X: Pixels = Pixels(24.); const PADDING_Y: Pixels = Pixels(2.); @@ -3626,7 +3778,7 @@ impl EditorElement { let origin = start_point + point(cursor_character_x, PADDING_Y); element.prepaint_at(origin, window, cx); - return Some(element); + return Some((element, origin)); } else if target_display_point.row() >= visible_row_range.end { let mut element = editor .render_edit_prediction_line_popover( @@ -3654,7 +3806,7 @@ impl EditorElement { ); element.prepaint_at(origin, window, cx); - return Some(element); + return Some((element, origin)); } else { const POLE_WIDTH: Pixels = px(2.); @@ -3694,7 +3846,7 @@ impl EditorElement { element.prepaint_at(origin, window, cx); - return Some(element); + return Some((element, origin)); } } @@ -3711,8 +3863,9 @@ impl EditorElement { let size = element.layout_as_root(AvailableSpace::min_size(), window, cx); let offset = point((text_bounds.size.width - size.width) / 2., PADDING_Y); - element.prepaint_at(text_bounds.origin + offset, window, cx); - Some(element) + let origin = text_bounds.origin + offset; + element.prepaint_at(origin, window, cx); + Some((element, origin)) } else if (target_display_point.row().as_f32() + 1.) > scroll_bottom { let mut element = editor .render_edit_prediction_line_popover( @@ -3729,8 +3882,9 @@ impl EditorElement { text_bounds.size.height - size.height - PADDING_Y, ); - element.prepaint_at(text_bounds.origin + offset, window, cx); - Some(element) + let origin = text_bounds.origin + offset; + element.prepaint_at(origin, window, cx); + Some((element, origin)) } else { let mut element = editor .render_edit_prediction_line_popover("Jump to Edit", None, window, cx)? @@ -3743,13 +3897,9 @@ impl EditorElement { editor.display_to_pixel_point(target_line_end, editor_snapshot, window) })?; - element.prepaint_as_root( - clamp_start(start_point + origin + point(PADDING_X, px(0.))), - AvailableSpace::min_size(), - window, - cx, - ); - Some(element) + let origin = clamp_start(start_point + origin + point(PADDING_X, px(0.))); + element.prepaint_as_root(origin, AvailableSpace::min_size(), window, cx); + Some((element, origin)) } } InlineCompletion::Edit { @@ -3805,13 +3955,9 @@ impl EditorElement { )) })?; - element.prepaint_as_root( - clamp_start(start_point + origin + point(PADDING_X, px(0.))), - AvailableSpace::min_size(), - window, - cx, - ); - return Some(element); + let origin = clamp_start(start_point + origin + point(PADDING_X, px(0.))); + element.prepaint_as_root(origin, AvailableSpace::min_size(), window, cx); + return Some((element, origin)); } EditDisplayMode::Inline => return None, EditDisplayMode::DiffPopover => {} @@ -4832,6 +4978,7 @@ impl EditorElement { self.paint_lines(&invisible_display_ranges, layout, window, cx); self.paint_redactions(layout, window); self.paint_cursors(layout, window, cx); + self.paint_inline_diagnostics(layout, window, cx); self.paint_inline_blame(layout, window, cx); self.paint_diff_hunk_controls(layout, window, cx); window.with_element_namespace("crease_trailers", |window| { @@ -5507,6 +5654,17 @@ impl EditorElement { } } + fn paint_inline_diagnostics( + &mut self, + layout: &mut EditorLayout, + window: &mut Window, + cx: &mut App, + ) { + for mut inline_diagnostic in layout.inline_diagnostics.drain() { + inline_diagnostic.1.paint(window, cx); + } + } + fn paint_inline_blame(&mut self, layout: &mut EditorLayout, window: &mut Window, cx: &mut App) { if let Some(mut inline_blame) = layout.inline_blame.take() { window.paint_layer(layout.position_map.text_hitbox.bounds, |window| { @@ -7221,6 +7379,40 @@ impl Element for EditorElement { ) }); + let (inline_completion_popover, inline_completion_popover_origin) = self + .layout_edit_prediction_popover( + &text_hitbox.bounds, + content_origin, + &snapshot, + start_row..end_row, + scroll_position.y, + scroll_position.y + height_in_lines, + &line_layouts, + line_height, + scroll_pixel_position, + newest_selection_head, + editor_width, + &style, + window, + cx, + ) + .unzip(); + + let mut inline_diagnostics = self.layout_inline_diagnostics( + &line_layouts, + &crease_trailers, + content_origin, + scroll_pixel_position, + inline_completion_popover_origin, + start_row, + end_row, + line_height, + em_width, + &style, + window, + cx, + ); + let mut inline_blame = None; if let Some(newest_selection_head) = newest_selection_head { let display_row = newest_selection_head.row(); @@ -7241,6 +7433,10 @@ impl Element for EditorElement { window, cx, ); + if inline_blame.is_some() { + // Blame overrides inline diagnostics + inline_diagnostics.remove(&display_row); + } } } @@ -7477,23 +7673,6 @@ impl Element for EditorElement { ); } - let inline_completion_popover = self.layout_edit_prediction_popover( - &text_hitbox.bounds, - content_origin, - &snapshot, - start_row..end_row, - scroll_position.y, - scroll_position.y + height_in_lines, - &line_layouts, - line_height, - scroll_pixel_position, - newest_selection_head, - editor_width, - &style, - window, - cx, - ); - let mouse_context_menu = self.layout_mouse_context_menu( &snapshot, start_row..end_row, @@ -7600,6 +7779,7 @@ impl Element for EditorElement { line_elements, line_numbers, blamed_display_rows, + inline_diagnostics, inline_blame, blocks, cursors, @@ -7777,6 +7957,7 @@ pub struct EditorLayout { line_numbers: Arc>, display_hunks: Vec<(DisplayDiffHunk, Option)>, blamed_display_rows: Option>, + inline_diagnostics: HashMap, inline_blame: Option, blocks: Vec, highlighted_ranges: Vec<(Range, Hsla)>, diff --git a/crates/project/src/project_settings.rs b/crates/project/src/project_settings.rs index 43751818368c96..f884ef4780c3a5 100644 --- a/crates/project/src/project_settings.rs +++ b/crates/project/src/project_settings.rs @@ -40,6 +40,10 @@ pub struct ProjectSettings { #[serde(default)] pub lsp: HashMap, + /// Configuration for Diagnostics-related features. + #[serde(default)] + pub diagnostics: DiagnosticsSettings, + /// Configuration for Git-related features #[serde(default)] pub git: GitSettings, @@ -78,6 +82,77 @@ pub enum DirenvSettings { Direct, } +#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] +pub struct DiagnosticsSettings { + /// Whether or not to include warning diagnostics + #[serde(default = "true_value")] + pub include_warnings: bool, + + /// Settings for showing inline diagnostics + #[serde(default)] + pub inline: InlineDiagnosticsSettings, +} + +#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)] +pub struct InlineDiagnosticsSettings { + /// Whether or not to show inline diagnostics + /// + /// Default: false + #[serde(default)] + pub enabled: bool, + /// Whether to only show the inline diaganostics after a delay after the + /// last editor event. + /// + /// Default: 150 + #[serde(default = "default_inline_diagnostics_debounce_ms")] + pub update_debounce_ms: u64, + /// The amount of padding between the end of the source line and the start + /// of the inline diagnostic in units of columns. + /// + /// Default: 4 + #[serde(default = "default_inline_diagnostics_padding")] + pub padding: u32, + /// The minimum column to display inline diagnostics. This setting can be + /// used to horizontally align inline diagnostics at some position. Lines + /// longer than this value will still push diagnostics further to the right. + /// + /// Default: 0 + #[serde(default)] + pub min_column: u32, + + #[serde(default)] + pub max_severity: Option, +} + +#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum DiagnosticSeverity { + Error, + Warning, + Info, + Hint, +} + +impl Default for InlineDiagnosticsSettings { + fn default() -> Self { + Self { + enabled: false, + update_debounce_ms: default_inline_diagnostics_debounce_ms(), + padding: default_inline_diagnostics_padding(), + min_column: 0, + max_severity: None, + } + } +} + +fn default_inline_diagnostics_debounce_ms() -> u64 { + 150 +} + +fn default_inline_diagnostics_padding() -> u32 { + 4 +} + #[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] pub struct GitSettings { /// Whether or not to show the git gutter. @@ -156,7 +231,7 @@ pub struct InlineBlameSettings { /// Whether to show commit summary as part of the inline blame. /// /// Default: false - #[serde(default = "false_value")] + #[serde(default)] pub show_commit_summary: bool, } @@ -164,10 +239,6 @@ const fn true_value() -> bool { true } -const fn false_value() -> bool { - false -} - #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] pub struct BinarySettings { pub path: Option, diff --git a/crates/zed/src/zed/quick_action_bar.rs b/crates/zed/src/zed/quick_action_bar.rs index 4b2de40e9e9a25..e453fc4922e9ae 100644 --- a/crates/zed/src/zed/quick_action_bar.rs +++ b/crates/zed/src/zed/quick_action_bar.rs @@ -91,6 +91,8 @@ impl Render for QuickActionBar { selection_menu_enabled, inlay_hints_enabled, supports_inlay_hints, + inline_diagnostics_enabled, + supports_inline_diagnostics, git_blame_inline_enabled, show_git_blame_gutter, auto_signature_help_enabled, @@ -102,6 +104,8 @@ impl Render for QuickActionBar { let editor = editor.read(cx); let selection_menu_enabled = editor.selection_menu_enabled(cx); let inlay_hints_enabled = editor.inlay_hints_enabled(); + let show_inline_diagnostics = editor.show_inline_diagnostics(); + let supports_inline_diagnostics = editor.inline_diagnostics_enabled(); let git_blame_inline_enabled = editor.git_blame_inline_enabled(); let show_git_blame_gutter = editor.show_git_blame_gutter(); let auto_signature_help_enabled = editor.auto_signature_help_enabled(cx); @@ -112,6 +116,8 @@ impl Render for QuickActionBar { selection_menu_enabled, inlay_hints_enabled, supports_inlay_hints, + show_inline_diagnostics, + supports_inline_diagnostics, git_blame_inline_enabled, show_git_blame_gutter, auto_signature_help_enabled, @@ -257,6 +263,29 @@ impl Render for QuickActionBar { ); } + if supports_inline_diagnostics { + menu = menu.toggleable_entry( + "Inline Diagnostics", + inline_diagnostics_enabled, + IconPosition::Start, + Some(editor::actions::ToggleInlineDiagnostics.boxed_clone()), + { + let editor = editor.clone(); + move |window, cx| { + editor + .update(cx, |editor, cx| { + editor.toggle_inline_diagnostics( + &editor::actions::ToggleInlineDiagnostics, + window, + cx, + ); + }) + .ok(); + } + }, + ); + } + menu = menu.toggleable_entry( "Selection Menu", selection_menu_enabled, diff --git a/docs/src/configuring-zed.md b/docs/src/configuring-zed.md index cf8f04f6febe5a..96177d39ede807 100644 --- a/docs/src/configuring-zed.md +++ b/docs/src/configuring-zed.md @@ -1204,6 +1204,112 @@ To interpret all `.c` files as C++, files called `MyLockFile` as TOML and files } ``` +## Diagnostics + +- Description: Configuration for diagnostics-related features. +- Setting: `diagnostics` +- Default: + +```json +{ + "diagnostics": { + "include_warnings": true, + "inline": { + "enabled": false + } + "update_with_cursor": false, + "primary_only": false, + "use_rendered": false, + } +} +``` + +### Inline Diagnostics + +- Description: Whether or not to show diagnostics information inline. +- Setting: `inline` +- Default: + +```json +{ + "diagnostics": { + "inline": { + "enabled": false, + "update_debounce_ms": 150, + "padding": 4, + "min_column": 0, + "max_severity": null + } + } +} +``` + +**Options** + +1. Enable inline diagnostics. + +```json +{ + "diagnostics": { + "inline": { + "enabled": true + } + } +} +``` + +2. Delay diagnostic updates until some time after the last diagnostic update. + +```json +{ + "diagnostics": { + "inline": { + "enabled": true, + "update_debounce_ms": 150 + } + } +} +``` + +3. Set padding between the end of the source line and the start of the diagnostic. + +```json +{ + "diagnostics": { + "inline": { + "enabled": true, + "padding": 4 + } + } +} +``` + +4. Horizontally align inline diagnostics at the given column. + +```json +{ + "diagnostics": { + "inline": { + "enabled": true, + "min_column": 80 + } + } +} +``` + +5. Show only warning and error diagnostics. + +```json +{ + "diagnostics": { + "inline": { + "enabled": true, + "max_severity": "warning" + } + } +} +``` + ## Git - Description: Configuration for git-related features.