1use editor::Editor;
2use gpui::{
3 elements::*, platform::CursorStyle, serde_json, Entity, ModelHandle, RenderContext,
4 Subscription, View, ViewContext, ViewHandle,
5};
6use language::Diagnostic;
7use project::Project;
8use settings::Settings;
9use workspace::StatusItemView;
10
11pub struct DiagnosticIndicator {
12 summary: project::DiagnosticSummary,
13 current_diagnostic: Option<Diagnostic>,
14 check_in_progress: bool,
15 _observe_active_editor: Option<Subscription>,
16}
17
18impl DiagnosticIndicator {
19 pub fn new(project: &ModelHandle<Project>, cx: &mut ViewContext<Self>) -> Self {
20 cx.subscribe(project, |this, project, event, cx| match event {
21 project::Event::DiskBasedDiagnosticsUpdated => {
22 cx.notify();
23 }
24 project::Event::DiskBasedDiagnosticsStarted => {
25 this.check_in_progress = true;
26 cx.notify();
27 }
28 project::Event::DiskBasedDiagnosticsFinished => {
29 this.summary = project.read(cx).diagnostic_summary(cx);
30 this.check_in_progress = false;
31 cx.notify();
32 }
33 _ => {}
34 })
35 .detach();
36 Self {
37 summary: project.read(cx).diagnostic_summary(cx),
38 check_in_progress: project.read(cx).is_running_disk_based_diagnostics(),
39 current_diagnostic: None,
40 _observe_active_editor: None,
41 }
42 }
43
44 fn update(&mut self, editor: ViewHandle<Editor>, cx: &mut ViewContext<Self>) {
45 let editor = editor.read(cx);
46 let buffer = editor.buffer().read(cx);
47 let cursor_position = editor
48 .newest_selection_with_snapshot::<usize>(&buffer.read(cx))
49 .head();
50 let new_diagnostic = buffer
51 .read(cx)
52 .diagnostics_in_range::<_, usize>(cursor_position..cursor_position, false)
53 .filter(|entry| !entry.range.is_empty())
54 .min_by_key(|entry| (entry.diagnostic.severity, entry.range.len()))
55 .map(|entry| entry.diagnostic);
56 if new_diagnostic != self.current_diagnostic {
57 self.current_diagnostic = new_diagnostic;
58 cx.notify();
59 }
60 }
61}
62
63impl Entity for DiagnosticIndicator {
64 type Event = ();
65}
66
67impl View for DiagnosticIndicator {
68 fn ui_name() -> &'static str {
69 "DiagnosticIndicator"
70 }
71
72 fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
73 enum Tag {}
74
75 let in_progress = self.check_in_progress;
76 let mut element = Flex::row().with_child(
77 MouseEventHandler::new::<Tag, _, _>(0, cx, |state, cx| {
78 let style = &cx
79 .global::<Settings>()
80 .theme
81 .workspace
82 .status_bar
83 .diagnostics;
84
85 let summary_style = if self.summary.error_count > 0 {
86 if state.hovered {
87 &style.summary_error_hover
88 } else {
89 &style.summary_error
90 }
91 } else if self.summary.warning_count > 0 {
92 if state.hovered {
93 &style.summary_warning_hover
94 } else {
95 &style.summary_warning
96 }
97 } else if state.hovered {
98 &style.summary_ok_hover
99 } else {
100 &style.summary_ok
101 };
102
103 let mut summary_row = Flex::row();
104 if self.summary.error_count > 0 {
105 summary_row.add_children([
106 Svg::new("icons/error-solid-14.svg")
107 .with_color(style.icon_color_error)
108 .constrained()
109 .with_width(style.icon_width)
110 .aligned()
111 .contained()
112 .with_margin_right(style.icon_spacing)
113 .named("error-icon"),
114 Label::new(
115 self.summary.error_count.to_string(),
116 summary_style.text.clone(),
117 )
118 .aligned()
119 .boxed(),
120 ]);
121 }
122
123 if self.summary.warning_count > 0 {
124 summary_row.add_children([
125 Svg::new("icons/warning-solid-14.svg")
126 .with_color(style.icon_color_warning)
127 .constrained()
128 .with_width(style.icon_width)
129 .aligned()
130 .contained()
131 .with_margin_right(style.icon_spacing)
132 .with_margin_left(if self.summary.error_count > 0 {
133 style.summary_spacing
134 } else {
135 0.
136 })
137 .named("warning-icon"),
138 Label::new(
139 self.summary.warning_count.to_string(),
140 summary_style.text.clone(),
141 )
142 .aligned()
143 .boxed(),
144 ]);
145 }
146
147 if self.summary.error_count == 0 && self.summary.warning_count == 0 {
148 summary_row.add_child(
149 Svg::new("icons/no-error-solid-14.svg")
150 .with_color(style.icon_color_ok)
151 .constrained()
152 .with_width(style.icon_width)
153 .aligned()
154 .named("ok-icon"),
155 );
156 }
157
158 summary_row
159 .constrained()
160 .with_height(style.height)
161 .contained()
162 .with_style(summary_style.container)
163 .boxed()
164 })
165 .with_cursor_style(CursorStyle::PointingHand)
166 .on_click(|cx| cx.dispatch_action(crate::Deploy))
167 .aligned()
168 .boxed(),
169 );
170
171 let style = &cx.global::<Settings>().theme.workspace.status_bar;
172
173 if in_progress {
174 element.add_child(
175 Label::new("checking…".into(), style.diagnostics.message.text.clone())
176 .aligned()
177 .contained()
178 .with_margin_left(style.item_spacing)
179 .boxed(),
180 );
181 } else if let Some(diagnostic) = &self.current_diagnostic {
182 element.add_child(
183 Label::new(
184 diagnostic.message.split('\n').next().unwrap().to_string(),
185 style.diagnostics.message.text.clone(),
186 )
187 .aligned()
188 .contained()
189 .with_margin_left(style.item_spacing)
190 .boxed(),
191 );
192 }
193
194 element.named("diagnostic indicator")
195 }
196
197 fn debug_json(&self, _: &gpui::AppContext) -> serde_json::Value {
198 serde_json::json!({ "summary": self.summary })
199 }
200}
201
202impl StatusItemView for DiagnosticIndicator {
203 fn set_active_pane_item(
204 &mut self,
205 active_pane_item: Option<&dyn workspace::ItemHandle>,
206 cx: &mut ViewContext<Self>,
207 ) {
208 if let Some(editor) = active_pane_item.and_then(|item| item.downcast::<Editor>()) {
209 self._observe_active_editor = Some(cx.observe(&editor, Self::update));
210 self.update(editor, cx);
211 } else {
212 self.current_diagnostic = None;
213 self._observe_active_editor = None;
214 }
215 cx.notify();
216 }
217}