1use anyhow::Result;
2use editor::{
3 context_header_renderer, diagnostic_block_renderer, diagnostic_header_renderer,
4 display_map::{BlockDisposition, BlockProperties},
5 BuildSettings, Editor, ExcerptProperties, MultiBuffer,
6};
7use gpui::{
8 action, elements::*, keymap::Binding, AppContext, Entity, ModelHandle, MutableAppContext,
9 RenderContext, Task, View, ViewContext, ViewHandle,
10};
11use language::{Bias, Buffer, Point};
12use postage::watch;
13use project::Project;
14use std::ops::Range;
15use workspace::Workspace;
16
17action!(Toggle);
18
19pub fn init(cx: &mut MutableAppContext) {
20 cx.add_bindings([Binding::new("alt-shift-D", Toggle, None)]);
21 cx.add_action(ProjectDiagnosticsEditor::toggle);
22}
23
24type Event = editor::Event;
25
26struct ProjectDiagnostics {
27 project: ModelHandle<Project>,
28}
29
30struct ProjectDiagnosticsEditor {
31 editor: ViewHandle<Editor>,
32 excerpts: ModelHandle<MultiBuffer>,
33 build_settings: BuildSettings,
34}
35
36impl ProjectDiagnostics {
37 fn new(project: ModelHandle<Project>) -> Self {
38 Self { project }
39 }
40}
41
42impl Entity for ProjectDiagnostics {
43 type Event = ();
44}
45
46impl Entity for ProjectDiagnosticsEditor {
47 type Event = Event;
48}
49
50impl View for ProjectDiagnosticsEditor {
51 fn ui_name() -> &'static str {
52 "ProjectDiagnosticsEditor"
53 }
54
55 fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
56 ChildView::new(self.editor.id()).boxed()
57 }
58
59 fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
60 cx.focus(&self.editor);
61 }
62}
63
64impl ProjectDiagnosticsEditor {
65 fn new(
66 replica_id: u16,
67 settings: watch::Receiver<workspace::Settings>,
68 cx: &mut ViewContext<Self>,
69 ) -> Self {
70 let excerpts = cx.add_model(|_| MultiBuffer::new(replica_id));
71 let build_settings = editor::settings_builder(excerpts.downgrade(), settings.clone());
72 let editor =
73 cx.add_view(|cx| Editor::for_buffer(excerpts.clone(), build_settings.clone(), cx));
74 cx.subscribe(&editor, |_, _, event, cx| cx.emit(*event))
75 .detach();
76 Self {
77 excerpts,
78 editor,
79 build_settings,
80 }
81 }
82
83 fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
84 let diagnostics = cx.add_model(|_| ProjectDiagnostics::new(workspace.project().clone()));
85 workspace.add_item(diagnostics, cx);
86 }
87
88 fn populate_excerpts(&mut self, buffer: ModelHandle<Buffer>, cx: &mut ViewContext<Self>) {
89 let mut blocks = Vec::new();
90 let snapshot = buffer.read(cx).snapshot();
91
92 let excerpts_snapshot = self.excerpts.update(cx, |excerpts, excerpts_cx| {
93 for group in snapshot.diagnostic_groups::<Point>() {
94 let mut pending_range: Option<(Range<Point>, usize)> = None;
95 let mut is_first_excerpt = true;
96 for (ix, entry) in group.entries.iter().map(Some).chain([None]).enumerate() {
97 if let Some((range, start_ix)) = &mut pending_range {
98 if let Some(entry) = entry {
99 if entry.range.start.row <= range.end.row + 1 {
100 range.end = range.end.max(entry.range.end);
101 continue;
102 }
103 }
104
105 let excerpt_start = Point::new(range.start.row.saturating_sub(1), 0);
106 let excerpt_end = snapshot
107 .clip_point(Point::new(range.end.row + 1, u32::MAX), Bias::Left);
108 let excerpt_id = excerpts.push_excerpt(
109 ExcerptProperties {
110 buffer: &buffer,
111 range: excerpt_start..excerpt_end,
112 },
113 excerpts_cx,
114 );
115
116 let header_position = (excerpt_id.clone(), language::Anchor::min());
117 if is_first_excerpt {
118 let primary = &group.entries[group.primary_ix].diagnostic;
119 let mut header = primary.clone();
120 header.message =
121 primary.message.split('\n').next().unwrap().to_string();
122 blocks.push(BlockProperties {
123 position: header_position,
124 height: 2,
125 render: diagnostic_header_renderer(
126 buffer.clone(),
127 header,
128 self.build_settings.clone(),
129 ),
130 disposition: BlockDisposition::Above,
131 });
132 } else {
133 blocks.push(BlockProperties {
134 position: header_position,
135 height: 1,
136 render: context_header_renderer(self.build_settings.clone()),
137 disposition: BlockDisposition::Above,
138 });
139 }
140
141 is_first_excerpt = false;
142 for entry in &group.entries[*start_ix..ix] {
143 let mut diagnostic = entry.diagnostic.clone();
144 if diagnostic.is_primary {
145 let mut lines = entry.diagnostic.message.split('\n');
146 lines.next();
147 diagnostic.message = lines.collect();
148 }
149
150 if !diagnostic.message.is_empty() {
151 let buffer_anchor = snapshot.anchor_before(entry.range.start);
152 blocks.push(BlockProperties {
153 position: (excerpt_id.clone(), buffer_anchor),
154 height: diagnostic.message.matches('\n').count() as u8 + 1,
155 render: diagnostic_block_renderer(
156 diagnostic,
157 true,
158 self.build_settings.clone(),
159 ),
160 disposition: BlockDisposition::Below,
161 });
162 }
163 }
164
165 pending_range.take();
166 }
167
168 if let Some(entry) = entry {
169 pending_range = Some((entry.range.clone(), ix));
170 }
171 }
172 }
173
174 excerpts.snapshot(excerpts_cx)
175 });
176
177 self.editor.update(cx, |editor, cx| {
178 editor.insert_blocks(
179 blocks.into_iter().map(|block| {
180 let (excerpt_id, text_anchor) = block.position;
181 BlockProperties {
182 position: excerpts_snapshot.anchor_in_excerpt(excerpt_id, text_anchor),
183 height: block.height,
184 render: block.render,
185 disposition: block.disposition,
186 }
187 }),
188 cx,
189 );
190 });
191 }
192}
193
194impl workspace::Item for ProjectDiagnostics {
195 type View = ProjectDiagnosticsEditor;
196
197 fn build_view(
198 handle: ModelHandle<Self>,
199 settings: watch::Receiver<workspace::Settings>,
200 cx: &mut ViewContext<Self::View>,
201 ) -> Self::View {
202 let project = handle.read(cx).project.clone();
203 let project_paths = project
204 .read(cx)
205 .diagnostic_summaries(cx)
206 .map(|e| e.0)
207 .collect::<Vec<_>>();
208
209 cx.spawn(|view, mut cx| {
210 let project = project.clone();
211 async move {
212 for project_path in project_paths {
213 let buffer = project
214 .update(&mut cx, |project, cx| project.open_buffer(project_path, cx))
215 .await?;
216 view.update(&mut cx, |view, cx| view.populate_excerpts(buffer, cx))
217 }
218 Result::<_, anyhow::Error>::Ok(())
219 }
220 })
221 .detach();
222
223 ProjectDiagnosticsEditor::new(project.read(cx).replica_id(), settings, cx)
224 }
225
226 fn project_path(&self) -> Option<project::ProjectPath> {
227 None
228 }
229}
230
231impl workspace::ItemView for ProjectDiagnosticsEditor {
232 fn title(&self, _: &AppContext) -> String {
233 "Project Diagnostics".to_string()
234 }
235
236 fn project_path(&self, _: &AppContext) -> Option<project::ProjectPath> {
237 None
238 }
239
240 fn save(&mut self, cx: &mut ViewContext<Self>) -> Result<Task<Result<()>>> {
241 self.excerpts.update(cx, |excerpts, cx| excerpts.save(cx))
242 }
243
244 fn save_as(
245 &mut self,
246 _: ModelHandle<project::Worktree>,
247 _: &std::path::Path,
248 _: &mut ViewContext<Self>,
249 ) -> Task<Result<()>> {
250 unreachable!()
251 }
252
253 fn is_dirty(&self, cx: &AppContext) -> bool {
254 self.excerpts.read(cx).read(cx).is_dirty()
255 }
256
257 fn has_conflict(&self, cx: &AppContext) -> bool {
258 self.excerpts.read(cx).read(cx).has_conflict()
259 }
260
261 fn should_update_tab_on_event(event: &Event) -> bool {
262 matches!(
263 event,
264 Event::Saved | Event::Dirtied | Event::FileHandleChanged
265 )
266 }
267
268 fn can_save(&self, _: &AppContext) -> bool {
269 true
270 }
271
272 fn can_save_as(&self, _: &AppContext) -> bool {
273 false
274 }
275}
276
277#[cfg(test)]
278mod tests {
279 use super::*;
280 use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16};
281 use unindent::Unindent as _;
282 use workspace::WorkspaceParams;
283
284 #[gpui::test]
285 fn test_diagnostics(cx: &mut MutableAppContext) {
286 let settings = WorkspaceParams::test(cx).settings;
287 let view = cx.add_view(Default::default(), |cx| {
288 ProjectDiagnosticsEditor::new(0, settings, cx)
289 });
290
291 let text = "
292 fn main() {
293 let x = vec![];
294 let y = vec![];
295 a(x);
296 b(y);
297 // comment 1
298 // comment 2
299 // comment 3
300 // comment 4
301 d(y);
302 e(x);
303 }
304 "
305 .unindent();
306
307 let buffer = cx.add_model(|cx| {
308 let mut buffer = Buffer::new(0, text, cx);
309 buffer
310 .update_diagnostics(
311 None,
312 vec![
313 DiagnosticEntry {
314 range: PointUtf16::new(2, 8)..PointUtf16::new(2, 9),
315 diagnostic: Diagnostic {
316 message:
317 "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait"
318 .to_string(),
319 severity: DiagnosticSeverity::INFORMATION,
320 is_primary: false,
321 group_id: 0,
322 ..Default::default()
323 },
324 },
325 DiagnosticEntry {
326 range: PointUtf16::new(4, 6)..PointUtf16::new(4, 7),
327 diagnostic: Diagnostic {
328 message: "value moved here".to_string(),
329 severity: DiagnosticSeverity::INFORMATION,
330 is_primary: false,
331 group_id: 0,
332 ..Default::default()
333 },
334 },
335 DiagnosticEntry {
336 range: PointUtf16::new(8, 6)..PointUtf16::new(8, 7),
337 diagnostic: Diagnostic {
338 message: "use of moved value\nvalue used here after move".to_string(),
339 severity: DiagnosticSeverity::ERROR,
340 is_primary: true,
341 group_id: 0,
342 ..Default::default()
343 },
344 },
345 ],
346 cx,
347 )
348 .unwrap();
349 buffer
350 });
351
352 view.update(cx, |view, cx| {
353 view.populate_excerpts(buffer, cx);
354 assert_eq!(
355 view.editor.update(cx, |editor, cx| editor.display_text(cx)),
356 concat!(
357 "\n", // primary diagnostic message
358 "\n", // filename
359 " let x = vec![];\n",
360 " let y = vec![];\n",
361 " a(x);\n",
362 "\n", // context ellipsis
363 " a(x);\n",
364 " b(y);\n",
365 " // comment 1\n",
366 "\n", // context ellipsis
367 " // comment 3\n",
368 " // comment 4\n",
369 " d(y);"
370 )
371 );
372 });
373 }
374}