This commit is contained in:
Oliver Azevedo Barnes 2026-05-31 00:52:33 -05:00 committed by GitHub
commit 736dd7164a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 382 additions and 12 deletions

View file

@ -1508,7 +1508,10 @@
"diagnostics": {
// Whether to show the project diagnostics button in the status bar.
"button": true,
// Whether to show warnings or not by default.
// Whether to show warnings and info-level diagnostics.
//
// When true, errors, warnings, and info-level diagnostics are all shown.
// When false, only errors are shown.
//
// Default: true
"include_warnings": true,

View file

@ -160,6 +160,7 @@ CREATE TABLE "worktree_diagnostic_summaries" (
"language_server_id" INTEGER NOT NULL,
"error_count" INTEGER NOT NULL,
"warning_count" INTEGER NOT NULL,
"info_count" INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (project_id, worktree_id, path),
FOREIGN KEY (project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE
);

View file

@ -446,7 +446,8 @@ CREATE TABLE public.worktree_diagnostic_summaries (
path character varying NOT NULL,
language_server_id bigint NOT NULL,
error_count integer NOT NULL,
warning_count integer NOT NULL
warning_count integer NOT NULL,
info_count integer NOT NULL DEFAULT 0
);
CREATE TABLE public.worktree_entries (

View file

@ -539,6 +539,7 @@ impl Database {
language_server_id: ActiveValue::set(summary.language_server_id as i64),
error_count: ActiveValue::set(summary.error_count as i32),
warning_count: ActiveValue::set(summary.warning_count as i32),
info_count: ActiveValue::set(summary.info_count as i32),
})
.on_conflict(
OnConflict::columns([
@ -550,6 +551,7 @@ impl Database {
worktree_diagnostic_summary::Column::LanguageServerId,
worktree_diagnostic_summary::Column::ErrorCount,
worktree_diagnostic_summary::Column::WarningCount,
worktree_diagnostic_summary::Column::InfoCount,
])
.to_owned(),
)
@ -926,6 +928,7 @@ impl Database {
language_server_id: db_summary.language_server_id as u64,
error_count: db_summary.error_count as u32,
warning_count: db_summary.warning_count as u32,
info_count: db_summary.info_count as u32,
});
}
}

View file

@ -13,6 +13,7 @@ pub struct Model {
pub language_server_id: i64,
pub error_count: i32,
pub warning_count: i32,
pub info_count: i32,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]

View file

@ -4125,6 +4125,7 @@ async fn test_collaborating_with_diagnostics(
DiagnosticSummary {
error_count: 1,
warning_count: 0,
info_count: 0,
},
)]
)
@ -4161,6 +4162,7 @@ async fn test_collaborating_with_diagnostics(
DiagnosticSummary {
error_count: 1,
warning_count: 0,
info_count: 0,
},
)]
);
@ -4202,6 +4204,7 @@ async fn test_collaborating_with_diagnostics(
DiagnosticSummary {
error_count: 1,
warning_count: 1,
info_count: 0,
},
)]
);
@ -4219,6 +4222,7 @@ async fn test_collaborating_with_diagnostics(
DiagnosticSummary {
error_count: 1,
warning_count: 1,
info_count: 0,
},
)]
);

View file

