1use collections::HashSet;
2use editor::Editor;
3use gpui::{
4 rems, Div, EventEmitter, IntoElement, ParentElement, Render, Styled, Subscription, View,
5 ViewContext, WeakView,
6};
7use language::Diagnostic;
8use lsp::LanguageServerId;
9use ui::{h_stack, prelude::*, Button, ButtonLike, Color, Icon, IconElement, Label, Tooltip};
10use workspace::{item::ItemHandle, StatusItemView, ToolbarItemEvent, Workspace};
11
12use crate::{Deploy, ProjectDiagnosticsEditor};
13
14pub struct DiagnosticIndicator {
15 summary: project::DiagnosticSummary,
16 active_editor: Option<WeakView<Editor>>,
17 workspace: WeakView<Workspace>,
18 current_diagnostic: Option<Diagnostic>,
19 in_progress_checks: HashSet<LanguageServerId>,
20 _observe_active_editor: Option<Subscription>,
21}
22
23impl Render for DiagnosticIndicator {
24 type Element = Div;
25
26 fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
27 let diagnostic_indicator = match (self.summary.error_count, self.summary.warning_count) {
28 (0, 0) => h_stack().child(
29 IconElement::new(Icon::Check)
30 .size(IconSize::Small)
31 .color(Color::Success),
32 ),
33 (0, warning_count) => h_stack()
34 .gap_1()
35 .child(
36 IconElement::new(Icon::ExclamationTriangle)
37 .size(IconSize::Small)
38 .color(Color::Warning),
39 )
40 .child(Label::new(warning_count.to_string()).size(LabelSize::Small)),
41 (error_count, 0) => h_stack()
42 .gap_1()
43 .child(
44 IconElement::new(Icon::XCircle)
45 .size(IconSize::Small)
46 .color(Color::Error),
47 )
48 .child(Label::new(error_count.to_string()).size(LabelSize::Small)),
49 (error_count, warning_count) => h_stack()
50 .gap_1()
51 .child(
52 IconElement::new(Icon::XCircle)
53 .size(IconSize::Small)
54 .color(Color::Error),
55 )
56 .child(Label::new(error_count.to_string()).size(LabelSize::Small))
57 .child(
58 IconElement::new(Icon::ExclamationTriangle)
59 .size(IconSize::Small)
60 .color(Color::Warning),
61 )
62 .child(Label::new(warning_count.to_string()).size(LabelSize::Small)),
63 };
64
65 let status = if !self.in_progress_checks.is_empty() {
66 Some(
67 Label::new("Checking…")
68 .size(LabelSize::Small)
69 .into_any_element(),
70 )
71 } else if let Some(diagnostic) = &self.current_diagnostic {
72 let message = diagnostic.message.split('\n').next().unwrap().to_string();
73 Some(
74 Button::new("diagnostic_message", message)
75 .label_size(LabelSize::Small)
76 .tooltip(|cx| {
77 Tooltip::for_action("Next Diagnostic", &editor::GoToDiagnostic, cx)
78 })
79 .on_click(cx.listener(|this, _, cx| {
80 this.go_to_next_diagnostic(cx);
81 }))
82 .into_any_element(),
83 )
84 } else {
85 None
86 };
87
88 h_stack()
89 .h(rems(1.375))
90 .gap_2()
91 .child(
92 ButtonLike::new("diagnostic-indicator")
93 .child(diagnostic_indicator)
94 .tooltip(|cx| Tooltip::for_action("Project Diagnostics", &Deploy, cx))
95 .on_click(cx.listener(|this, _, cx| {
96 if let Some(workspace) = this.workspace.upgrade() {
97 workspace.update(cx, |workspace, cx| {
98 ProjectDiagnosticsEditor::deploy(workspace, &Default::default(), cx)
99 })
100 }
101 })),
102 )
103 .children(status)
104 }
105}
106
107impl DiagnosticIndicator {
108 pub fn new(workspace: &Workspace, cx: &mut ViewContext<Self>) -> Self {
109 let project = workspace.project();
110 cx.subscribe(project, |this, project, event, cx| match event {
111 project::Event::DiskBasedDiagnosticsStarted { language_server_id } => {
112 this.in_progress_checks.insert(*language_server_id);
113 cx.notify();
114 }
115
116 project::Event::DiskBasedDiagnosticsFinished { language_server_id }
117 | project::Event::LanguageServerRemoved(language_server_id) => {
118 this.summary = project.read(cx).diagnostic_summary(false, cx);
119 this.in_progress_checks.remove(language_server_id);
120 cx.notify();
121 }
122
123 project::Event::DiagnosticsUpdated { .. } => {
124 this.summary = project.read(cx).diagnostic_summary(false, cx);
125 cx.notify();
126 }
127
128 _ => {}
129 })
130 .detach();
131
132 Self {
133 summary: project.read(cx).diagnostic_summary(false, cx),
134 in_progress_checks: project
135 .read(cx)
136 .language_servers_running_disk_based_diagnostics()
137 .collect(),
138 active_editor: None,
139 workspace: workspace.weak_handle(),
140 current_diagnostic: None,
141 _observe_active_editor: None,
142 }
143 }
144
145 fn go_to_next_diagnostic(&mut self, cx: &mut ViewContext<Self>) {
146 if let Some(editor) = self.active_editor.as_ref().and_then(|e| e.upgrade()) {
147 editor.update(cx, |editor, cx| {
148 editor.go_to_diagnostic_impl(editor::Direction::Next, cx);
149 })
150 }
151 }
152
153 fn update(&mut self, editor: View<Editor>, cx: &mut ViewContext<Self>) {
154 let editor = editor.read(cx);
155 let buffer = editor.buffer().read(cx);
156 let cursor_position = editor.selections.newest::<usize>(cx).head();
157 let new_diagnostic = buffer
158 .snapshot(cx)
159 .diagnostics_in_range::<_, usize>(cursor_position..cursor_position, false)
160 .filter(|entry| !entry.range.is_empty())
161 .min_by_key(|entry| (entry.diagnostic.severity, entry.range.len()))
162 .map(|entry| entry.diagnostic);
163 if new_diagnostic != self.current_diagnostic {
164 self.current_diagnostic = new_diagnostic;
165 cx.notify();
166 }
167 }
168}
169
170impl EventEmitter<ToolbarItemEvent> for DiagnosticIndicator {}
171
172impl StatusItemView for DiagnosticIndicator {
173 fn set_active_pane_item(
174 &mut self,
175 active_pane_item: Option<&dyn ItemHandle>,
176 cx: &mut ViewContext<Self>,
177 ) {
178 if let Some(editor) = active_pane_item.and_then(|item| item.downcast::<Editor>()) {
179 self.active_editor = Some(editor.downgrade());
180 self._observe_active_editor = Some(cx.observe(&editor, Self::update));
181 self.update(editor, cx);
182 } else {
183 self.active_editor = None;
184 self.current_diagnostic = None;
185 self._observe_active_editor = None;
186 }
187 cx.notify();
188 }
189}