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