1pub mod items;
2
3use anyhow::Result;
4use collections::{BTreeSet, HashMap, HashSet};
5use editor::{
6 diagnostic_block_renderer, diagnostic_style,
7 display_map::{BlockDisposition, BlockId, BlockProperties, RenderBlock},
8 items::BufferItemHandle,
9 Autoscroll, BuildSettings, Editor, ExcerptId, ExcerptProperties, MultiBuffer, ToOffset,
10};
11use gpui::{
12 action, elements::*, keymap::Binding, AnyViewHandle, AppContext, Entity, ModelHandle,
13 MutableAppContext, RenderContext, Task, View, ViewContext, ViewHandle, WeakViewHandle,
14};
15use language::{Bias, Buffer, Diagnostic, DiagnosticEntry, Point, Selection, SelectionGoal};
16use postage::watch;
17use project::{Project, ProjectPath};
18use std::{
19 any::{Any, TypeId},
20 cmp::Ordering,
21 mem,
22 ops::Range,
23 path::PathBuf,
24 sync::Arc,
25};
26use util::TryFutureExt;
27use workspace::{ItemNavHistory, Workspace};
28
29action!(Deploy);
30action!(OpenExcerpts);
31
32const CONTEXT_LINE_COUNT: u32 = 1;
33
34pub fn init(cx: &mut MutableAppContext) {
35 cx.add_bindings([
36 Binding::new("alt-shift-D", Deploy, Some("Workspace")),
37 Binding::new(
38 "alt-shift-D",
39 OpenExcerpts,
40 Some("ProjectDiagnosticsEditor"),
41 ),
42 ]);
43 cx.add_action(ProjectDiagnosticsEditor::deploy);
44 cx.add_action(ProjectDiagnosticsEditor::open_excerpts);
45}
46
47type Event = editor::Event;
48
49struct ProjectDiagnostics {
50 project: ModelHandle<Project>,
51}
52
53struct ProjectDiagnosticsEditor {
54 model: ModelHandle<ProjectDiagnostics>,
55 workspace: WeakViewHandle<Workspace>,
56 editor: ViewHandle<Editor>,
57 excerpts: ModelHandle<MultiBuffer>,
58 path_states: Vec<PathState>,
59 paths_to_update: BTreeSet<ProjectPath>,
60 build_settings: BuildSettings,
61 settings: watch::Receiver<workspace::Settings>,
62}
63
64struct PathState {
65 path: ProjectPath,
66 header: Option<BlockId>,
67 diagnostic_groups: Vec<DiagnosticGroupState>,
68}
69
70struct DiagnosticGroupState {
71 primary_diagnostic: DiagnosticEntry<language::Anchor>,
72 primary_excerpt_ix: usize,
73 excerpts: Vec<ExcerptId>,
74 blocks: HashSet<BlockId>,
75 block_count: usize,
76}
77
78impl ProjectDiagnostics {
79 fn new(project: ModelHandle<Project>) -> Self {
80 Self { project }
81 }
82}
83
84impl Entity for ProjectDiagnostics {
85 type Event = ();
86}
87
88impl Entity for ProjectDiagnosticsEditor {
89 type Event = Event;
90}
91
92impl View for ProjectDiagnosticsEditor {
93 fn ui_name() -> &'static str {
94 "ProjectDiagnosticsEditor"
95 }
96
97 fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
98 if self.path_states.is_empty() {
99 let theme = &self.settings.borrow().theme.project_diagnostics;
100 Label::new(
101 "No problems detected in the project".to_string(),
102 theme.empty_message.clone(),
103 )
104 .aligned()
105 .contained()
106 .with_style(theme.container)
107 .boxed()
108 } else {
109 ChildView::new(self.editor.id()).boxed()
110 }
111 }
112
113 fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
114 if !self.path_states.is_empty() {
115 cx.focus(&self.editor);
116 }
117 }
118}
119
120impl ProjectDiagnosticsEditor {
121 fn new(
122 model: ModelHandle<ProjectDiagnostics>,
123 workspace: WeakViewHandle<Workspace>,
124 settings: watch::Receiver<workspace::Settings>,
125 cx: &mut ViewContext<Self>,
126 ) -> Self {
127 let project = model.read(cx).project.clone();
128 cx.subscribe(&project, |this, _, event, cx| match event {
129 project::Event::DiskBasedDiagnosticsFinished => {
130 let paths = mem::take(&mut this.paths_to_update);
131 this.update_excerpts(paths, cx);
132 }
133 project::Event::DiagnosticsUpdated(path) => {
134 this.paths_to_update.insert(path.clone());
135 }
136 _ => {}
137 })
138 .detach();
139
140 let excerpts = cx.add_model(|cx| MultiBuffer::new(project.read(cx).replica_id()));
141 let build_settings = editor::settings_builder(excerpts.downgrade(), settings.clone());
142 let editor =
143 cx.add_view(|cx| Editor::for_buffer(excerpts.clone(), build_settings.clone(), cx));
144 cx.subscribe(&editor, |_, _, event, cx| cx.emit(*event))
145 .detach();
146
147 let paths_to_update = project
148 .read(cx)
149 .diagnostic_summaries(cx)
150 .map(|e| e.0)
151 .collect();
152 let this = Self {
153 model,
154 workspace,
155 excerpts,
156 editor,
157 build_settings,
158 settings,
159 path_states: Default::default(),
160 paths_to_update: Default::default(),
161 };
162 this.update_excerpts(paths_to_update, cx);
163 this
164 }
165
166 fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext<Workspace>) {
167 if let Some(existing) = workspace.item_of_type::<ProjectDiagnostics>(cx) {
168 workspace.activate_item(&existing, cx);
169 } else {
170 let diagnostics =
171 cx.add_model(|_| ProjectDiagnostics::new(workspace.project().clone()));
172 workspace.open_item(diagnostics, cx);
173 }
174 }
175
176 fn open_excerpts(&mut self, _: &OpenExcerpts, cx: &mut ViewContext<Self>) {
177 if let Some(workspace) = self.workspace.upgrade(cx) {
178 let editor = self.editor.read(cx);
179 let excerpts = self.excerpts.read(cx);
180 let mut new_selections_by_buffer = HashMap::default();
181
182 for selection in editor.local_selections::<usize>(cx) {
183 for (buffer, mut range) in
184 excerpts.excerpted_buffers(selection.start..selection.end, cx)
185 {
186 if selection.reversed {
187 mem::swap(&mut range.start, &mut range.end);
188 }
189 new_selections_by_buffer
190 .entry(buffer)
191 .or_insert(Vec::new())
192 .push(range)
193 }
194 }
195
196 workspace.update(cx, |workspace, cx| {
197 for (buffer, ranges) in new_selections_by_buffer {
198 let buffer = BufferItemHandle(buffer);
199 if !workspace.activate_pane_for_item(&buffer, cx) {
200 workspace.activate_next_pane(cx);
201 }
202 let editor = workspace
203 .open_item(buffer, cx)
204 .downcast::<Editor>()
205 .unwrap();
206 editor.update(cx, |editor, cx| {
207 editor.select_ranges(ranges, Some(Autoscroll::Center), cx)
208 });
209 }
210 });
211 }
212 }
213
214 fn update_excerpts(&self, paths: BTreeSet<ProjectPath>, cx: &mut ViewContext<Self>) {
215 let project = self.model.read(cx).project.clone();
216 cx.spawn(|this, mut cx| {
217 async move {
218 for path in paths {
219 let buffer = project
220 .update(&mut cx, |project, cx| project.open_buffer(path.clone(), cx))
221 .await?;
222 this.update(&mut cx, |view, cx| view.populate_excerpts(path, buffer, cx))
223 }
224 Result::<_, anyhow::Error>::Ok(())
225 }
226 .log_err()
227 })
228 .detach();
229 }
230
231 fn populate_excerpts(
232 &mut self,
233 path: ProjectPath,
234 buffer: ModelHandle<Buffer>,
235 cx: &mut ViewContext<Self>,
236 ) {
237 let was_empty = self.path_states.is_empty();
238 let snapshot = buffer.read(cx).snapshot();
239 let path_ix = match self.path_states.binary_search_by_key(&&path, |e| &e.path) {
240 Ok(ix) => ix,
241 Err(ix) => {
242 self.path_states.insert(
243 ix,
244 PathState {
245 path: path.clone(),
246 header: None,
247 diagnostic_groups: Default::default(),
248 },
249 );
250 ix
251 }
252 };
253
254 let mut prev_excerpt_id = if path_ix > 0 {
255 let prev_path_last_group = &self.path_states[path_ix - 1]
256 .diagnostic_groups
257 .last()
258 .unwrap();
259 prev_path_last_group.excerpts.last().unwrap().clone()
260 } else {
261 ExcerptId::min()
262 };
263
264 let path_state = &mut self.path_states[path_ix];
265 let mut groups_to_add = Vec::new();
266 let mut group_ixs_to_remove = Vec::new();
267 let mut blocks_to_add = Vec::new();
268 let mut blocks_to_remove = HashSet::default();
269 let mut first_excerpt_id = None;
270 let excerpts_snapshot = self.excerpts.update(cx, |excerpts, excerpts_cx| {
271 let mut old_groups = path_state.diagnostic_groups.iter().enumerate().peekable();
272 let mut new_groups = snapshot
273 .diagnostic_groups()
274 .into_iter()
275 .filter(|group| group.entries[group.primary_ix].diagnostic.is_disk_based)
276 .peekable();
277
278 loop {
279 let mut to_insert = None;
280 let mut to_remove = None;
281 let mut to_keep = None;
282 match (old_groups.peek(), new_groups.peek()) {
283 (None, None) => break,
284 (None, Some(_)) => to_insert = new_groups.next(),
285 (Some(_), None) => to_remove = old_groups.next(),
286 (Some((_, old_group)), Some(new_group)) => {
287 let old_primary = &old_group.primary_diagnostic;
288 let new_primary = &new_group.entries[new_group.primary_ix];
289 match compare_diagnostics(old_primary, new_primary, &snapshot) {
290 Ordering::Less => to_remove = old_groups.next(),
291 Ordering::Equal => {
292 to_keep = old_groups.next();
293 new_groups.next();
294 }
295 Ordering::Greater => to_insert = new_groups.next(),
296 }
297 }
298 }
299
300 if let Some(group) = to_insert {
301 let mut group_state = DiagnosticGroupState {
302 primary_diagnostic: group.entries[group.primary_ix].clone(),
303 primary_excerpt_ix: 0,
304 excerpts: Default::default(),
305 blocks: Default::default(),
306 block_count: 0,
307 };
308 let mut pending_range: Option<(Range<Point>, usize)> = None;
309 let mut is_first_excerpt_for_group = true;
310 for (ix, entry) in group.entries.iter().map(Some).chain([None]).enumerate() {
311 let resolved_entry = entry.map(|e| e.resolve::<Point>(&snapshot));
312 if let Some((range, start_ix)) = &mut pending_range {
313 if let Some(entry) = resolved_entry.as_ref() {
314 if entry.range.start.row
315 <= range.end.row + 1 + CONTEXT_LINE_COUNT * 2
316 {
317 range.end = range.end.max(entry.range.end);
318 continue;
319 }
320 }
321
322 let excerpt_start =
323 Point::new(range.start.row.saturating_sub(CONTEXT_LINE_COUNT), 0);
324 let excerpt_end = snapshot.clip_point(
325 Point::new(range.end.row + CONTEXT_LINE_COUNT, u32::MAX),
326 Bias::Left,
327 );
328 let excerpt_id = excerpts.insert_excerpt_after(
329 &prev_excerpt_id,
330 ExcerptProperties {
331 buffer: &buffer,
332 range: excerpt_start..excerpt_end,
333 },
334 excerpts_cx,
335 );
336
337 prev_excerpt_id = excerpt_id.clone();
338 first_excerpt_id.get_or_insert_with(|| prev_excerpt_id.clone());
339 group_state.excerpts.push(excerpt_id.clone());
340 let header_position = (excerpt_id.clone(), language::Anchor::min());
341
342 if is_first_excerpt_for_group {
343 is_first_excerpt_for_group = false;
344 let primary = &group.entries[group.primary_ix].diagnostic;
345 let mut header = primary.clone();
346 header.message =
347 primary.message.split('\n').next().unwrap().to_string();
348 group_state.block_count += 1;
349 blocks_to_add.push(BlockProperties {
350 position: header_position,
351 height: 2,
352 render: diagnostic_header_renderer(
353 header,
354 true,
355 self.build_settings.clone(),
356 ),
357 disposition: BlockDisposition::Above,
358 });
359 } else {
360 group_state.block_count += 1;
361 blocks_to_add.push(BlockProperties {
362 position: header_position,
363 height: 1,
364 render: context_header_renderer(self.build_settings.clone()),
365 disposition: BlockDisposition::Above,
366 });
367 }
368
369 for entry in &group.entries[*start_ix..ix] {
370 let mut diagnostic = entry.diagnostic.clone();
371 if diagnostic.is_primary {
372 group_state.primary_excerpt_ix = group_state.excerpts.len() - 1;
373 diagnostic.message =
374 entry.diagnostic.message.split('\n').skip(1).collect();
375 }
376
377 if !diagnostic.message.is_empty() {
378 group_state.block_count += 1;
379 blocks_to_add.push(BlockProperties {
380 position: (excerpt_id.clone(), entry.range.start.clone()),
381 height: diagnostic.message.matches('\n').count() as u8 + 1,
382 render: diagnostic_block_renderer(
383 diagnostic,
384 true,
385 self.build_settings.clone(),
386 ),
387 disposition: BlockDisposition::Below,
388 });
389 }
390 }
391
392 pending_range.take();
393 }
394
395 if let Some(entry) = resolved_entry {
396 pending_range = Some((entry.range.clone(), ix));
397 }
398 }
399
400 groups_to_add.push(group_state);
401 } else if let Some((group_ix, group_state)) = to_remove {
402 excerpts.remove_excerpts(group_state.excerpts.iter(), excerpts_cx);
403 group_ixs_to_remove.push(group_ix);
404 blocks_to_remove.extend(group_state.blocks.iter().copied());
405 } else if let Some((_, group)) = to_keep {
406 prev_excerpt_id = group.excerpts.last().unwrap().clone();
407 first_excerpt_id.get_or_insert_with(|| prev_excerpt_id.clone());
408 }
409 }
410
411 excerpts.snapshot(excerpts_cx)
412 });
413
414 self.editor.update(cx, |editor, cx| {
415 blocks_to_remove.extend(path_state.header);
416 editor.remove_blocks(blocks_to_remove, cx);
417 let header_block = first_excerpt_id.map(|excerpt_id| BlockProperties {
418 position: excerpts_snapshot.anchor_in_excerpt(excerpt_id, language::Anchor::min()),
419 height: 2,
420 render: path_header_renderer(buffer, self.build_settings.clone()),
421 disposition: BlockDisposition::Above,
422 });
423 let block_ids = editor.insert_blocks(
424 blocks_to_add
425 .into_iter()
426 .map(|block| {
427 let (excerpt_id, text_anchor) = block.position;
428 BlockProperties {
429 position: excerpts_snapshot.anchor_in_excerpt(excerpt_id, text_anchor),
430 height: block.height,
431 render: block.render,
432 disposition: block.disposition,
433 }
434 })
435 .chain(header_block.into_iter()),
436 cx,
437 );
438
439 let mut block_ids = block_ids.into_iter();
440 for group_state in &mut groups_to_add {
441 group_state.blocks = block_ids.by_ref().take(group_state.block_count).collect();
442 }
443 path_state.header = block_ids.next();
444 });
445
446 for ix in group_ixs_to_remove.into_iter().rev() {
447 path_state.diagnostic_groups.remove(ix);
448 }
449 path_state.diagnostic_groups.extend(groups_to_add);
450 path_state.diagnostic_groups.sort_unstable_by(|a, b| {
451 let range_a = &a.primary_diagnostic.range;
452 let range_b = &b.primary_diagnostic.range;
453 range_a
454 .start
455 .cmp(&range_b.start, &snapshot)
456 .unwrap()
457 .then_with(|| range_a.end.cmp(&range_b.end, &snapshot).unwrap())
458 });
459
460 if path_state.diagnostic_groups.is_empty() {
461 self.path_states.remove(path_ix);
462 }
463
464 self.editor.update(cx, |editor, cx| {
465 let groups;
466 let mut selections;
467 let new_excerpt_ids_by_selection_id;
468 if was_empty {
469 groups = self.path_states.first()?.diagnostic_groups.as_slice();
470 new_excerpt_ids_by_selection_id = [(0, ExcerptId::min())].into_iter().collect();
471 selections = vec![Selection {
472 id: 0,
473 start: 0,
474 end: 0,
475 reversed: false,
476 goal: SelectionGoal::None,
477 }];
478 } else {
479 groups = self.path_states.get(path_ix)?.diagnostic_groups.as_slice();
480 new_excerpt_ids_by_selection_id = editor.refresh_selections(cx);
481 selections = editor.local_selections::<usize>(cx);
482 }
483
484 // If any selection has lost its position, move it to start of the next primary diagnostic.
485 for selection in &mut selections {
486 if let Some(new_excerpt_id) = new_excerpt_ids_by_selection_id.get(&selection.id) {
487 let group_ix = match groups.binary_search_by(|probe| {
488 probe.excerpts.last().unwrap().cmp(&new_excerpt_id)
489 }) {
490 Ok(ix) | Err(ix) => ix,
491 };
492 if let Some(group) = groups.get(group_ix) {
493 let offset = excerpts_snapshot
494 .anchor_in_excerpt(
495 group.excerpts[group.primary_excerpt_ix].clone(),
496 group.primary_diagnostic.range.start.clone(),
497 )
498 .to_offset(&excerpts_snapshot);
499 selection.start = offset;
500 selection.end = offset;
501 }
502 }
503 }
504 editor.update_selections(selections, None, cx);
505 Some(())
506 });
507
508 if self.path_states.is_empty() {
509 if self.editor.is_focused(cx) {
510 cx.focus_self();
511 }
512 } else {
513 if cx.handle().is_focused(cx) {
514 cx.focus(&self.editor);
515 }
516 }
517 cx.notify();
518 }
519}
520
521impl workspace::Item for ProjectDiagnostics {
522 type View = ProjectDiagnosticsEditor;
523
524 fn build_view(
525 handle: ModelHandle<Self>,
526 workspace: &Workspace,
527 nav_history: ItemNavHistory,
528 cx: &mut ViewContext<Self::View>,
529 ) -> Self::View {
530 let diagnostics = ProjectDiagnosticsEditor::new(
531 handle,
532 workspace.weak_handle(),
533 workspace.settings(),
534 cx,
535 );
536 diagnostics
537 .editor
538 .update(cx, |editor, _| editor.set_nav_history(Some(nav_history)));
539 diagnostics
540 }
541
542 fn project_path(&self) -> Option<project::ProjectPath> {
543 None
544 }
545}
546
547impl workspace::ItemView for ProjectDiagnosticsEditor {
548 type ItemHandle = ModelHandle<ProjectDiagnostics>;
549
550 fn item_handle(&self, _: &AppContext) -> Self::ItemHandle {
551 self.model.clone()
552 }
553
554 fn title(&self, _: &AppContext) -> String {
555 "Project Diagnostics".to_string()
556 }
557
558 fn project_path(&self, _: &AppContext) -> Option<project::ProjectPath> {
559 None
560 }
561
562 fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) {
563 self.editor
564 .update(cx, |editor, cx| editor.navigate(data, cx));
565 }
566
567 fn is_dirty(&self, cx: &AppContext) -> bool {
568 self.excerpts.read(cx).read(cx).is_dirty()
569 }
570
571 fn has_conflict(&self, cx: &AppContext) -> bool {
572 self.excerpts.read(cx).read(cx).has_conflict()
573 }
574
575 fn can_save(&self, _: &AppContext) -> bool {
576 true
577 }
578
579 fn save(&mut self, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
580 self.excerpts.update(cx, |excerpts, cx| excerpts.save(cx))
581 }
582
583 fn can_save_as(&self, _: &AppContext) -> bool {
584 false
585 }
586
587 fn save_as(
588 &mut self,
589 _: ModelHandle<Project>,
590 _: PathBuf,
591 _: &mut ViewContext<Self>,
592 ) -> Task<Result<()>> {
593 unreachable!()
594 }
595
596 fn should_activate_item_on_event(event: &Self::Event) -> bool {
597 Editor::should_activate_item_on_event(event)
598 }
599
600 fn should_update_tab_on_event(event: &Event) -> bool {
601 matches!(
602 event,
603 Event::Saved | Event::Dirtied | Event::FileHandleChanged
604 )
605 }
606
607 fn clone_on_split(&self, cx: &mut ViewContext<Self>) -> Option<Self>
608 where
609 Self: Sized,
610 {
611 let diagnostics = ProjectDiagnosticsEditor::new(
612 self.model.clone(),
613 self.workspace.clone(),
614 self.settings.clone(),
615 cx,
616 );
617 diagnostics.editor.update(cx, |editor, cx| {
618 let nav_history = self
619 .editor
620 .read(cx)
621 .nav_history()
622 .map(|nav_history| ItemNavHistory::new(nav_history.history(), &cx.handle()));
623 editor.set_nav_history(nav_history);
624 });
625 Some(diagnostics)
626 }
627
628 fn act_as_type(
629 &self,
630 type_id: TypeId,
631 self_handle: &ViewHandle<Self>,
632 _: &AppContext,
633 ) -> Option<AnyViewHandle> {
634 if type_id == TypeId::of::<Self>() {
635 Some(self_handle.into())
636 } else if type_id == TypeId::of::<Editor>() {
637 Some((&self.editor).into())
638 } else {
639 None
640 }
641 }
642
643 fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
644 self.editor.update(cx, |editor, cx| editor.deactivated(cx));
645 }
646}
647
648fn path_header_renderer(buffer: ModelHandle<Buffer>, build_settings: BuildSettings) -> RenderBlock {
649 Arc::new(move |cx| {
650 let settings = build_settings(cx);
651 let file_path = if let Some(file) = buffer.read(&**cx).file() {
652 file.path().to_string_lossy().to_string()
653 } else {
654 "untitled".to_string()
655 };
656 let mut text_style = settings.style.text.clone();
657 let style = settings.style.diagnostic_path_header;
658 text_style.color = style.text;
659 Label::new(file_path, text_style)
660 .aligned()
661 .left()
662 .contained()
663 .with_style(style.header)
664 .with_padding_left(cx.line_number_x)
665 .expanded()
666 .named("path header block")
667 })
668}
669
670fn diagnostic_header_renderer(
671 diagnostic: Diagnostic,
672 is_valid: bool,
673 build_settings: BuildSettings,
674) -> RenderBlock {
675 Arc::new(move |cx| {
676 let settings = build_settings(cx);
677 let mut text_style = settings.style.text.clone();
678 let diagnostic_style = diagnostic_style(diagnostic.severity, is_valid, &settings.style);
679 text_style.color = diagnostic_style.text;
680 Text::new(diagnostic.message.clone(), text_style)
681 .with_soft_wrap(false)
682 .aligned()
683 .left()
684 .contained()
685 .with_style(diagnostic_style.header)
686 .with_padding_left(cx.line_number_x)
687 .expanded()
688 .named("diagnostic header")
689 })
690}
691
692fn context_header_renderer(build_settings: BuildSettings) -> RenderBlock {
693 Arc::new(move |cx| {
694 let settings = build_settings(cx);
695 let text_style = settings.style.text.clone();
696 Label::new("…".to_string(), text_style)
697 .contained()
698 .with_padding_left(cx.line_number_x)
699 .named("collapsed context")
700 })
701}
702
703fn compare_diagnostics<L: language::ToOffset, R: language::ToOffset>(
704 lhs: &DiagnosticEntry<L>,
705 rhs: &DiagnosticEntry<R>,
706 snapshot: &language::BufferSnapshot,
707) -> Ordering {
708 lhs.range
709 .start
710 .to_offset(&snapshot)
711 .cmp(&rhs.range.start.to_offset(snapshot))
712 .then_with(|| {
713 lhs.range
714 .end
715 .to_offset(&snapshot)
716 .cmp(&rhs.range.end.to_offset(snapshot))
717 })
718 .then_with(|| lhs.diagnostic.message.cmp(&rhs.diagnostic.message))
719}
720
721#[cfg(test)]
722mod tests {
723 use super::*;
724 use editor::{display_map::BlockContext, DisplayPoint, EditorSnapshot};
725 use gpui::TestAppContext;
726 use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16};
727 use serde_json::json;
728 use std::sync::Arc;
729 use unindent::Unindent as _;
730 use workspace::WorkspaceParams;
731
732 #[gpui::test]
733 async fn test_diagnostics(mut cx: TestAppContext) {
734 let params = cx.update(WorkspaceParams::test);
735 let project = params.project.clone();
736 let workspace = cx.add_view(0, |cx| Workspace::new(¶ms, cx));
737
738 params
739 .fs
740 .as_fake()
741 .insert_tree(
742 "/test",
743 json!({
744 "consts.rs": "
745 const a: i32 = 'a';
746 const b: i32 = c;
747 "
748 .unindent(),
749
750 "main.rs": "
751 fn main() {
752 let x = vec![];
753 let y = vec![];
754 a(x);
755 b(y);
756 // comment 1
757 // comment 2
758 c(y);
759 d(x);
760 }
761 "
762 .unindent(),
763 }),
764 )
765 .await;
766
767 let (worktree, _) = project
768 .update(&mut cx, |project, cx| {
769 project.find_or_create_worktree_for_abs_path("/test", false, cx)
770 })
771 .await
772 .unwrap();
773 let worktree_id = worktree.read_with(&cx, |tree, _| tree.id());
774
775 // Create some diagnostics
776 worktree.update(&mut cx, |worktree, cx| {
777 worktree
778 .as_local_mut()
779 .unwrap()
780 .update_diagnostic_entries(
781 Arc::from("/test/main.rs".as_ref()),
782 None,
783 vec![
784 DiagnosticEntry {
785 range: PointUtf16::new(1, 8)..PointUtf16::new(1, 9),
786 diagnostic: Diagnostic {
787 message:
788 "move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait"
789 .to_string(),
790 severity: DiagnosticSeverity::INFORMATION,
791 is_primary: false,
792 is_disk_based: true,
793 group_id: 1,
794 ..Default::default()
795 },
796 },
797 DiagnosticEntry {
798 range: PointUtf16::new(2, 8)..PointUtf16::new(2, 9),
799 diagnostic: Diagnostic {
800 message:
801 "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait"
802 .to_string(),
803 severity: DiagnosticSeverity::INFORMATION,
804 is_primary: false,
805 is_disk_based: true,
806 group_id: 0,
807 ..Default::default()
808 },
809 },
810 DiagnosticEntry {
811 range: PointUtf16::new(3, 6)..PointUtf16::new(3, 7),
812 diagnostic: Diagnostic {
813 message: "value moved here".to_string(),
814 severity: DiagnosticSeverity::INFORMATION,
815 is_primary: false,
816 is_disk_based: true,
817 group_id: 1,
818 ..Default::default()
819 },
820 },
821 DiagnosticEntry {
822 range: PointUtf16::new(4, 6)..PointUtf16::new(4, 7),
823 diagnostic: Diagnostic {
824 message: "value moved here".to_string(),
825 severity: DiagnosticSeverity::INFORMATION,
826 is_primary: false,
827 is_disk_based: true,
828 group_id: 0,
829 ..Default::default()
830 },
831 },
832 DiagnosticEntry {
833 range: PointUtf16::new(7, 6)..PointUtf16::new(7, 7),
834 diagnostic: Diagnostic {
835 message: "use of moved value\nvalue used here after move".to_string(),
836 severity: DiagnosticSeverity::ERROR,
837 is_primary: true,
838 is_disk_based: true,
839 group_id: 0,
840 ..Default::default()
841 },
842 },
843 DiagnosticEntry {
844 range: PointUtf16::new(8, 6)..PointUtf16::new(8, 7),
845 diagnostic: Diagnostic {
846 message: "use of moved value\nvalue used here after move".to_string(),
847 severity: DiagnosticSeverity::ERROR,
848 is_primary: true,
849 is_disk_based: true,
850 group_id: 1,
851 ..Default::default()
852 },
853 },
854 ],
855 cx,
856 )
857 .unwrap();
858 });
859
860 // Open the project diagnostics view while there are already diagnostics.
861 let model = cx.add_model(|_| ProjectDiagnostics::new(project.clone()));
862 let view = cx.add_view(0, |cx| {
863 ProjectDiagnosticsEditor::new(model, workspace.downgrade(), params.settings, cx)
864 });
865
866 view.next_notification(&cx).await;
867 view.update(&mut cx, |view, cx| {
868 let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
869
870 assert_eq!(
871 editor_blocks(&editor, cx),
872 [
873 (0, "path header block".into()),
874 (2, "diagnostic header".into()),
875 (15, "diagnostic header".into()),
876 (24, "collapsed context".into()),
877 ]
878 );
879 assert_eq!(
880 editor.text(),
881 concat!(
882 //
883 // main.rs
884 //
885 "\n", // filename
886 "\n", // padding
887 // diagnostic group 1
888 "\n", // primary message
889 "\n", // padding
890 " let x = vec![];\n",
891 " let y = vec![];\n",
892 "\n", // supporting diagnostic
893 " a(x);\n",
894 " b(y);\n",
895 "\n", // supporting diagnostic
896 " // comment 1\n",
897 " // comment 2\n",
898 " c(y);\n",
899 "\n", // supporting diagnostic
900 " d(x);\n",
901 // diagnostic group 2
902 "\n", // primary message
903 "\n", // padding
904 "fn main() {\n",
905 " let x = vec![];\n",
906 "\n", // supporting diagnostic
907 " let y = vec![];\n",
908 " a(x);\n",
909 "\n", // supporting diagnostic
910 " b(y);\n",
911 "\n", // context ellipsis
912 " c(y);\n",
913 " d(x);\n",
914 "\n", // supporting diagnostic
915 "}"
916 )
917 );
918
919 // Cursor is at the first diagnostic
920 view.editor.update(cx, |editor, cx| {
921 assert_eq!(
922 editor.selected_display_ranges(cx),
923 [DisplayPoint::new(12, 6)..DisplayPoint::new(12, 6)]
924 );
925 });
926 });
927
928 // Diagnostics are added for another earlier path.
929 worktree.update(&mut cx, |worktree, cx| {
930 worktree
931 .as_local_mut()
932 .unwrap()
933 .update_diagnostic_entries(
934 Arc::from("/test/consts.rs".as_ref()),
935 None,
936 vec![DiagnosticEntry {
937 range: PointUtf16::new(0, 15)..PointUtf16::new(0, 15),
938 diagnostic: Diagnostic {
939 message: "mismatched types\nexpected `usize`, found `char`".to_string(),
940 severity: DiagnosticSeverity::ERROR,
941 is_primary: true,
942 is_disk_based: true,
943 group_id: 0,
944 ..Default::default()
945 },
946 }],
947 cx,
948 )
949 .unwrap();
950 });
951 project.update(&mut cx, |_, cx| {
952 cx.emit(project::Event::DiagnosticsUpdated(ProjectPath {
953 worktree_id,
954 path: Arc::from("/test/consts.rs".as_ref()),
955 }));
956 cx.emit(project::Event::DiskBasedDiagnosticsFinished);
957 });
958
959 view.next_notification(&cx).await;
960 view.update(&mut cx, |view, cx| {
961 let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
962
963 assert_eq!(
964 editor_blocks(&editor, cx),
965 [
966 (0, "path header block".into()),
967 (2, "diagnostic header".into()),
968 (7, "path header block".into()),
969 (9, "diagnostic header".into()),
970 (22, "diagnostic header".into()),
971 (31, "collapsed context".into()),
972 ]
973 );
974 assert_eq!(
975 editor.text(),
976 concat!(
977 //
978 // consts.rs
979 //
980 "\n", // filename
981 "\n", // padding
982 // diagnostic group 1
983 "\n", // primary message
984 "\n", // padding
985 "const a: i32 = 'a';\n",
986 "\n", // supporting diagnostic
987 "const b: i32 = c;\n",
988 //
989 // main.rs
990 //
991 "\n", // filename
992 "\n", // padding
993 // diagnostic group 1
994 "\n", // primary message
995 "\n", // padding
996 " let x = vec![];\n",
997 " let y = vec![];\n",
998 "\n", // supporting diagnostic
999 " a(x);\n",
1000 " b(y);\n",
1001 "\n", // supporting diagnostic
1002 " // comment 1\n",
1003 " // comment 2\n",
1004 " c(y);\n",
1005 "\n", // supporting diagnostic
1006 " d(x);\n",
1007 // diagnostic group 2
1008 "\n", // primary message
1009 "\n", // filename
1010 "fn main() {\n",
1011 " let x = vec![];\n",
1012 "\n", // supporting diagnostic
1013 " let y = vec![];\n",
1014 " a(x);\n",
1015 "\n", // supporting diagnostic
1016 " b(y);\n",
1017 "\n", // context ellipsis
1018 " c(y);\n",
1019 " d(x);\n",
1020 "\n", // supporting diagnostic
1021 "}"
1022 )
1023 );
1024
1025 // Cursor keeps its position.
1026 view.editor.update(cx, |editor, cx| {
1027 assert_eq!(
1028 editor.selected_display_ranges(cx),
1029 [DisplayPoint::new(19, 6)..DisplayPoint::new(19, 6)]
1030 );
1031 });
1032 });
1033
1034 // Diagnostics are added to the first path
1035 worktree.update(&mut cx, |worktree, cx| {
1036 worktree
1037 .as_local_mut()
1038 .unwrap()
1039 .update_diagnostic_entries(
1040 Arc::from("/test/consts.rs".as_ref()),
1041 None,
1042 vec![
1043 DiagnosticEntry {
1044 range: PointUtf16::new(0, 15)..PointUtf16::new(0, 15),
1045 diagnostic: Diagnostic {
1046 message: "mismatched types\nexpected `usize`, found `char`"
1047 .to_string(),
1048 severity: DiagnosticSeverity::ERROR,
1049 is_primary: true,
1050 is_disk_based: true,
1051 group_id: 0,
1052 ..Default::default()
1053 },
1054 },
1055 DiagnosticEntry {
1056 range: PointUtf16::new(1, 15)..PointUtf16::new(1, 15),
1057 diagnostic: Diagnostic {
1058 message: "unresolved name `c`".to_string(),
1059 severity: DiagnosticSeverity::ERROR,
1060 is_primary: true,
1061 is_disk_based: true,
1062 group_id: 1,
1063 ..Default::default()
1064 },
1065 },
1066 ],
1067 cx,
1068 )
1069 .unwrap();
1070 });
1071 project.update(&mut cx, |_, cx| {
1072 cx.emit(project::Event::DiagnosticsUpdated(ProjectPath {
1073 worktree_id,
1074 path: Arc::from("/test/consts.rs".as_ref()),
1075 }));
1076 cx.emit(project::Event::DiskBasedDiagnosticsFinished);
1077 });
1078
1079 view.next_notification(&cx).await;
1080 view.update(&mut cx, |view, cx| {
1081 let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
1082
1083 assert_eq!(
1084 editor_blocks(&editor, cx),
1085 [
1086 (0, "path header block".into()),
1087 (2, "diagnostic header".into()),
1088 (7, "diagnostic header".into()),
1089 (12, "path header block".into()),
1090 (14, "diagnostic header".into()),
1091 (27, "diagnostic header".into()),
1092 (36, "collapsed context".into()),
1093 ]
1094 );
1095 assert_eq!(
1096 editor.text(),
1097 concat!(
1098 //
1099 // consts.rs
1100 //
1101 "\n", // filename
1102 "\n", // padding
1103 // diagnostic group 1
1104 "\n", // primary message
1105 "\n", // padding
1106 "const a: i32 = 'a';\n",
1107 "\n", // supporting diagnostic
1108 "const b: i32 = c;\n",
1109 // diagnostic group 2
1110 "\n", // primary message
1111 "\n", // padding
1112 "const a: i32 = 'a';\n",
1113 "const b: i32 = c;\n",
1114 "\n", // supporting diagnostic
1115 //
1116 // main.rs
1117 //
1118 "\n", // filename
1119 "\n", // padding
1120 // diagnostic group 1
1121 "\n", // primary message
1122 "\n", // padding
1123 " let x = vec![];\n",
1124 " let y = vec![];\n",
1125 "\n", // supporting diagnostic
1126 " a(x);\n",
1127 " b(y);\n",
1128 "\n", // supporting diagnostic
1129 " // comment 1\n",
1130 " // comment 2\n",
1131 " c(y);\n",
1132 "\n", // supporting diagnostic
1133 " d(x);\n",
1134 // diagnostic group 2
1135 "\n", // primary message
1136 "\n", // filename
1137 "fn main() {\n",
1138 " let x = vec![];\n",
1139 "\n", // supporting diagnostic
1140 " let y = vec![];\n",
1141 " a(x);\n",
1142 "\n", // supporting diagnostic
1143 " b(y);\n",
1144 "\n", // context ellipsis
1145 " c(y);\n",
1146 " d(x);\n",
1147 "\n", // supporting diagnostic
1148 "}"
1149 )
1150 );
1151 });
1152 }
1153
1154 fn editor_blocks(editor: &EditorSnapshot, cx: &AppContext) -> Vec<(u32, String)> {
1155 editor
1156 .blocks_in_range(0..editor.max_point().row())
1157 .filter_map(|(row, block)| {
1158 block
1159 .render(&BlockContext {
1160 cx,
1161 anchor_x: 0.,
1162 line_number_x: 0.,
1163 })
1164 .name()
1165 .map(|s| (row, s.to_string()))
1166 })
1167 .collect()
1168 }
1169}