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