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