Skip to repository content271 lines · 10.5 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T03:32:16.641Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
items.rs
1use std::time::Duration;
2
3use editor::{Editor, MultiBufferOffset};
4use gpui::{
5 App, Context, Entity, EventEmitter, IntoElement, ParentElement, Render, Styled, Subscription,
6 Task, WeakEntity, Window,
7};
8use language::Diagnostic;
9use project::project_settings::{GoToDiagnosticSeverityFilter, ProjectSettings};
10use settings::Settings;
11use ui::{Button, ButtonLike, Color, Icon, IconName, Label, Tooltip, h_flex, prelude::*};
12use util::ResultExt;
13use workspace::{HideStatusItem, StatusItemView, ToolbarItemEvent, Workspace, item::ItemHandle};
14
15use crate::{Deploy, IncludeWarnings, ProjectDiagnosticsEditor};
16
17/// The status bar item that displays diagnostic counts.
18pub struct DiagnosticIndicator {
19 summary: project::DiagnosticSummary,
20 workspace: WeakEntity<Workspace>,
21 current_diagnostic: Option<Diagnostic>,
22 active_editor: Option<WeakEntity<Editor>>,
23 _observe_active_editor: Option<Subscription>,
24
25 diagnostics_update: Task<()>,
26 diagnostic_summary_update: Task<()>,
27}
28
29impl Render for DiagnosticIndicator {
30 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
31 let indicator = h_flex().gap_2().min_w_0().overflow_x_hidden();
32 if !ProjectSettings::get_global(cx).diagnostics.button {
33 return indicator.hidden();
34 }
35
36 let diagnostic_indicator = match (self.summary.error_count, self.summary.warning_count) {
37 (0, 0) => h_flex().child(
38 Icon::new(IconName::Check)
39 .size(IconSize::Small)
40 .color(Color::Default),
41 ),
42 (error_count, warning_count) => h_flex()
43 .gap_1()
44 .when(error_count > 0, |this| {
45 this.child(
46 Icon::new(IconName::XCircle)
47 .size(IconSize::Small)
48 .color(Color::Error),
49 )
50 .child(Label::new(error_count.to_string()).size(LabelSize::Small))
51 })
52 .when(warning_count > 0, |this| {
53 this.child(
54 Icon::new(IconName::Warning)
55 .size(IconSize::Small)
56 .color(Color::Warning),
57 )
58 .child(Label::new(warning_count.to_string()).size(LabelSize::Small))
59 }),
60 };
61
62 let status = if let Some(diagnostic) = &self.current_diagnostic {
63 let message = diagnostic
64 .message
65 .split_once('\n')
66 .map_or(&*diagnostic.message, |(first, _)| first);
67 let diagnostics_already_active = self.any_active_diagnostics(cx);
68 let tooltip = if !diagnostics_already_active {
69 "Expand Diagnostics"
70 } else {
71 "Next Diagnostic"
72 };
73 Some(
74 Button::new("diagnostic_message", SharedString::new(message))
75 .label_size(LabelSize::Small)
76 .truncate(true)
77 .tab_index(0isize)
78 .tooltip(move |_window, cx| {
79 Tooltip::for_action(
80 tooltip,
81 &editor::actions::GoToDiagnostic::default(),
82 cx,
83 )
84 })
85 .on_click(
86 cx.listener(|this, _, window, cx| this.go_to_next_diagnostic(window, cx)),
87 ),
88 )
89 } else {
90 None
91 };
92
93 let diagnostics_label = match (self.summary.error_count, self.summary.warning_count) {
94 (0, 0) => "Project diagnostics: no problems".to_string(),
95 (errors, warnings) => {
96 let mut parts = Vec::new();
97 if errors > 0 {
98 parts.push(format!(
99 "{errors} error{}",
100 if errors == 1 { "" } else { "s" }
101 ));
102 }
103 if warnings > 0 {
104 parts.push(format!(
105 "{warnings} warning{}",
106 if warnings == 1 { "" } else { "s" }
107 ));
108 }
109 format!("Project diagnostics: {}", parts.join(", "))
110 }
111 };
112
113 indicator
114 .child(
115 ButtonLike::new("diagnostic-indicator")
116 .child(diagnostic_indicator)
117 .tab_index(0isize)
118 .aria_label(diagnostics_label)
119 .tooltip(move |_window, cx| {
120 Tooltip::for_action("Project Diagnostics", &Deploy, cx)
121 })
122 .on_click(cx.listener(|this, _, window, cx| {
123 if let Some(workspace) = this.workspace.upgrade() {
124 if this.summary.error_count == 0 && this.summary.warning_count > 0 {
125 cx.update_default_global(
126 |show_warnings: &mut IncludeWarnings, _| show_warnings.0 = true,
127 );
128 }
129 workspace.update(cx, |workspace, cx| {
130 ProjectDiagnosticsEditor::deploy(
131 workspace,
132 &Default::default(),
133 window,
134 cx,
135 )
136 })
137 }
138 })),
139 )
140 .children(status)
141 }
142}
143
144impl DiagnosticIndicator {
145 pub fn new(workspace: &Workspace, cx: &mut Context<Self>) -> Self {
146 let project = workspace.project();
147 cx.subscribe(project, |this, project, event, cx| match event {
148 project::Event::DiskBasedDiagnosticsStarted { .. } => {
149 cx.notify();
150 }
151
152 project::Event::DiskBasedDiagnosticsFinished { .. }
153 | project::Event::LanguageServerRemoved(_) => {
154 this.summary = project.read(cx).diagnostic_summary(false, cx);
155 cx.notify();
156 }
157
158 project::Event::DiagnosticsUpdated { .. } => {
159 this.diagnostic_summary_update = cx.spawn(async move |this, cx| {
160 cx.background_executor()
161 .timer(Duration::from_millis(30))
162 .await;
163 this.update(cx, |this, cx| {
164 this.summary = project.read(cx).diagnostic_summary(false, cx);
165 cx.notify();
166 })
167 .log_err();
168 });
169 }
170
171 _ => {}
172 })
173 .detach();
174
175 Self {
176 summary: project.read(cx).diagnostic_summary(false, cx),
177 active_editor: None,
178 workspace: workspace.weak_handle(),
179 current_diagnostic: None,
180 _observe_active_editor: None,
181 diagnostics_update: Task::ready(()),
182 diagnostic_summary_update: Task::ready(()),
183 }
184 }
185
186 fn any_active_diagnostics(&self, cx: &mut Context<Self>) -> bool {
187 if let Some(editor) = self.active_editor.as_ref().and_then(|e| e.upgrade()) {
188 editor.read(cx).any_active_diagnostics()
189 } else {
190 false
191 }
192 }
193
194 fn go_to_next_diagnostic(&mut self, window: &mut Window, cx: &mut Context<Self>) {
195 if let Some(editor) = self.active_editor.as_ref().and_then(|e| e.upgrade()) {
196 editor.update(cx, |editor, cx| {
197 editor.go_to_diagnostic_at_cursor(
198 editor::Direction::Next,
199 GoToDiagnosticSeverityFilter::default(),
200 window,
201 cx,
202 );
203 })
204 }
205 }
206
207 fn update(&mut self, editor: Entity<Editor>, window: &mut Window, cx: &mut Context<Self>) {
208 let (buffer, cursor_position) = editor.update(cx, |editor, cx| {
209 let buffer = editor.buffer().read(cx).snapshot(cx);
210 let cursor_position = editor
211 .selections
212 .newest::<MultiBufferOffset>(&editor.display_snapshot(cx))
213 .head();
214 (buffer, cursor_position)
215 });
216 let new_diagnostic = buffer
217 .diagnostics_in_range::<MultiBufferOffset>(cursor_position..cursor_position)
218 .filter(|entry| !entry.range.is_empty())
219 .min_by_key(|entry| {
220 (
221 entry.diagnostic.severity,
222 entry.range.end - entry.range.start,
223 )
224 })
225 .map(|entry| entry.diagnostic);
226 if new_diagnostic != self.current_diagnostic.as_ref() {
227 let new_diagnostic = new_diagnostic.cloned();
228 self.diagnostics_update =
229 cx.spawn_in(window, async move |diagnostics_indicator, cx| {
230 cx.background_executor()
231 .timer(Duration::from_millis(50))
232 .await;
233 diagnostics_indicator
234 .update(cx, |diagnostics_indicator, cx| {
235 diagnostics_indicator.current_diagnostic = new_diagnostic;
236 cx.notify();
237 })
238 .ok();
239 });
240 }
241 }
242}
243
244impl EventEmitter<ToolbarItemEvent> for DiagnosticIndicator {}
245
246impl StatusItemView for DiagnosticIndicator {
247 fn set_active_pane_item(
248 &mut self,
249 active_pane_item: Option<&dyn ItemHandle>,
250 window: &mut Window,
251 cx: &mut Context<Self>,
252 ) {
253 if let Some(editor) = active_pane_item.and_then(|item| item.downcast::<Editor>()) {
254 self.active_editor = Some(editor.downgrade());
255 self._observe_active_editor = Some(cx.observe_in(&editor, window, Self::update));
256 self.update(editor, window, cx);
257 } else {
258 self.active_editor = None;
259 self.current_diagnostic = None;
260 self._observe_active_editor = None;
261 }
262 cx.notify();
263 }
264
265 fn hide_setting(&self, _: &App) -> Option<HideStatusItem> {
266 Some(HideStatusItem::new(|settings| {
267 settings.diagnostics.get_or_insert_default().button = Some(false);
268 }))
269 }
270}
271