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