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(IconElement::new(Icon::Check).color(Color::Success)),
29 (0, warning_count) => h_stack()
30 .gap_1()
31 .child(IconElement::new(Icon::ExclamationTriangle).color(Color::Warning))
32 .child(Label::new(warning_count.to_string())),
33 (error_count, 0) => h_stack()
34 .gap_1()
35 .child(IconElement::new(Icon::XCircle).color(Color::Error))
36 .child(Label::new(error_count.to_string())),
37 (error_count, warning_count) => h_stack()
38 .gap_1()
39 .child(IconElement::new(Icon::XCircle).color(Color::Error))
40 .child(Label::new(error_count.to_string()))
41 .child(IconElement::new(Icon::ExclamationTriangle).color(Color::Warning))
42 .child(Label::new(warning_count.to_string())),
43 };
44
45 let status = if !self.in_progress_checks.is_empty() {
46 Some(Label::new("Checking…").into_any_element())
47 } else if let Some(diagnostic) = &self.current_diagnostic {
48 let message = diagnostic.message.split('\n').next().unwrap().to_string();
49 Some(
50 Button::new("diagnostic_message", message)
51 .tooltip(|cx| {
52 Tooltip::for_action("Next Diagnostic", &editor::GoToDiagnostic, cx)
53 })
54 .on_click(cx.listener(|this, _, cx| {
55 this.go_to_next_diagnostic(cx);
56 }))
57 .into_any_element(),
58 )
59 } else {
60 None
61 };
62
63 h_stack()
64 .h(rems(1.375))
65 .gap_2()
66 .child(
67 ButtonLike::new("diagnostic-indicator")
68 .child(diagnostic_indicator)
69 .tooltip(|cx| Tooltip::for_action("Project Diagnostics", &Deploy, cx))
70 .on_click(cx.listener(|this, _, cx| {
71 if let Some(workspace) = this.workspace.upgrade() {
72 workspace.update(cx, |workspace, cx| {
73 ProjectDiagnosticsEditor::deploy(workspace, &Default::default(), cx)
74 })
75 }
76 })),
77 )
78 .children(status)
79 }
80}
81
82impl DiagnosticIndicator {
83 pub fn new(workspace: &Workspace, cx: &mut ViewContext<Self>) -> Self {
84 let project = workspace.project();
85 cx.subscribe(project, |this, project, event, cx| match event {
86 project::Event::DiskBasedDiagnosticsStarted { language_server_id } => {
87 this.in_progress_checks.insert(*language_server_id);
88 cx.notify();
89 }
90
91 project::Event::DiskBasedDiagnosticsFinished { language_server_id }
92 | project::Event::LanguageServerRemoved(language_server_id) => {
93 this.summary = project.read(cx).diagnostic_summary(false, cx);
94 this.in_progress_checks.remove(language_server_id);
95 cx.notify();
96 }
97
98 project::Event::DiagnosticsUpdated { .. } => {
99 this.summary = project.read(cx).diagnostic_summary(false, cx);
100 cx.notify();
101 }
102
103 _ => {}
104 })
105 .detach();
106
107 Self {
108 summary: project.read(cx).diagnostic_summary(false, cx),
109 in_progress_checks: project
110 .read(cx)
111 .language_servers_running_disk_based_diagnostics()
112 .collect(),
113 active_editor: None,
114 workspace: workspace.weak_handle(),
115 current_diagnostic: None,
116 _observe_active_editor: None,
117 }
118 }
119
120 fn go_to_next_diagnostic(&mut self, cx: &mut ViewContext<Self>) {
121 if let Some(editor) = self.active_editor.as_ref().and_then(|e| e.upgrade()) {
122 editor.update(cx, |editor, cx| {
123 editor.go_to_diagnostic_impl(editor::Direction::Next, cx);
124 })
125 }
126 }
127
128 fn update(&mut self, editor: View<Editor>, cx: &mut ViewContext<Self>) {
129 let editor = editor.read(cx);
130 let buffer = editor.buffer().read(cx);
131 let cursor_position = editor.selections.newest::<usize>(cx).head();
132 let new_diagnostic = buffer
133 .snapshot(cx)
134 .diagnostics_in_range::<_, usize>(cursor_position..cursor_position, false)
135 .filter(|entry| !entry.range.is_empty())
136 .min_by_key(|entry| (entry.diagnostic.severity, entry.range.len()))
137 .map(|entry| entry.diagnostic);
138 if new_diagnostic != self.current_diagnostic {
139 self.current_diagnostic = new_diagnostic;
140 cx.notify();
141 }
142 }
143}
144
145impl EventEmitter<ToolbarItemEvent> for DiagnosticIndicator {}
146
147impl StatusItemView for DiagnosticIndicator {
148 fn set_active_pane_item(
149 &mut self,
150 active_pane_item: Option<&dyn ItemHandle>,
151 cx: &mut ViewContext<Self>,
152 ) {
153 if let Some(editor) = active_pane_item.and_then(|item| item.downcast::<Editor>()) {
154 self.active_editor = Some(editor.downgrade());
155 self._observe_active_editor = Some(cx.observe(&editor, Self::update));
156 self.update(editor, cx);
157 } else {
158 self.active_editor = None;
159 self.current_diagnostic = None;
160 self._observe_active_editor = None;
161 }
162 cx.notify();
163 }
164}