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