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 util::TryFutureExt;
16use workspace::Workspace;
17
18action!(Toggle);
19
20const CONTEXT_LINE_COUNT: u32 = 1;
21
22pub fn init(cx: &mut MutableAppContext) {
23 cx.add_bindings([Binding::new("alt-shift-D", Toggle, None)]);
24 cx.add_action(ProjectDiagnosticsEditor::toggle);
25}
26
27type Event = editor::Event;
28
29struct ProjectDiagnostics {
30 project: ModelHandle<Project>,
31}
32
33struct ProjectDiagnosticsEditor {
34 editor: ViewHandle<Editor>,
35 excerpts: ModelHandle<MultiBuffer>,
36 build_settings: BuildSettings,
37}
38
39impl ProjectDiagnostics {
40 fn new(project: ModelHandle<Project>) -> Self {
41 Self { project }
42 }
43}
44
45impl Entity for ProjectDiagnostics {
46 type Event = ();
47}
48
49impl Entity for ProjectDiagnosticsEditor {
50 type Event = Event;
51}
52
53impl View for ProjectDiagnosticsEditor {
54 fn ui_name() -> &'static str {
55 "ProjectDiagnosticsEditor"
56 }
57
58 fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
59 ChildView::new(self.editor.id()).boxed()
60 }
61
62 fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
63 cx.focus(&self.editor);
64 }
65}
66
67impl ProjectDiagnosticsEditor {
68 fn new(
69 project: ModelHandle<Project>,
70 settings: watch::Receiver<workspace::Settings>,
71 cx: &mut ViewContext<Self>,
72 ) -> Self {
73 let project_paths = project
74 .read(cx)
75 .diagnostic_summaries(cx)
76 .map(|e| e.0)
77 .collect::<Vec<_>>();
78
79 cx.spawn(|this, mut cx| {
80 let project = project.clone();
81 async move {
82 for project_path in project_paths {
83 let buffer = project
84 .update(&mut cx, |project, cx| project.open_buffer(project_path, cx))
85 .await?;
86 this.update(&mut cx, |view, cx| view.populate_excerpts(buffer, cx))
87 }
88 Result::<_, anyhow::Error>::Ok(())
89 }
90 })
91 .detach();
92
93 cx.subscribe(&project, |_, project, event, cx| {
94 if let project::Event::DiagnosticsUpdated(project_path) = event {
95 let project_path = project_path.clone();
96 cx.spawn(|this, mut cx| {
97 async move {
98 let buffer = project
99 .update(&mut cx, |project, cx| project.open_buffer(project_path, cx))
100 .await?;
101 this.update(&mut cx, |view, cx| view.populate_excerpts(buffer, cx));
102 Ok(())
103 }
104 .log_err()
105 })
106 .detach();
107 }
108 })
109 .detach();
110
111 let excerpts = cx.add_model(|cx| MultiBuffer::new(project.read(cx).replica_id()));
112 let build_settings = editor::settings_builder(excerpts.downgrade(), settings.clone());
113 let editor =
114 cx.add_view(|cx| Editor::for_buffer(excerpts.clone(), build_settings.clone(), cx));
115 cx.subscribe(&editor, |_, _, event, cx| cx.emit(*event))
116 .detach();
117 Self {
118 excerpts,
119 editor,
120 build_settings,
121 }
122 }
123
124 #[cfg(test)]
125 fn text(&self, cx: &AppContext) -> String {
126 self.editor.read(cx).text(cx)
127 }
128
129 fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
130 let diagnostics = cx.add_model(|_| ProjectDiagnostics::new(workspace.project().clone()));
131 workspace.add_item(diagnostics, cx);
132 }
133
134 fn populate_excerpts(&mut self, buffer: ModelHandle<Buffer>, cx: &mut ViewContext<Self>) {
135 let mut blocks = Vec::new();
136 let snapshot = buffer.read(cx).snapshot();
137
138 let excerpts_snapshot = self.excerpts.update(cx, |excerpts, excerpts_cx| {
139 for group in snapshot.diagnostic_groups::<Point>() {
140 let mut pending_range: Option<(Range<Point>, usize)> = None;
141 let mut is_first_excerpt = true;
142 for (ix, entry) in group.entries.iter().map(Some).chain([None]).enumerate() {
143 if let Some((range, start_ix)) = &mut pending_range {
144 if let Some(entry) = entry {
145 if entry.range.start.row <= range.end.row + 1 + CONTEXT_LINE_COUNT * 2 {
146 range.end = range.end.max(entry.range.end);
147 continue;
148 }
149 }
150
151 let excerpt_start =
152 Point::new(range.start.row.saturating_sub(CONTEXT_LINE_COUNT), 0);
153 let excerpt_end = snapshot.clip_point(
154 Point::new(range.end.row + CONTEXT_LINE_COUNT, u32::MAX),
155 Bias::Left,
156 );
157 let excerpt_id = excerpts.push_excerpt(
158 ExcerptProperties {
159 buffer: &buffer,
160 range: excerpt_start..excerpt_end,
161 },
162 excerpts_cx,
163 );
164
165 let header_position = (excerpt_id.clone(), language::Anchor::min());
166 if is_first_excerpt {
167 let primary = &group.entries[group.primary_ix].diagnostic;
168 let mut header = primary.clone();
169 header.message =
170 primary.message.split('\n').next().unwrap().to_string();
171 blocks.push(BlockProperties {
172 position: header_position,
173 height: 2,
174 render: diagnostic_header_renderer(
175 buffer.clone(),
176 header,
177 self.build_settings.clone(),
178 ),
179 disposition: BlockDisposition::Above,
180 });
181 } else {
182 blocks.push(BlockProperties {
183 position: header_position,
184 height: 1,
185 render: context_header_renderer(self.build_settings.clone()),
186 disposition: BlockDisposition::Above,
187 });
188 }
189
190 is_first_excerpt = false;
191 for entry in &group.entries[*start_ix..ix] {
192 let mut diagnostic = entry.diagnostic.clone();
193 if diagnostic.is_primary {
194 let mut lines = entry.diagnostic.message.split('\n');
195 lines.next();
196 diagnostic.message = lines.collect();
197 }
198
199 if !diagnostic.message.is_empty() {
200 let buffer_anchor = snapshot.anchor_before(entry.range.start);
201 blocks.push(BlockProperties {
202 position: (excerpt_id.clone(), buffer_anchor),
203 height: diagnostic.message.matches('\n').count() as u8 + 1,
204 render: diagnostic_block_renderer(
205 diagnostic,
206 true,
207 self.build_settings.clone(),
208 ),
209 disposition: BlockDisposition::Below,
210 });
211 }
212 }
213
214 pending_range.take();
215 }
216
217 if let Some(entry) = entry {
218 pending_range = Some((entry.range.clone(), ix));
219 }
220 }
221 }
222
223 excerpts.snapshot(excerpts_cx)
224 });
225
226 self.editor.update(cx, |editor, cx| {
227 editor.insert_blocks(
228 blocks.into_iter().map(|block| {
229 let (excerpt_id, text_anchor) = block.position;
230 BlockProperties {
231 position: excerpts_snapshot.anchor_in_excerpt(excerpt_id, text_anchor),
232 height: block.height,
233 render: block.render,
234 disposition: block.disposition,
235 }
236 }),
237 cx,
238 );
239 });
240 cx.notify();
241 }
242}
243
244impl workspace::Item for ProjectDiagnostics {
245 type View = ProjectDiagnosticsEditor;
246
247 fn build_view(
248 handle: ModelHandle<Self>,
249 settings: watch::Receiver<workspace::Settings>,
250 cx: &mut ViewContext<Self::View>,
251 ) -> Self::View {
252 let project = handle.read(cx).project.clone();
253 ProjectDiagnosticsEditor::new(project, settings, cx)
254 }
255
256 fn project_path(&self) -> Option<project::ProjectPath> {
257 None
258 }
259}
260
261impl workspace::ItemView for ProjectDiagnosticsEditor {
262 fn title(&self, _: &AppContext) -> String {
263 "Project Diagnostics".to_string()
264 }
265
266 fn project_path(&self, _: &AppContext) -> Option<project::ProjectPath> {
267 None
268 }
269
270 fn save(&mut self, cx: &mut ViewContext<Self>) -> Result<Task<Result<()>>> {
271 self.excerpts.update(cx, |excerpts, cx| excerpts.save(cx))
272 }
273
274 fn save_as(
275 &mut self,
276 _: ModelHandle<project::Worktree>,
277 _: &std::path::Path,
278 _: &mut ViewContext<Self>,
279 ) -> Task<Result<()>> {
280 unreachable!()
281 }
282
283 fn is_dirty(&self, cx: &AppContext) -> bool {
284 self.excerpts.read(cx).read(cx).is_dirty()
285 }
286
287 fn has_conflict(&self, cx: &AppContext) -> bool {
288 self.excerpts.read(cx).read(cx).has_conflict()
289 }
290
291 fn should_update_tab_on_event(event: &Event) -> bool {
292 matches!(
293 event,
294 Event::Saved | Event::Dirtied | Event::FileHandleChanged
295 )
296 }
297
298 fn can_save(&self, _: &AppContext) -> bool {
299 true
300 }
301
302 fn can_save_as(&self, _: &AppContext) -> bool {
303 false
304 }
305}
306
307#[cfg(test)]
308mod tests {
309 use super::*;
310 use client::{http::ServerResponse, test::FakeHttpClient, Client, UserStore};
311 use gpui::TestAppContext;
312 use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, LanguageRegistry, PointUtf16};
313 use project::FakeFs;
314 use serde_json::json;
315 use std::sync::Arc;
316 use unindent::Unindent as _;
317 use workspace::WorkspaceParams;
318
319 #[gpui::test]
320 async fn test_diagnostics(mut cx: TestAppContext) {
321 let settings = cx.update(WorkspaceParams::test).settings;
322 let http_client = FakeHttpClient::new(|_| async move { Ok(ServerResponse::new(404)) });
323 let client = Client::new();
324 let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
325 let fs = Arc::new(FakeFs::new());
326
327 let project = cx.update(|cx| {
328 Project::local(
329 client.clone(),
330 user_store,
331 Arc::new(LanguageRegistry::new()),
332 fs.clone(),
333 cx,
334 )
335 });
336
337 fs.insert_tree(
338 "/test",
339 json!({
340 "a.rs": "
341 const a: i32 = 'a';
342 ".unindent(),
343
344 "main.rs": "
345 fn main() {
346 let x = vec![];
347 let y = vec![];
348 a(x);
349 b(y);
350 // comment 1
351 // comment 2
352 c(y);
353 d(x);
354 }
355 "
356 .unindent(),
357 }),
358 )
359 .await;
360
361 let worktree = project
362 .update(&mut cx, |project, cx| {
363 project.add_local_worktree("/test", cx)
364 })
365 .await
366 .unwrap();
367
368 worktree.update(&mut cx, |worktree, cx| {
369 worktree
370 .update_diagnostic_entries(
371 Arc::from("/test/main.rs".as_ref()),
372 None,
373 vec![
374 DiagnosticEntry {
375 range: PointUtf16::new(1, 8)..PointUtf16::new(1, 9),
376 diagnostic: Diagnostic {
377 message:
378 "move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait"
379 .to_string(),
380 severity: DiagnosticSeverity::INFORMATION,
381 is_primary: false,
382 group_id: 1,
383 ..Default::default()
384 },
385 },
386 DiagnosticEntry {
387 range: PointUtf16::new(2, 8)..PointUtf16::new(2, 9),
388 diagnostic: Diagnostic {
389 message:
390 "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait"
391 .to_string(),
392 severity: DiagnosticSeverity::INFORMATION,
393 is_primary: false,
394 group_id: 0,
395 ..Default::default()
396 },
397 },
398 DiagnosticEntry {
399 range: PointUtf16::new(3, 6)..PointUtf16::new(3, 7),
400 diagnostic: Diagnostic {
401 message: "value moved here".to_string(),
402 severity: DiagnosticSeverity::INFORMATION,
403 is_primary: false,
404 group_id: 1,
405 ..Default::default()
406 },
407 },
408 DiagnosticEntry {
409 range: PointUtf16::new(4, 6)..PointUtf16::new(4, 7),
410 diagnostic: Diagnostic {
411 message: "value moved here".to_string(),
412 severity: DiagnosticSeverity::INFORMATION,
413 is_primary: false,
414 group_id: 0,
415 ..Default::default()
416 },
417 },
418 DiagnosticEntry {
419 range: PointUtf16::new(7, 6)..PointUtf16::new(7, 7),
420 diagnostic: Diagnostic {
421 message: "use of moved value\nvalue used here after move".to_string(),
422 severity: DiagnosticSeverity::ERROR,
423 is_primary: true,
424 group_id: 0,
425 ..Default::default()
426 },
427 },
428 DiagnosticEntry {
429 range: PointUtf16::new(8, 6)..PointUtf16::new(8, 7),
430 diagnostic: Diagnostic {
431 message: "use of moved value\nvalue used here after move".to_string(),
432 severity: DiagnosticSeverity::ERROR,
433 is_primary: true,
434 group_id: 1,
435 ..Default::default()
436 },
437 },
438 ],
439 cx,
440 )
441 .unwrap();
442 });
443
444 let view = cx.add_view(Default::default(), |cx| {
445 ProjectDiagnosticsEditor::new(project.clone(), settings, cx)
446 });
447
448 view.condition(&mut cx, |view, cx| view.text(cx).contains("fn main()"))
449 .await;
450
451 view.update(&mut cx, |view, cx| {
452 let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
453
454 assert_eq!(
455 editor.text(),
456 concat!(
457 // Diagnostic group 1 (error for `y`)
458 "\n", // primary message
459 "\n", // filename
460 " let x = vec![];\n",
461 " let y = vec![];\n",
462 "\n", // supporting diagnostic
463 " a(x);\n",
464 " b(y);\n",
465 "\n", // supporting diagnostic
466 " // comment 1\n",
467 " // comment 2\n",
468 " c(y);\n",
469 "\n", // supporting diagnostic
470 " d(x);\n",
471 // Diagnostic group 2 (error for `x`)
472 "\n", // primary message
473 "\n", // filename
474 "fn main() {\n",
475 " let x = vec![];\n",
476 "\n", // supporting diagnostic
477 " let y = vec![];\n",
478 " a(x);\n",
479 "\n", // supporting diagnostic
480 " b(y);\n",
481 "\n", // context ellipsis
482 " c(y);\n",
483 " d(x);\n",
484 "\n", // supporting diagnostic
485 "}"
486 )
487 );
488 });
489
490 worktree.update(&mut cx, |worktree, cx| {
491 worktree
492 .update_diagnostic_entries(
493 Arc::from("/test/a.rs".as_ref()),
494 None,
495 vec![DiagnosticEntry {
496 range: PointUtf16::new(0, 15)..PointUtf16::new(0, 15),
497 diagnostic: Diagnostic {
498 message: "mismatched types\nexpected `usize`, found `char`".to_string(),
499 severity: DiagnosticSeverity::ERROR,
500 is_primary: true,
501 group_id: 0,
502 ..Default::default()
503 },
504 }],
505 cx,
506 )
507 .unwrap();
508 });
509
510 view.condition(&mut cx, |view, cx| view.text(cx).contains("const a"))
511 .await;
512
513 view.update(&mut cx, |view, cx| {
514 let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
515
516 assert_eq!(
517 editor.text(),
518 concat!(
519 // a.rs
520 "\n", // primary message
521 "\n", // filename
522 "const a: i32 = 'a';\n",
523 // main.rs, diagnostic group 1
524 "\n", // primary message
525 "\n", // filename
526 " let x = vec![];\n",
527 " let y = vec![];\n",
528 "\n", // supporting diagnostic
529 " a(x);\n",
530 " b(y);\n",
531 "\n", // supporting diagnostic
532 " // comment 1\n",
533 " // comment 2\n",
534 " c(y);\n",
535 "\n", // supporting diagnostic
536 " d(x);\n",
537 // main.rs, diagnostic group 2
538 "\n", // primary message
539 "\n", // filename
540 "fn main() {\n",
541 " let x = vec![];\n",
542 "\n", // supporting diagnostic
543 " let y = vec![];\n",
544 " a(x);\n",
545 "\n", // supporting diagnostic
546 " b(y);\n",
547 "\n", // context ellipsis
548 " c(y);\n",
549 " d(x);\n",
550 "\n", // supporting diagnostic
551 "}"
552 )
553 );
554 });
555 }
556}