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