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, WorktreeId};
18use std::{cmp::Ordering, mem, ops::Range, 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<PathState>,
52 paths_to_update: HashMap<WorktreeId, 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::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 if !workspace.activate_pane_for_item(&buffer, cx) {
202 workspace.activate_next_pane(cx);
203 }
204 let editor = workspace
205 .open_item(buffer, cx)
206 .to_any()
207 .downcast::<Editor>()
208 .unwrap();
209 editor.update(cx, |editor, cx| {
210 editor.select_ranges(ranges, Some(Autoscroll::Center), cx)
211 });
212 }
213 });
214 }
215 }
216
217 fn update_excerpts(&self, paths: BTreeSet<ProjectPath>, cx: &mut ViewContext<Self>) {
218 let project = self.model.read(cx).project.clone();
219 cx.spawn(|this, mut cx| {
220 async move {
221 for path in paths {
222 let buffer = project
223 .update(&mut cx, |project, cx| project.open_buffer(path.clone(), cx))
224 .await?;
225 this.update(&mut cx, |view, cx| view.populate_excerpts(path, buffer, cx))
226 }
227 Result::<_, anyhow::Error>::Ok(())
228 }
229 .log_err()
230 })
231 .detach();
232 }
233
234 fn populate_excerpts(
235 &mut self,
236 path: ProjectPath,
237 buffer: ModelHandle<Buffer>,
238 cx: &mut ViewContext<Self>,
239 ) {
240 let was_empty = self.path_states.is_empty();
241 let snapshot = buffer.read(cx).snapshot();
242 let path_ix = match self.path_states.binary_search_by_key(&&path, |e| &e.path) {
243 Ok(ix) => ix,
244 Err(ix) => {
245 self.path_states.insert(
246 ix,
247 PathState {
248 path: path.clone(),
249 header: None,
250 diagnostic_groups: Default::default(),
251 },
252 );
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]
259 .diagnostic_groups
260 .last()
261 .unwrap();
262 prev_path_last_group.excerpts.last().unwrap().clone()
263 } else {
264 ExcerptId::min()
265 };
266
267 let path_state = &mut self.path_states[path_ix];
268 let mut groups_to_add = Vec::new();
269 let mut group_ixs_to_remove = Vec::new();
270 let mut blocks_to_add = Vec::new();
271 let mut blocks_to_remove = HashSet::default();
272 let mut first_excerpt_id = None;
273 let excerpts_snapshot = self.excerpts.update(cx, |excerpts, excerpts_cx| {
274 let mut old_groups = path_state.diagnostic_groups.iter().enumerate().peekable();
275 let mut new_groups = snapshot
276 .diagnostic_groups()
277 .into_iter()
278 .filter(|group| group.entries[group.primary_ix].diagnostic.is_disk_based)
279 .peekable();
280
281 loop {
282 let mut to_insert = None;
283 let mut to_remove = None;
284 let mut to_keep = None;
285 match (old_groups.peek(), new_groups.peek()) {
286 (None, None) => break,
287 (None, Some(_)) => to_insert = new_groups.next(),
288 (Some(_), None) => to_remove = old_groups.next(),
289 (Some((_, old_group)), Some(new_group)) => {
290 let old_primary = &old_group.primary_diagnostic;
291 let new_primary = &new_group.entries[new_group.primary_ix];
292 match compare_diagnostics(old_primary, new_primary, &snapshot) {
293 Ordering::Less => to_remove = old_groups.next(),
294 Ordering::Equal => {
295 to_keep = old_groups.next();
296 new_groups.next();
297 }
298 Ordering::Greater => to_insert = new_groups.next(),
299 }
300 }
301 }
302
303 if let Some(group) = to_insert {
304 let mut group_state = DiagnosticGroupState {
305 primary_diagnostic: group.entries[group.primary_ix].clone(),
306 primary_excerpt_ix: 0,
307 excerpts: Default::default(),
308 blocks: Default::default(),
309 block_count: 0,
310 };
311 let mut pending_range: Option<(Range<Point>, usize)> = None;
312 let mut is_first_excerpt_for_group = true;
313 for (ix, entry) in group.entries.iter().map(Some).chain([None]).enumerate() {
314 let resolved_entry = entry.map(|e| e.resolve::<Point>(&snapshot));
315 if let Some((range, start_ix)) = &mut pending_range {
316 if let Some(entry) = resolved_entry.as_ref() {
317 if entry.range.start.row
318 <= range.end.row + 1 + CONTEXT_LINE_COUNT * 2
319 {
320 range.end = range.end.max(entry.range.end);
321 continue;
322 }
323 }
324
325 let excerpt_start =
326 Point::new(range.start.row.saturating_sub(CONTEXT_LINE_COUNT), 0);
327 let excerpt_end = snapshot.clip_point(
328 Point::new(range.end.row + CONTEXT_LINE_COUNT, u32::MAX),
329 Bias::Left,
330 );
331 let excerpt_id = excerpts.insert_excerpt_after(
332 &prev_excerpt_id,
333 ExcerptProperties {
334 buffer: &buffer,
335 range: excerpt_start..excerpt_end,
336 },
337 excerpts_cx,
338 );
339
340 prev_excerpt_id = excerpt_id.clone();
341 first_excerpt_id.get_or_insert_with(|| prev_excerpt_id.clone());
342 group_state.excerpts.push(excerpt_id.clone());
343 let header_position = (excerpt_id.clone(), language::Anchor::min());
344
345 if is_first_excerpt_for_group {
346 is_first_excerpt_for_group = false;
347 let primary = &group.entries[group.primary_ix].diagnostic;
348 let mut header = primary.clone();
349 header.message =
350 primary.message.split('\n').next().unwrap().to_string();
351 group_state.block_count += 1;
352 blocks_to_add.push(BlockProperties {
353 position: header_position,
354 height: 2,
355 render: diagnostic_header_renderer(
356 header,
357 true,
358 self.build_settings.clone(),
359 ),
360 disposition: BlockDisposition::Above,
361 });
362 } else {
363 group_state.block_count += 1;
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 blocks_to_add.push(BlockProperties {
383 position: (excerpt_id.clone(), entry.range.start.clone()),
384 height: diagnostic.message.matches('\n').count() as u8 + 1,
385 render: diagnostic_block_renderer(
386 diagnostic,
387 true,
388 self.build_settings.clone(),
389 ),
390 disposition: BlockDisposition::Below,
391 });
392 }
393 }
394
395 pending_range.take();
396 }
397
398 if let Some(entry) = resolved_entry {
399 pending_range = Some((entry.range.clone(), ix));
400 }
401 }
402
403 groups_to_add.push(group_state);
404 } else if let Some((group_ix, group_state)) = to_remove {
405 excerpts.remove_excerpts(group_state.excerpts.iter(), excerpts_cx);
406 group_ixs_to_remove.push(group_ix);
407 blocks_to_remove.extend(group_state.blocks.iter().copied());
408 } else if let Some((_, group)) = to_keep {
409 prev_excerpt_id = group.excerpts.last().unwrap().clone();
410 first_excerpt_id.get_or_insert_with(|| prev_excerpt_id.clone());
411 }
412 }
413
414 excerpts.snapshot(excerpts_cx)
415 });
416
417 self.editor.update(cx, |editor, cx| {
418 blocks_to_remove.extend(path_state.header);
419 editor.remove_blocks(blocks_to_remove, cx);
420 let header_block = first_excerpt_id.map(|excerpt_id| BlockProperties {
421 position: excerpts_snapshot.anchor_in_excerpt(excerpt_id, language::Anchor::min()),
422 height: 2,
423 render: path_header_renderer(buffer, self.build_settings.clone()),
424 disposition: BlockDisposition::Above,
425 });
426 let mut block_ids = editor
427 .insert_blocks(
428 header_block
429 .into_iter()
430 .chain(blocks_to_add.into_iter().map(|block| {
431 let (excerpt_id, text_anchor) = block.position;
432 BlockProperties {
433 position: excerpts_snapshot
434 .anchor_in_excerpt(excerpt_id, text_anchor),
435 height: block.height,
436 render: block.render,
437 disposition: block.disposition,
438 }
439 })),
440 cx,
441 )
442 .into_iter();
443
444 path_state.header = block_ids.next();
445 for group_state in &mut groups_to_add {
446 group_state.blocks = block_ids.by_ref().take(group_state.block_count).collect();
447 }
448 });
449
450 for ix in group_ixs_to_remove.into_iter().rev() {
451 path_state.diagnostic_groups.remove(ix);
452 }
453 path_state.diagnostic_groups.extend(groups_to_add);
454 path_state.diagnostic_groups.sort_unstable_by(|a, b| {
455 let range_a = &a.primary_diagnostic.range;
456 let range_b = &b.primary_diagnostic.range;
457 range_a
458 .start
459 .cmp(&range_b.start, &snapshot)
460 .unwrap()
461 .then_with(|| range_a.end.cmp(&range_b.end, &snapshot).unwrap())
462 });
463
464 if path_state.diagnostic_groups.is_empty() {
465 self.path_states.remove(path_ix);
466 }
467
468 self.editor.update(cx, |editor, cx| {
469 let groups;
470 let mut selections;
471 let new_excerpt_ids_by_selection_id;
472 if was_empty {
473 groups = self.path_states.first()?.diagnostic_groups.as_slice();
474 new_excerpt_ids_by_selection_id = [(0, ExcerptId::min())].into_iter().collect();
475 selections = vec![Selection {
476 id: 0,
477 start: 0,
478 end: 0,
479 reversed: false,
480 goal: SelectionGoal::None,
481 }];
482 } else {
483 groups = self.path_states.get(path_ix)?.diagnostic_groups.as_slice();
484 new_excerpt_ids_by_selection_id = editor.refresh_selections(cx);
485 selections = editor.local_selections::<usize>(cx);
486 }
487
488 // If any selection has lost its position, move it to start of the next primary diagnostic.
489 for selection in &mut selections {
490 if let Some(new_excerpt_id) = new_excerpt_ids_by_selection_id.get(&selection.id) {
491 let group_ix = match groups.binary_search_by(|probe| {
492 probe.excerpts.last().unwrap().cmp(&new_excerpt_id)
493 }) {
494 Ok(ix) | Err(ix) => ix,
495 };
496 if let Some(group) = groups.get(group_ix) {
497 let offset = excerpts_snapshot
498 .anchor_in_excerpt(
499 group.excerpts[group.primary_excerpt_ix].clone(),
500 group.primary_diagnostic.range.start.clone(),
501 )
502 .to_offset(&excerpts_snapshot);
503 selection.start = offset;
504 selection.end = offset;
505 }
506 }
507 }
508 editor.update_selections(selections, None, cx);
509 Some(())
510 });
511
512 if self.path_states.is_empty() {
513 if self.editor.is_focused(cx) {
514 cx.focus_self();
515 }
516 } else {
517 if cx.handle().is_focused(cx) {
518 cx.focus(&self.editor);
519 }
520 }
521 cx.notify();
522 }
523}
524
525impl workspace::Item for ProjectDiagnostics {
526 type View = ProjectDiagnosticsEditor;
527
528 fn build_view(
529 handle: ModelHandle<Self>,
530 workspace: &Workspace,
531 cx: &mut ViewContext<Self::View>,
532 ) -> Self::View {
533 ProjectDiagnosticsEditor::new(handle, workspace.weak_handle(), workspace.settings(), cx)
534 }
535
536 fn project_path(&self) -> Option<project::ProjectPath> {
537 None
538 }
539}
540
541impl workspace::ItemView for ProjectDiagnosticsEditor {
542 type ItemHandle = ModelHandle<ProjectDiagnostics>;
543
544 fn item_handle(&self, _: &AppContext) -> Self::ItemHandle {
545 self.model.clone()
546 }
547
548 fn title(&self, _: &AppContext) -> String {
549 "Project Diagnostics".to_string()
550 }
551
552 fn project_path(&self, _: &AppContext) -> Option<project::ProjectPath> {
553 None
554 }
555
556 fn is_dirty(&self, cx: &AppContext) -> bool {
557 self.excerpts.read(cx).read(cx).is_dirty()
558 }
559
560 fn has_conflict(&self, cx: &AppContext) -> bool {
561 self.excerpts.read(cx).read(cx).has_conflict()
562 }
563
564 fn can_save(&self, _: &AppContext) -> bool {
565 true
566 }
567
568 fn save(&mut self, cx: &mut ViewContext<Self>) -> Result<Task<Result<()>>> {
569 self.excerpts.update(cx, |excerpts, cx| excerpts.save(cx))
570 }
571
572 fn can_save_as(&self, _: &AppContext) -> bool {
573 false
574 }
575
576 fn save_as(
577 &mut self,
578 _: ModelHandle<project::Worktree>,
579 _: &std::path::Path,
580 _: &mut ViewContext<Self>,
581 ) -> Task<Result<()>> {
582 unreachable!()
583 }
584
585 fn should_activate_item_on_event(event: &Self::Event) -> bool {
586 Editor::should_activate_item_on_event(event)
587 }
588
589 fn should_update_tab_on_event(event: &Event) -> bool {
590 matches!(
591 event,
592 Event::Saved | Event::Dirtied | Event::FileHandleChanged
593 )
594 }
595}
596
597fn path_header_renderer(buffer: ModelHandle<Buffer>, build_settings: BuildSettings) -> RenderBlock {
598 Arc::new(move |cx| {
599 let settings = build_settings(cx);
600 let file_path = if let Some(file) = buffer.read(&**cx).file() {
601 file.path().to_string_lossy().to_string()
602 } else {
603 "untitled".to_string()
604 };
605 Label::new(file_path, settings.style.text.clone())
606 .aligned()
607 .left()
608 .contained()
609 .with_padding_left(cx.line_number_x)
610 .expanded()
611 .boxed()
612 })
613}
614
615fn diagnostic_header_renderer(
616 diagnostic: Diagnostic,
617 is_valid: bool,
618 build_settings: BuildSettings,
619) -> RenderBlock {
620 Arc::new(move |cx| {
621 let settings = build_settings(cx);
622 let mut text_style = settings.style.text.clone();
623 let diagnostic_style = diagnostic_style(diagnostic.severity, is_valid, &settings.style);
624 text_style.color = diagnostic_style.text;
625 Text::new(diagnostic.message.clone(), text_style)
626 .with_soft_wrap(false)
627 .aligned()
628 .left()
629 .contained()
630 .with_style(diagnostic_style.header)
631 .with_padding_left(cx.line_number_x)
632 .expanded()
633 .boxed()
634 })
635}
636
637fn context_header_renderer(build_settings: BuildSettings) -> RenderBlock {
638 Arc::new(move |cx| {
639 let settings = build_settings(cx);
640 let text_style = settings.style.text.clone();
641 Label::new("…".to_string(), text_style)
642 .contained()
643 .with_padding_left(cx.line_number_x)
644 .boxed()
645 })
646}
647
648fn compare_diagnostics<L: language::ToOffset, R: language::ToOffset>(
649 lhs: &DiagnosticEntry<L>,
650 rhs: &DiagnosticEntry<R>,
651 snapshot: &language::BufferSnapshot,
652) -> Ordering {
653 lhs.range
654 .start
655 .to_offset(&snapshot)
656 .cmp(&rhs.range.start.to_offset(snapshot))
657 .then_with(|| {
658 lhs.range
659 .end
660 .to_offset(&snapshot)
661 .cmp(&rhs.range.end.to_offset(snapshot))
662 })
663 .then_with(|| lhs.diagnostic.message.cmp(&rhs.diagnostic.message))
664}
665
666#[cfg(test)]
667mod tests {
668 use super::*;
669 use client::{http::ServerResponse, test::FakeHttpClient, Client, UserStore};
670 use editor::DisplayPoint;
671 use gpui::TestAppContext;
672 use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, LanguageRegistry, PointUtf16};
673 use project::{worktree, FakeFs};
674 use serde_json::json;
675 use std::sync::Arc;
676 use unindent::Unindent as _;
677 use workspace::WorkspaceParams;
678
679 #[gpui::test]
680 async fn test_diagnostics(mut cx: TestAppContext) {
681 let workspace_params = cx.update(WorkspaceParams::test);
682 let settings = workspace_params.settings.clone();
683 let http_client = FakeHttpClient::new(|_| async move { Ok(ServerResponse::new(404)) });
684 let client = Client::new(http_client.clone());
685 let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
686 let fs = Arc::new(FakeFs::new());
687
688 let project = cx.update(|cx| {
689 Project::local(
690 client.clone(),
691 user_store,
692 Arc::new(LanguageRegistry::new()),
693 fs.clone(),
694 cx,
695 )
696 });
697
698 fs.insert_tree(
699 "/test",
700 json!({
701 "a.rs": "
702 const a: i32 = 'a';
703 ".unindent(),
704
705 "main.rs": "
706 fn main() {
707 let x = vec![];
708 let y = vec![];
709 a(x);
710 b(y);
711 // comment 1
712 // comment 2
713 c(y);
714 d(x);
715 }
716 "
717 .unindent(),
718 }),
719 )
720 .await;
721
722 let worktree = project
723 .update(&mut cx, |project, cx| {
724 project.add_local_worktree("/test", cx)
725 })
726 .await
727 .unwrap();
728
729 worktree.update(&mut cx, |worktree, cx| {
730 worktree
731 .update_diagnostic_entries(
732 Arc::from("/test/main.rs".as_ref()),
733 None,
734 vec![
735 DiagnosticEntry {
736 range: PointUtf16::new(1, 8)..PointUtf16::new(1, 9),
737 diagnostic: Diagnostic {
738 message:
739 "move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait"
740 .to_string(),
741 severity: DiagnosticSeverity::INFORMATION,
742 is_primary: false,
743 is_disk_based: true,
744 group_id: 1,
745 ..Default::default()
746 },
747 },
748 DiagnosticEntry {
749 range: PointUtf16::new(2, 8)..PointUtf16::new(2, 9),
750 diagnostic: Diagnostic {
751 message:
752 "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait"
753 .to_string(),
754 severity: DiagnosticSeverity::INFORMATION,
755 is_primary: false,
756 is_disk_based: true,
757 group_id: 0,
758 ..Default::default()
759 },
760 },
761 DiagnosticEntry {
762 range: PointUtf16::new(3, 6)..PointUtf16::new(3, 7),
763 diagnostic: Diagnostic {
764 message: "value moved here".to_string(),
765 severity: DiagnosticSeverity::INFORMATION,
766 is_primary: false,
767 is_disk_based: true,
768 group_id: 1,
769 ..Default::default()
770 },
771 },
772 DiagnosticEntry {
773 range: PointUtf16::new(4, 6)..PointUtf16::new(4, 7),
774 diagnostic: Diagnostic {
775 message: "value moved here".to_string(),
776 severity: DiagnosticSeverity::INFORMATION,
777 is_primary: false,
778 is_disk_based: true,
779 group_id: 0,
780 ..Default::default()
781 },
782 },
783 DiagnosticEntry {
784 range: PointUtf16::new(7, 6)..PointUtf16::new(7, 7),
785 diagnostic: Diagnostic {
786 message: "use of moved value\nvalue used here after move".to_string(),
787 severity: DiagnosticSeverity::ERROR,
788 is_primary: true,
789 is_disk_based: true,
790 group_id: 0,
791 ..Default::default()
792 },
793 },
794 DiagnosticEntry {
795 range: PointUtf16::new(8, 6)..PointUtf16::new(8, 7),
796 diagnostic: Diagnostic {
797 message: "use of moved value\nvalue used here after move".to_string(),
798 severity: DiagnosticSeverity::ERROR,
799 is_primary: true,
800 is_disk_based: true,
801 group_id: 1,
802 ..Default::default()
803 },
804 },
805 ],
806 cx,
807 )
808 .unwrap();
809 });
810
811 let model = cx.add_model(|_| ProjectDiagnostics::new(project.clone()));
812 let workspace = cx.add_view(0, |cx| Workspace::new(&workspace_params, cx));
813
814 let view = cx.add_view(0, |cx| {
815 ProjectDiagnosticsEditor::new(model, workspace.downgrade(), settings, cx)
816 });
817
818 view.condition(&mut cx, |view, cx| view.text(cx).contains("fn main()"))
819 .await;
820
821 view.update(&mut cx, |view, cx| {
822 let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
823
824 assert_eq!(
825 editor.text(),
826 concat!(
827 //
828 // main.rs, diagnostic group 1
829 //
830 "\n", // padding
831 "\n", // primary message
832 "\n", // filename
833 " let x = vec![];\n",
834 " let y = vec![];\n",
835 "\n", // supporting diagnostic
836 " a(x);\n",
837 " b(y);\n",
838 "\n", // supporting diagnostic
839 " // comment 1\n",
840 " // comment 2\n",
841 " c(y);\n",
842 "\n", // supporting diagnostic
843 " d(x);\n",
844 //
845 // main.rs, diagnostic group 2
846 //
847 "\n", // padding
848 "\n", // primary message
849 "\n", // filename
850 "fn main() {\n",
851 " let x = vec![];\n",
852 "\n", // supporting diagnostic
853 " let y = vec![];\n",
854 " a(x);\n",
855 "\n", // supporting diagnostic
856 " b(y);\n",
857 "\n", // context ellipsis
858 " c(y);\n",
859 " d(x);\n",
860 "\n", // supporting diagnostic
861 "}"
862 )
863 );
864
865 view.editor.update(cx, |editor, cx| {
866 assert_eq!(
867 editor.selected_display_ranges(cx),
868 [DisplayPoint::new(11, 6)..DisplayPoint::new(11, 6)]
869 );
870 });
871 });
872
873 worktree.update(&mut cx, |worktree, cx| {
874 worktree
875 .update_diagnostic_entries(
876 Arc::from("/test/a.rs".as_ref()),
877 None,
878 vec![DiagnosticEntry {
879 range: PointUtf16::new(0, 15)..PointUtf16::new(0, 15),
880 diagnostic: Diagnostic {
881 message: "mismatched types\nexpected `usize`, found `char`".to_string(),
882 severity: DiagnosticSeverity::ERROR,
883 is_primary: true,
884 is_disk_based: true,
885 group_id: 0,
886 ..Default::default()
887 },
888 }],
889 cx,
890 )
891 .unwrap();
892 cx.emit(worktree::Event::DiskBasedDiagnosticsUpdated);
893 });
894
895 view.condition(&mut cx, |view, cx| view.text(cx).contains("const a"))
896 .await;
897
898 view.update(&mut cx, |view, cx| {
899 let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
900
901 assert_eq!(
902 editor.text(),
903 concat!(
904 //
905 // a.rs
906 //
907 "\n", // padding
908 "\n", // primary message
909 "\n", // filename
910 "const a: i32 = 'a';\n",
911 "\n", // supporting diagnostic
912 "\n", // context line
913 //
914 // main.rs, diagnostic group 1
915 //
916 "\n", // padding
917 "\n", // primary message
918 "\n", // filename
919 " let x = vec![];\n",
920 " let y = vec![];\n",
921 "\n", // supporting diagnostic
922 " a(x);\n",
923 " b(y);\n",
924 "\n", // supporting diagnostic
925 " // comment 1\n",
926 " // comment 2\n",
927 " c(y);\n",
928 "\n", // supporting diagnostic
929 " d(x);\n",
930 //
931 // main.rs, diagnostic group 2
932 //
933 "\n", // padding
934 "\n", // primary message
935 "\n", // filename
936 "fn main() {\n",
937 " let x = vec![];\n",
938 "\n", // supporting diagnostic
939 " let y = vec![];\n",
940 " a(x);\n",
941 "\n", // supporting diagnostic
942 " b(y);\n",
943 "\n", // context ellipsis
944 " c(y);\n",
945 " d(x);\n",
946 "\n", // supporting diagnostic
947 "}"
948 )
949 );
950 });
951 }
952}