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.