@ -318,7 +318,7 @@ impl BufferDiagnosticsEditor {
let buffer_snapshot_max = buffer_snapshot.max_point();
let max_severity = Self::max_diagnostics_severity(self.include_warnings)
.into_lsp()
.unwrap_or(lsp::DiagnosticSeverity::WARNING);
.unwrap_or(lsp::DiagnosticSeverity::INFORMATION);
cx.spawn_in(window, async move |buffer_diagnostics_editor, mut cx| {
// Fetch the diagnostics for the whole of the buffer
@ -658,7 +658,7 @@ impl BufferDiagnosticsEditor {
fn max_diagnostics_severity(include_warnings: bool) -> DiagnosticSeverity {
match include_warnings {
true => DiagnosticSeverity::Warning,
true => DiagnosticSeverity::Info,
false => DiagnosticSeverity::Error,
}
}

View file

@ -226,7 +226,7 @@ impl ProjectDiagnosticsEditor {
editor.disable_inline_diagnostics();
editor.set_max_diagnostics_severity(
if include_warnings {
DiagnosticSeverity::Warning
DiagnosticSeverity::Info
} else {
DiagnosticSeverity::Error
},
@ -262,7 +262,7 @@ impl ProjectDiagnosticsEditor {
this.editor.update(cx, |editor, cx| {
editor.set_max_diagnostics_severity(
if include_warnings {
DiagnosticSeverity::Warning
DiagnosticSeverity::Info
} else {
DiagnosticSeverity::Error
},
@ -497,7 +497,7 @@ impl ProjectDiagnosticsEditor {
let buffer_id = buffer_snapshot.remote_id();
let max_severity = if self.include_warnings {
lsp::DiagnosticSeverity::WARNING
lsp::DiagnosticSeverity::INFORMATION
} else {
lsp::DiagnosticSeverity::ERROR
};

View file

@ -2023,12 +2023,221 @@ async fn test_buffer_diagnostics_multiple_servers(cx: &mut TestAppContext) {
*buffer_diagnostics.summary(),
DiagnosticSummary {
warning_count: 2,
error_count: 0
error_count: 0,
info_count: 0,
}
);
})
}
#[gpui::test]
async fn test_buffer_diagnostics_includes_info_with_warnings(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
path!("/test"),
json!({
"main.rs": "
fn main() {
let x = 1;
let y = 2;
}
"
.unindent(),
}),
)
.await;
let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let cx = &mut VisualTestContext::from_window(window.into(), cx);
let project_path = project::ProjectPath {
worktree_id: project.read_with(cx, |project, cx| {
project.worktrees(cx).next().unwrap().read(cx).id()
}),
path: rel_path("main.rs").into(),
};
let buffer = project
.update(cx, |project, cx| {
project.open_buffer(project_path.clone(), cx)
})
.await
.ok();
let language_server_id = LanguageServerId(0);
let uri = lsp::Uri::from_file_path(path!("/test/main.rs")).unwrap();
let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
lsp_store.update(cx, |lsp_store, cx| {
lsp_store
.update_diagnostics(
language_server_id,
lsp::PublishDiagnosticsParams {
uri: uri.clone(),
diagnostics: vec![
lsp::Diagnostic {
range: lsp::Range::new(
lsp::Position::new(1, 8),
lsp::Position::new(1, 9),
),
severity: Some(lsp::DiagnosticSeverity::INFORMATION),
message: "unused variable: `x`".to_string(),
..Default::default()
},
lsp::Diagnostic {
range: lsp::Range::new(
lsp::Position::new(2, 8),
lsp::Position::new(2, 9),
),
severity: Some(lsp::DiagnosticSeverity::ERROR),
message: "undefined function".to_string(),
..Default::default()
},
],
version: None,
},
None,
DiagnosticSourceKind::Pushed,
&[],
cx,
)
.unwrap();
});
let buffer_diagnostics = window.build_entity(cx, |window, cx| {
BufferDiagnosticsEditor::new(
project_path.clone(),
project.clone(),
buffer,
true,
window,
cx,
)
});
let editor = buffer_diagnostics.update(cx, |buffer_diagnostics, _cx| {
buffer_diagnostics.editor().clone()
});
cx.executor()
.advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10));
let content = editor_content_with_blocks(&editor, cx);
assert!(
content.contains("unused variable: `x`"),
"info diagnostic should be shown when include_warnings is true, got:\n{content}"
);
assert!(
content.contains("undefined function"),
"error diagnostic should always be shown, got:\n{content}"
);
}
#[gpui::test]
async fn test_buffer_diagnostics_excludes_info_without_warnings(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
path!("/test"),
json!({
"main.rs": "
fn main() {
let x = 1;
let y = 2;
}
"
.unindent(),
}),
)
.await;
let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let cx = &mut VisualTestContext::from_window(window.into(), cx);
let project_path = project::ProjectPath {
worktree_id: project.read_with(cx, |project, cx| {
project.worktrees(cx).next().unwrap().read(cx).id()
}),
path: rel_path("main.rs").into(),
};
let buffer = project
.update(cx, |project, cx| {
project.open_buffer(project_path.clone(), cx)
})
.await
.ok();
let language_server_id = LanguageServerId(0);
let uri = lsp::Uri::from_file_path(path!("/test/main.rs")).unwrap();
let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
lsp_store.update(cx, |lsp_store, cx| {
lsp_store
.update_diagnostics(
language_server_id,
lsp::PublishDiagnosticsParams {
uri: uri.clone(),
diagnostics: vec![
lsp::Diagnostic {
range: lsp::Range::new(
lsp::Position::new(1, 8),
lsp::Position::new(1, 9),
),
severity: Some(lsp::DiagnosticSeverity::INFORMATION),
message: "unused variable: `x`".to_string(),
..Default::default()
},
lsp::Diagnostic {
range: lsp::Range::new(
lsp::Position::new(2, 8),
lsp::Position::new(2, 9),
),
severity: Some(lsp::DiagnosticSeverity::ERROR),
message: "undefined function".to_string(),
..Default::default()
},
],
version: None,
},
None,
DiagnosticSourceKind::Pushed,
&[],
cx,
)
.unwrap();
});
let buffer_diagnostics = window.build_entity(cx, |window, cx| {
BufferDiagnosticsEditor::new(
project_path.clone(),
project.clone(),
buffer,
false,
window,
cx,
)
});
let editor = buffer_diagnostics.update(cx, |buffer_diagnostics, _cx| {
buffer_diagnostics.editor().clone()
});
cx.executor()
.advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10));
let content = editor_content_with_blocks(&editor, cx);
assert!(
!content.contains("unused variable: `x`"),
"info diagnostic should be hidden when include_warnings is false, got:\n{content}"
);
assert!(
content.contains("undefined function"),
"error diagnostic should always be shown, got:\n{content}"
);
}
fn init_test(cx: &mut TestAppContext) {
cx.update(|cx| {
zlog::init_test();

View file

@ -33,8 +33,9 @@ impl Render for DiagnosticIndicator {
return indicator.hidden();
}
let include_warnings = ProjectSettings::get_global(cx).diagnostics.include_warnings;
let diagnostic_indicator = match (self.summary.error_count, self.summary.warning_count) {
(0, 0) => h_flex().child(
(0, 0) if !include_warnings || self.summary.info_count == 0 => h_flex().child(
Icon::new(IconName::Check)
.size(IconSize::Small)
.color(Color::Default),
@ -56,6 +57,14 @@ impl Render for DiagnosticIndicator {
.color(Color::Warning),
)
.child(Label::new(warning_count.to_string()).size(LabelSize::Small))
})
.when(self.summary.info_count > 0 && include_warnings, |this| {
this.child(
Icon::new(IconName::Info)
.size(IconSize::Small)
.color(Color::Info),
)
.child(Label::new(self.summary.info_count.to_string()).size(LabelSize::Small))
}),
};

View file

@ -8135,6 +8135,7 @@ impl LspStore {
for (_, _, path_summary) in self.diagnostic_summaries(include_ignored, cx) {
summary.error_count += path_summary.error_count;
summary.warning_count += path_summary.warning_count;
summary.info_count += path_summary.info_count;
}
summary
}
@ -8150,12 +8151,13 @@ impl LspStore {
.get(&project_path.worktree_id)
.and_then(|map| map.get(&project_path.path))
{
let (error_count, warning_count) = summaries.iter().fold(
(0, 0),
|(error_count, warning_count), (_language_server_id, summary)| {
let (error_count, warning_count, info_count) = summaries.iter().fold(
(0, 0, 0),
|(error_count, warning_count, info_count), (_language_server_id, summary)| {
(
error_count + summary.error_count,
warning_count + summary.warning_count,
info_count + summary.info_count,
)
},
);
@ -8163,6 +8165,7 @@ impl LspStore {
DiagnosticSummary {
error_count,
warning_count,
info_count,
}
} else {
DiagnosticSummary::default()
@ -8549,6 +8552,7 @@ impl LspStore {
language_server_id: server_id.0 as u64,
error_count: 0,
warning_count: 0,
info_count: 0,
}),
more_summaries: Vec::new(),
})
@ -8798,6 +8802,7 @@ impl LspStore {
language_server_id: server_id.0 as u64,
error_count: new_summary.error_count,
warning_count: new_summary.warning_count,
info_count: new_summary.info_count,
})
}
None => {
@ -8809,6 +8814,7 @@ impl LspStore {
language_server_id: server_id.0 as u64,
error_count: new_summary.error_count,
warning_count: new_summary.warning_count,
info_count: new_summary.info_count,
}),
more_summaries: Vec::new(),
})
@ -8893,6 +8899,7 @@ impl LspStore {
language_server_id: server_id.0 as u64,
error_count: new_summary.error_count as u32,
warning_count: new_summary.warning_count as u32,
info_count: new_summary.info_count as u32,
},
))))
} else {
@ -9738,6 +9745,7 @@ impl LspStore {
let summary = DiagnosticSummary {
error_count: message_summary.error_count as usize,
warning_count: message_summary.warning_count as usize,
info_count: message_summary.info_count as usize,
};
if summary.is_empty() {
@ -9770,6 +9778,7 @@ impl LspStore {
language_server_id: server_id.0 as u64,
error_count: summary.error_count as u32,
warning_count: summary.warning_count as u32,
info_count: summary.info_count as u32,
})
}
None => {
@ -9781,6 +9790,7 @@ impl LspStore {
language_server_id: server_id.0 as u64,
error_count: summary.error_count as u32,
warning_count: summary.warning_count as u32,
info_count: summary.info_count as u32,
}),
more_summaries: Vec::new(),
})
@ -11241,6 +11251,7 @@ impl LspStore {
language_server_id: server_id.0 as u64,
error_count: 0,
warning_count: 0,
info_count: 0,
}),
more_summaries: Vec::new(),
})
@ -14413,6 +14424,7 @@ pub struct LanguageServerProgress {
pub struct DiagnosticSummary {
pub error_count: usize,
pub warning_count: usize,
pub info_count: usize,
}
impl DiagnosticSummary {
@ -14420,6 +14432,7 @@ impl DiagnosticSummary {
let mut this = Self {
error_count: 0,
warning_count: 0,
info_count: 0,
};
for entry in diagnostics {
@ -14427,6 +14440,7 @@ impl DiagnosticSummary {
match entry.diagnostic.severity {
DiagnosticSeverity::ERROR => this.error_count += 1,
DiagnosticSeverity::WARNING => this.warning_count += 1,
DiagnosticSeverity::INFORMATION => this.info_count += 1,
_ => {}
}
}
@ -14449,6 +14463,7 @@ impl DiagnosticSummary {
language_server_id: language_server_id.0 as u64,
error_count: self.error_count as u32,
warning_count: self.warning_count as u32,
info_count: self.info_count as u32,
}
}
}

View file

@ -2798,6 +2798,7 @@ async fn test_omitted_diagnostics(cx: &mut gpui::TestAppContext) {
DiagnosticSummary {
error_count: 1,
warning_count: 0,
info_count: 0,
}
)]
);
@ -3094,6 +3095,7 @@ async fn test_restarting_server_with_diagnostics_published(cx: &mut gpui::TestAp
DiagnosticSummary {
error_count: 1,
warning_count: 0,
info_count: 0,
}
);
});
@ -3120,6 +3122,7 @@ async fn test_restarting_server_with_diagnostics_published(cx: &mut gpui::TestAp
DiagnosticSummary {
error_count: 0,
warning_count: 0,
info_count: 0,
}
);
});
@ -3843,6 +3846,7 @@ async fn test_diagnostics_from_multiple_language_servers(cx: &mut gpui::TestAppC
DiagnosticSummary {
error_count: 2,
warning_count: 0,
info_count: 0,
}
);
});
@ -3906,6 +3910,7 @@ async fn test_diagnostic_summaries_cleared_on_worktree_entry_removal(
DiagnosticSummary {
error_count: 1,
warning_count: 1,
info_count: 0,
}
);
});
@ -3921,6 +3926,7 @@ async fn test_diagnostic_summaries_cleared_on_worktree_entry_removal(
DiagnosticSummary {
error_count: 0,
warning_count: 1,
info_count: 0,
},
);
});
@ -3965,6 +3971,7 @@ async fn test_diagnostic_summaries_cleared_on_server_restart(cx: &mut gpui::Test
DiagnosticSummary {
error_count: 1,
warning_count: 0,
info_count: 0,
}
);
});
@ -3995,6 +4002,7 @@ async fn test_diagnostic_summaries_cleared_on_server_restart(cx: &mut gpui::Test
DiagnosticSummary {
error_count: 0,
warning_count: 0,
info_count: 0,
}
);
});
@ -4084,6 +4092,7 @@ async fn test_diagnostic_summaries_cleared_on_buffer_reload(cx: &mut gpui::TestA
DiagnosticSummary {
error_count: 1,
warning_count: 0,
info_count: 0,
}
);
});

