1use editor::Editor;
2use gpui::{
3 rems, EventEmitter, IntoElement, ParentElement, Render, Styled, Subscription, View,
4 ViewContext, 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 .h(rems(1.375))
81 .gap_2()
82 .child(
83 ButtonLike::new("diagnostic-indicator")
84 .child(diagnostic_indicator)
85 .tooltip(|cx| Tooltip::for_action("Project Diagnostics", &Deploy, cx))
86 .on_click(cx.listener(|this, _, cx| {
87 if let Some(workspace) = this.workspace.upgrade() {
88 workspace.update(cx, |workspace, cx| {
89 ProjectDiagnosticsEditor::deploy(workspace, &Default::default(), cx)
90 })
91 }
92 })),
93 )
94 .children(status)
95 }
96}
97
98impl DiagnosticIndicator {
99 pub fn new(workspace: &Workspace, cx: &mut ViewContext<Self>) -> Self {
100 let project = workspace.project();
101 cx.subscribe(project, |this, project, event, cx| match event {
102 project::Event::DiskBasedDiagnosticsStarted { .. } => {
103 cx.notify();
104 }
105
106 project::Event::DiskBasedDiagnosticsFinished { .. }
107 | project::Event::LanguageServerRemoved(_) => {
108 this.summary = project.read(cx).diagnostic_summary(false, cx);
109 cx.notify();
110 }
111
112 project::Event::DiagnosticsUpdated { .. } => {
113 this.summary = project.read(cx).diagnostic_summary(false, cx);
114 cx.notify();
115 }
116
117 _ => {}
118 })
119 .detach();
120
121 Self {
122 summary: project.read(cx).diagnostic_summary(false, cx),
123 active_editor: None,
124 workspace: workspace.weak_handle(),
125 current_diagnostic: None,
126 _observe_active_editor: None,
127 }
128 }
129
130 fn go_to_next_diagnostic(&mut self, cx: &mut ViewContext<Self>) {
131 if let Some(editor) = self.active_editor.as_ref().and_then(|e| e.upgrade()) {
132 editor.update(cx, |editor, cx| {
133 editor.go_to_diagnostic_impl(editor::Direction::Next, cx);
134 })
135 }
136 }
137
138 fn update(&mut self, editor: View<Editor>, cx: &mut ViewContext<Self>) {
139 let (buffer, cursor_position) = editor.update(cx, |editor, cx| {
140 let buffer = editor.buffer().read(cx).snapshot(cx);
141 let cursor_position = editor.selections.newest::<usize>(cx).head();
142 (buffer, cursor_position)
143 });
144 let new_diagnostic = buffer
145 .diagnostics_in_range::<_, usize>(cursor_position..cursor_position, false)
146 .filter(|entry| !entry.range.is_empty())
147 .min_by_key(|entry| (entry.diagnostic.severity, entry.range.len()))
148 .map(|entry| entry.diagnostic);
149 if new_diagnostic != self.current_diagnostic {
150 self.current_diagnostic = new_diagnostic;
151 cx.notify();
152 }
153 }
154}
155
156impl EventEmitter<ToolbarItemEvent> for DiagnosticIndicator {}
157
158impl StatusItemView for DiagnosticIndicator {
159 fn set_active_pane_item(
160 &mut self,
161 active_pane_item: Option<&dyn ItemHandle>,
162 cx: &mut ViewContext<Self>,
163 ) {
164 if let Some(editor) = active_pane_item.and_then(|item| item.downcast::<Editor>()) {
165 self.active_editor = Some(editor.downgrade());
166 self._observe_active_editor = Some(cx.observe(&editor, Self::update));
167 self.update(editor, cx);
168 } else {
169 self.active_editor = None;
170 self.current_diagnostic = None;
171 self._observe_active_editor = None;
172 }
173 cx.notify();
174 }
175}