Skip to content

Commit

Permalink
Add initial inline diagnostics support (#25297)
Browse files Browse the repository at this point in the history
https://github.com/user-attachments/assets/eb881707-e575-47ef-9ae0-67d8085d8065

Closes #22668
Closes #4901

Takes #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
<img width="242" alt="image"
src="https://github.com/user-attachments/assets/8e177511-4626-4434-902b-d6aa4d3fafd0"
/>

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 <[email protected]>
Co-authored-by: Danilo Leal <[email protected]>
  • Loading branch information
3 people authored Feb 20, 2025
1 parent 74c581b commit 5ae93ce
Show file tree
Hide file tree
Showing 11 changed files with 579 additions and 81 deletions.
1 change: 0 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 19 additions & 1 deletion assets/settings/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
1 change: 0 additions & 1 deletion crates/diagnostics/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 3 additions & 5 deletions crates/diagnostics/src/diagnostics.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
pub mod items;
mod project_diagnostics_settings;
mod toolbar_controls;

#[cfg(test)]
Expand All @@ -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},
Expand All @@ -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();
}

Expand Down Expand Up @@ -178,6 +175,7 @@ impl ProjectDiagnosticsEditor {
cx,
);
editor.set_vertical_scroll_margin(5, cx);
editor.disable_inline_diagnostics();
editor
});
cx.subscribe_in(
Expand Down Expand Up @@ -287,7 +285,7 @@ impl ProjectDiagnosticsEditor {

let include_warnings = match cx.try_global::<IncludeWarnings>() {
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| {
Expand Down
28 changes: 0 additions & 28 deletions crates/diagnostics/src/project_diagnostics_settings.rs

This file was deleted.

1 change: 1 addition & 0 deletions crates/editor/src/actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,7 @@ gpui::actions!(
ToggleGitBlameInline,
ToggleIndentGuides,
ToggleInlayHints,
ToggleInlineDiagnostics,
ToggleEditPrediction,
ToggleLineNumbers,
SwapSelectionEnds,
Expand Down
124 changes: 124 additions & 0 deletions crates/editor/src/editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -594,6 +603,10 @@ pub struct Editor {
select_larger_syntax_node_stack: Vec<Box<[Selection<usize>]>>,
ime_transaction: Option<TransactionId>,
active_diagnostics: Option<ActiveDiagnosticGroup>,
show_inline_diagnostics: bool,
inline_diagnostics_update: Task<()>,
inline_diagnostics_enabled: bool,
inline_diagnostics: Vec<(Anchor, InlineDiagnostic)>,
soft_wrap_mode_override: Option<language_settings::SoftWrap>,

// TODO: make this a access method
Expand Down Expand Up @@ -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 _),
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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<Self>,
) {
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<Selection<Anchor>>,
Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -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);
}
Expand Down
Loading

0 comments on commit 5ae93ce

Please sign in to comment.