View file

@ -593,6 +593,7 @@ message DiagnosticSummary {
uint64 language_server_id = 2;
uint32 error_count = 3;
uint32 warning_count = 4;
uint32 info_count = 5;
}
message UpdateLanguageServer {

View file

@ -0,0 +1,114 @@
# Plan: Show `info_count` in the Status Bar
The goal is consistency: the status bar should reflect what the diagnostics panel
shows. When `include_warnings` is true the panel now shows errors, warnings, and
info-level diagnostics, so the status bar counter should too.
No new icons, toolbar buttons, or actions. The `include_warnings` setting name
stays the same. Changes touch six areas.
---
## 1. Extend `DiagnosticSummary``crates/project/src/lsp_store.rs`
**Struct** — add `pub info_count: usize`.
**`DiagnosticSummary::new()`** — add an arm to the severity match:
```rust
DiagnosticSeverity::INFORMATION => this.info_count += 1,
```
**`is_empty()`** — no change. It gates the checkmark icon ("no errors or
warnings"). An info-only workspace still showing the checkmark is acceptable.
**`diagnostic_summary()`** — add `summary.info_count += path_summary.info_count`
alongside the existing error/warning accumulation.
**`diagnostic_summary_for_path()`** — expand the fold from a 2-tuple to a 3-tuple
`(error_count, warning_count, info_count)` and include it in the returned struct.
**`to_proto()`** — add `info_count: self.info_count as u32`.
---
## 2. Extend the proto message — `crates/proto/proto/lsp.proto`
Add a new field to `message DiagnosticSummary`:
```proto
uint32 info_count = 5;
```
Protobuf field ordering means old clients silently ignore the new field, so this
is backward-compatible. No DB schema or migration change is needed — diagnostics
are not persisted in the collab database.
After updating the schema, run the repo's normal proto regeneration/build path so
the Rust bindings for `proto::DiagnosticSummary` pick up the new field. Also keep
the proto change Buf-clean (`buf lint` / `buf format`) so CI passes.
---
## 3. Propagate `info_count` through proto serialization — `crates/project/src/lsp_store.rs`
Every site in `lsp_store.rs` that constructs `proto::DiagnosticSummary { ...,
error_count, warning_count }` needs `info_count` added as well.
Every site in `lsp_store.rs` that deserializes a proto message back into
`DiagnosticSummary { error_count, warning_count }` needs
`info_count: message_summary.info_count as usize`.
---
## 4. Show `info_count` in the status bar — `crates/diagnostics/src/items.rs`
In `render()`, keep the outer checkmark-vs-counts decision based on
`error_count` and `warning_count` only, so an info-only workspace still renders
the checkmark. Within the existing non-checkmark branch, after the existing
`.when(warning_count > 0, ...)` child, add:
```rust
let include_warnings = ProjectSettings::get_global(cx).diagnostics.include_warnings;
// ...
.when(self.summary.info_count > 0 && include_warnings, |this| {
this.child(
Icon::new(IconName::Info)
.size(IconSize::Small)
.color(Color::Info),
)
.child(Label::new(self.summary.info_count.to_string()).size(LabelSize::Small))
})
```
`IconName::Info` and `Color::Info` already exist and are used elsewhere; no new
icons are introduced.
The auto-enable logic in the `on_click` handler (which sets `include_warnings =
true` when the user clicks the status bar with errors = 0 and warnings > 0) does
not need to change — info-only scenarios are rare and can be handled in a follow-up.
---
## 5. Fix the collab integration test — `crates/collab/tests/integration/integration_tests.rs`
`test_collaborating_with_diagnostics` constructs `DiagnosticSummary` literals.
Add `info_count: 0` to each, or derive `Default` on the struct and use
`..Default::default()` struct-update syntax.
---
## 6. Fix the diagnostics unit test — `crates/diagnostics/src/diagnostics_tests.rs`
`test_buffer_diagnostics_multiple_servers` asserts on `*buffer_diagnostics.summary()`
using a `DiagnosticSummary` literal. Add `info_count: 0` there too.
---
## Summary of files touched
| File | Change |
|------|--------|
| `crates/project/src/lsp_store.rs` | Add `info_count` to struct, `new()`, `is_empty()` (no-op), `diagnostic_summary()`, `diagnostic_summary_for_path()`, `to_proto()`, and all proto construction/deserialization sites |
| `crates/proto/proto/lsp.proto` | Add `uint32 info_count = 5` to `DiagnosticSummary` message and regenerate/build the Rust bindings as required by the repo's proto pipeline |
| `crates/diagnostics/src/items.rs` | Render `info_count` in status bar when `include_warnings` is true |
| `crates/collab/tests/integration/integration_tests.rs` | Add `info_count: 0` to `DiagnosticSummary` literals |
| `crates/diagnostics/src/diagnostics_tests.rs` | Add `info_count: 0` to `DiagnosticSummary` literal |