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