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