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 unindent::Unindent as _;
729 use workspace::WorkspaceParams;
730
731 #[gpui::test]
732 async fn test_diagnostics(mut cx: TestAppContext) {
733 let params = cx.update(WorkspaceParams::test);
734 let project = params.project.clone();
735 let workspace = cx.add_view(0, |cx| Workspace::new(¶ms, cx));
736
737 params
738 .fs
739 .as_fake()
740 .insert_tree(
741 "/test",
742 json!({
743 "consts.rs": "
744 const a: i32 = 'a';
745 const b: i32 = c;
746 "
747 .unindent(),
748
749 "main.rs": "
750 fn main() {
751 let x = vec![];
752 let y = vec![];
753 a(x);
754 b(y);
755 // comment 1
756 // comment 2
757 c(y);
758 d(x);
759 }
760 "
761 .unindent(),
762 }),
763 )
764 .await;
765
766 project
767 .update(&mut cx, |project, cx| {
768 project.find_or_create_local_worktree("/test", false, cx)
769 })
770 .await
771 .unwrap();
772
773 // Create some diagnostics
774 project.update(&mut cx, |project, cx| {
775 project
776 .update_diagnostic_entries(
777 PathBuf::from("/test/main.rs"),
778 None,
779 vec![
780 DiagnosticEntry {
781 range: PointUtf16::new(1, 8)..PointUtf16::new(1, 9),
782 diagnostic: Diagnostic {
783 message:
784 "move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait"
785 .to_string(),
786 severity: DiagnosticSeverity::INFORMATION,
787 is_primary: false,
788 is_disk_based: true,
789 group_id: 1,
790 ..Default::default()
791 },
792 },
793 DiagnosticEntry {
794 range: PointUtf16::new(2, 8)..PointUtf16::new(2, 9),
795 diagnostic: Diagnostic {
796 message:
797 "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait"
798 .to_string(),
799 severity: DiagnosticSeverity::INFORMATION,
800 is_primary: false,
801 is_disk_based: true,
802 group_id: 0,
803 ..Default::default()
804 },
805 },
806 DiagnosticEntry {
807 range: PointUtf16::new(3, 6)..PointUtf16::new(3, 7),
808 diagnostic: Diagnostic {
809 message: "value moved here".to_string(),
810 severity: DiagnosticSeverity::INFORMATION,
811 is_primary: false,
812 is_disk_based: true,
813 group_id: 1,
814 ..Default::default()
815 },
816 },
817 DiagnosticEntry {
818 range: PointUtf16::new(4, 6)..PointUtf16::new(4, 7),
819 diagnostic: Diagnostic {
820 message: "value moved here".to_string(),
821 severity: DiagnosticSeverity::INFORMATION,
822 is_primary: false,
823 is_disk_based: true,
824 group_id: 0,
825 ..Default::default()
826 },
827 },
828 DiagnosticEntry {
829 range: PointUtf16::new(7, 6)..PointUtf16::new(7, 7),
830 diagnostic: Diagnostic {
831 message: "use of moved value\nvalue used here after move".to_string(),
832 severity: DiagnosticSeverity::ERROR,
833 is_primary: true,
834 is_disk_based: true,
835 group_id: 0,
836 ..Default::default()
837 },
838 },
839 DiagnosticEntry {
840 range: PointUtf16::new(8, 6)..PointUtf16::new(8, 7),
841 diagnostic: Diagnostic {
842 message: "use of moved value\nvalue used here after move".to_string(),
843 severity: DiagnosticSeverity::ERROR,
844 is_primary: true,
845 is_disk_based: true,
846 group_id: 1,
847 ..Default::default()
848 },
849 },
850 ],
851 cx,
852 )
853 .unwrap();
854 });
855
856 // Open the project diagnostics view while there are already diagnostics.
857 let model = cx.add_model(|_| ProjectDiagnostics::new(project.clone()));
858 let view = cx.add_view(0, |cx| {
859 ProjectDiagnosticsEditor::new(model, workspace.downgrade(), params.settings, cx)
860 });
861
862 view.next_notification(&cx).await;
863 view.update(&mut cx, |view, cx| {
864 let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
865
866 assert_eq!(
867 editor_blocks(&editor, cx),
868 [
869 (0, "path header block".into()),
870 (2, "diagnostic header".into()),
871 (15, "diagnostic header".into()),
872 (24, "collapsed context".into()),
873 ]
874 );
875 assert_eq!(
876 editor.text(),
877 concat!(
878 //
879 // main.rs
880 //
881 "\n", // filename
882 "\n", // padding
883 // diagnostic group 1
884 "\n", // primary message
885 "\n", // padding
886 " let x = vec![];\n",
887 " let y = vec![];\n",
888 "\n", // supporting diagnostic
889 " a(x);\n",
890 " b(y);\n",
891 "\n", // supporting diagnostic
892 " // comment 1\n",
893 " // comment 2\n",
894 " c(y);\n",
895 "\n", // supporting diagnostic
896 " d(x);\n",
897 // diagnostic group 2
898 "\n", // primary message
899 "\n", // padding
900 "fn main() {\n",
901 " let x = vec![];\n",
902 "\n", // supporting diagnostic
903 " let y = vec![];\n",
904 " a(x);\n",
905 "\n", // supporting diagnostic
906 " b(y);\n",
907 "\n", // context ellipsis
908 " c(y);\n",
909 " d(x);\n",
910 "\n", // supporting diagnostic
911 "}"
912 )
913 );
914
915 // Cursor is at the first diagnostic
916 view.editor.update(cx, |editor, cx| {
917 assert_eq!(
918 editor.selected_display_ranges(cx),
919 [DisplayPoint::new(12, 6)..DisplayPoint::new(12, 6)]
920 );
921 });
922 });
923
924 // Diagnostics are added for another earlier path.
925 project.update(&mut cx, |project, cx| {
926 project.disk_based_diagnostics_started(cx);
927 project
928 .update_diagnostic_entries(
929 PathBuf::from("/test/consts.rs"),
930 None,
931 vec![DiagnosticEntry {
932 range: PointUtf16::new(0, 15)..PointUtf16::new(0, 15),
933 diagnostic: Diagnostic {
934 message: "mismatched types\nexpected `usize`, found `char`".to_string(),
935 severity: DiagnosticSeverity::ERROR,
936 is_primary: true,
937 is_disk_based: true,
938 group_id: 0,
939 ..Default::default()
940 },
941 }],
942 cx,
943 )
944 .unwrap();
945 project.disk_based_diagnostics_finished(cx);
946 });
947
948 view.next_notification(&cx).await;
949 view.update(&mut cx, |view, cx| {
950 let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
951
952 assert_eq!(
953 editor_blocks(&editor, cx),
954 [
955 (0, "path header block".into()),
956 (2, "diagnostic header".into()),
957 (7, "path header block".into()),
958 (9, "diagnostic header".into()),
959 (22, "diagnostic header".into()),
960 (31, "collapsed context".into()),
961 ]
962 );
963 assert_eq!(
964 editor.text(),
965 concat!(
966 //
967 // consts.rs
968 //
969 "\n", // filename
970 "\n", // padding
971 // diagnostic group 1
972 "\n", // primary message
973 "\n", // padding
974 "const a: i32 = 'a';\n",
975 "\n", // supporting diagnostic
976 "const b: i32 = c;\n",
977 //
978 // main.rs
979 //
980 "\n", // filename
981 "\n", // padding
982 // diagnostic group 1
983 "\n", // primary message
984 "\n", // padding
985 " let x = vec![];\n",
986 " let y = vec![];\n",
987 "\n", // supporting diagnostic
988 " a(x);\n",
989 " b(y);\n",
990 "\n", // supporting diagnostic
991 " // comment 1\n",
992 " // comment 2\n",
993 " c(y);\n",
994 "\n", // supporting diagnostic
995 " d(x);\n",
996 // diagnostic group 2
997 "\n", // primary message
998 "\n", // filename
999 "fn main() {\n",
1000 " let x = vec![];\n",
1001 "\n", // supporting diagnostic
1002 " let y = vec![];\n",
1003 " a(x);\n",
1004 "\n", // supporting diagnostic
1005 " b(y);\n",
1006 "\n", // context ellipsis
1007 " c(y);\n",
1008 " d(x);\n",
1009 "\n", // supporting diagnostic
1010 "}"
1011 )
1012 );
1013
1014 // Cursor keeps its position.
1015 view.editor.update(cx, |editor, cx| {
1016 assert_eq!(
1017 editor.selected_display_ranges(cx),
1018 [DisplayPoint::new(19, 6)..DisplayPoint::new(19, 6)]
1019 );
1020 });
1021 });
1022
1023 // Diagnostics are added to the first path
1024 project.update(&mut cx, |project, cx| {
1025 project.disk_based_diagnostics_started(cx);
1026 project
1027 .update_diagnostic_entries(
1028 PathBuf::from("/test/consts.rs"),
1029 None,
1030 vec![
1031 DiagnosticEntry {
1032 range: PointUtf16::new(0, 15)..PointUtf16::new(0, 15),
1033 diagnostic: Diagnostic {
1034 message: "mismatched types\nexpected `usize`, found `char`"
1035 .to_string(),
1036 severity: DiagnosticSeverity::ERROR,
1037 is_primary: true,
1038 is_disk_based: true,
1039 group_id: 0,
1040 ..Default::default()
1041 },
1042 },
1043 DiagnosticEntry {
1044 range: PointUtf16::new(1, 15)..PointUtf16::new(1, 15),
1045 diagnostic: Diagnostic {
1046 message: "unresolved name `c`".to_string(),
1047 severity: DiagnosticSeverity::ERROR,
1048 is_primary: true,
1049 is_disk_based: true,
1050 group_id: 1,
1051 ..Default::default()
1052 },
1053 },
1054 ],
1055 cx,
1056 )
1057 .unwrap();
1058 project.disk_based_diagnostics_finished(cx);
1059 });
1060
1061 view.next_notification(&cx).await;
1062 view.update(&mut cx, |view, cx| {
1063 let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
1064
1065 assert_eq!(
1066 editor_blocks(&editor, cx),
1067 [
1068 (0, "path header block".into()),
1069 (2, "diagnostic header".into()),
1070 (7, "diagnostic header".into()),
1071 (12, "path header block".into()),
1072 (14, "diagnostic header".into()),
1073 (27, "diagnostic header".into()),
1074 (36, "collapsed context".into()),
1075 ]
1076 );
1077 assert_eq!(
1078 editor.text(),
1079 concat!(
1080 //
1081 // consts.rs
1082 //
1083 "\n", // filename
1084 "\n", // padding
1085 // diagnostic group 1
1086 "\n", // primary message
1087 "\n", // padding
1088 "const a: i32 = 'a';\n",
1089 "\n", // supporting diagnostic
1090 "const b: i32 = c;\n",
1091 // diagnostic group 2
1092 "\n", // primary message
1093 "\n", // padding
1094 "const a: i32 = 'a';\n",
1095 "const b: i32 = c;\n",
1096 "\n", // supporting diagnostic
1097 //
1098 // main.rs
1099 //
1100 "\n", // filename
1101 "\n", // padding
1102 // diagnostic group 1
1103 "\n", // primary message
1104 "\n", // padding
1105 " let x = vec![];\n",
1106 " let y = vec![];\n",
1107 "\n", // supporting diagnostic
1108 " a(x);\n",
1109 " b(y);\n",
1110 "\n", // supporting diagnostic
1111 " // comment 1\n",
1112 " // comment 2\n",
1113 " c(y);\n",
1114 "\n", // supporting diagnostic
1115 " d(x);\n",
1116 // diagnostic group 2
1117 "\n", // primary message
1118 "\n", // filename
1119 "fn main() {\n",
1120 " let x = vec![];\n",
1121 "\n", // supporting diagnostic
1122 " let y = vec![];\n",
1123 " a(x);\n",
1124 "\n", // supporting diagnostic
1125 " b(y);\n",
1126 "\n", // context ellipsis
1127 " c(y);\n",
1128 " d(x);\n",
1129 "\n", // supporting diagnostic
1130 "}"
1131 )
1132 );
1133 });
1134 }
1135
1136 fn editor_blocks(editor: &EditorSnapshot, cx: &AppContext) -> Vec<(u32, String)> {
1137 editor
1138 .blocks_in_range(0..editor.max_point().row())
1139 .filter_map(|(row, block)| {
1140 block
1141 .render(&BlockContext {
1142 cx,
1143 anchor_x: 0.,
1144 line_number_x: 0.,
1145 })
1146 .name()
1147 .map(|s| (row, s.to_string()))
1148 })
1149 .collect()
1150 }
1151}