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, AnyViewHandle, AppContext, Entity, ModelHandle,
13 MutableAppContext, RenderContext, Task, View, ViewContext, ViewHandle, WeakViewHandle,
14};
15use language::{Bias, Buffer, Diagnostic, DiagnosticEntry, Point, Selection, SelectionGoal};
16use postage::watch;
17use project::{Project, ProjectPath};
18use std::{any::TypeId, cmp::Ordering, mem, ops::Range, path::PathBuf, rc::Rc, sync::Arc};
19use util::TryFutureExt;
20use workspace::{NavHistory, 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: 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::DiskBasedDiagnosticsFinished => {
123 let paths = mem::take(&mut this.paths_to_update);
124 this.update_excerpts(paths, cx);
125 }
126 project::Event::DiagnosticsUpdated(path) => {
127 this.paths_to_update.insert(path.clone());
128 }
129 _ => {}
130 })
131 .detach();
132
133 let excerpts = cx.add_model(|cx| MultiBuffer::new(project.read(cx).replica_id()));
134 let build_settings = editor::settings_builder(excerpts.downgrade(), settings.clone());
135 let editor =
136 cx.add_view(|cx| Editor::for_buffer(excerpts.clone(), build_settings.clone(), cx));
137 cx.subscribe(&editor, |_, _, event, cx| cx.emit(*event))
138 .detach();
139
140 let paths_to_update = project
141 .read(cx)
142 .diagnostic_summaries(cx)
143 .map(|e| e.0)
144 .collect();
145 let this = Self {
146 model,
147 workspace,
148 excerpts,
149 editor,
150 build_settings,
151 settings,
152 path_states: Default::default(),
153 paths_to_update: Default::default(),
154 };
155 this.update_excerpts(paths_to_update, cx);
156 this
157 }
158
159 fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext<Workspace>) {
160 if let Some(existing) = workspace.item_of_type::<ProjectDiagnostics>(cx) {
161 workspace.activate_item(&existing, cx);
162 } else {
163 let diagnostics =
164 cx.add_model(|_| ProjectDiagnostics::new(workspace.project().clone()));
165 workspace.open_item(diagnostics, cx);
166 }
167 }
168
169 fn open_excerpts(&mut self, _: &OpenExcerpts, cx: &mut ViewContext<Self>) {
170 if let Some(workspace) = self.workspace.upgrade(cx) {
171 let editor = self.editor.read(cx);
172 let excerpts = self.excerpts.read(cx);
173 let mut new_selections_by_buffer = HashMap::default();
174
175 for selection in editor.local_selections::<usize>(cx) {
176 for (buffer, mut range) in
177 excerpts.excerpted_buffers(selection.start..selection.end, cx)
178 {
179 if selection.reversed {
180 mem::swap(&mut range.start, &mut range.end);
181 }
182 new_selections_by_buffer
183 .entry(buffer)
184 .or_insert(Vec::new())
185 .push(range)
186 }
187 }
188
189 workspace.update(cx, |workspace, cx| {
190 for (buffer, ranges) in new_selections_by_buffer {
191 let buffer = BufferItemHandle(buffer);
192 if !workspace.activate_pane_for_item(&buffer, cx) {
193 workspace.activate_next_pane(cx);
194 }
195 let editor = workspace
196 .open_item(buffer, cx)
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: BTreeSet<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.clone(), cx))
214 .await?;
215 this.update(&mut cx, |view, cx| view.populate_excerpts(path, buffer, cx))
216 }
217 Result::<_, anyhow::Error>::Ok(())
218 }
219 .log_err()
220 })
221 .detach();
222 }
223
224 fn populate_excerpts(
225 &mut self,
226 path: ProjectPath,
227 buffer: ModelHandle<Buffer>,
228 cx: &mut ViewContext<Self>,
229 ) {
230 let was_empty = self.path_states.is_empty();
231 let snapshot = buffer.read(cx).snapshot();
232 let path_ix = match self.path_states.binary_search_by_key(&&path, |e| &e.path) {
233 Ok(ix) => ix,
234 Err(ix) => {
235 self.path_states.insert(
236 ix,
237 PathState {
238 path: path.clone(),
239 header: None,
240 diagnostic_groups: Default::default(),
241 },
242 );
243 ix
244 }
245 };
246
247 let mut prev_excerpt_id = if path_ix > 0 {
248 let prev_path_last_group = &self.path_states[path_ix - 1]
249 .diagnostic_groups
250 .last()
251 .unwrap();
252 prev_path_last_group.excerpts.last().unwrap().clone()
253 } else {
254 ExcerptId::min()
255 };
256
257 let path_state = &mut self.path_states[path_ix];
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 first_excerpt_id = None;
263 let excerpts_snapshot = self.excerpts.update(cx, |excerpts, excerpts_cx| {
264 let mut old_groups = path_state.diagnostic_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_remove = 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_remove = 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_remove = 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 primary_excerpt_ix: 0,
297 excerpts: Default::default(),
298 blocks: Default::default(),
299 block_count: 0,
300 };
301 let mut pending_range: Option<(Range<Point>, usize)> = None;
302 let mut is_first_excerpt_for_group = true;
303 for (ix, entry) in group.entries.iter().map(Some).chain([None]).enumerate() {
304 let resolved_entry = entry.map(|e| e.resolve::<Point>(&snapshot));
305 if let Some((range, start_ix)) = &mut pending_range {
306 if let Some(entry) = resolved_entry.as_ref() {
307 if entry.range.start.row
308 <= range.end.row + 1 + CONTEXT_LINE_COUNT * 2
309 {
310 range.end = range.end.max(entry.range.end);
311 continue;
312 }
313 }
314
315 let excerpt_start =
316 Point::new(range.start.row.saturating_sub(CONTEXT_LINE_COUNT), 0);
317 let excerpt_end = snapshot.clip_point(
318 Point::new(range.end.row + CONTEXT_LINE_COUNT, u32::MAX),
319 Bias::Left,
320 );
321 let excerpt_id = excerpts.insert_excerpt_after(
322 &prev_excerpt_id,
323 ExcerptProperties {
324 buffer: &buffer,
325 range: excerpt_start..excerpt_end,
326 },
327 excerpts_cx,
328 );
329
330 prev_excerpt_id = excerpt_id.clone();
331 first_excerpt_id.get_or_insert_with(|| prev_excerpt_id.clone());
332 group_state.excerpts.push(excerpt_id.clone());
333 let header_position = (excerpt_id.clone(), language::Anchor::min());
334
335 if is_first_excerpt_for_group {
336 is_first_excerpt_for_group = false;
337 let primary = &group.entries[group.primary_ix].diagnostic;
338 let mut header = primary.clone();
339 header.message =
340 primary.message.split('\n').next().unwrap().to_string();
341 group_state.block_count += 1;
342 blocks_to_add.push(BlockProperties {
343 position: header_position,
344 height: 2,
345 render: diagnostic_header_renderer(
346 header,
347 true,
348 self.build_settings.clone(),
349 ),
350 disposition: BlockDisposition::Above,
351 });
352 } else {
353 group_state.block_count += 1;
354 blocks_to_add.push(BlockProperties {
355 position: header_position,
356 height: 1,
357 render: context_header_renderer(self.build_settings.clone()),
358 disposition: BlockDisposition::Above,
359 });
360 }
361
362 for entry in &group.entries[*start_ix..ix] {
363 let mut diagnostic = entry.diagnostic.clone();
364 if diagnostic.is_primary {
365 group_state.primary_excerpt_ix = group_state.excerpts.len() - 1;
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 blocks_to_add.push(BlockProperties {
373 position: (excerpt_id.clone(), entry.range.start.clone()),
374 height: diagnostic.message.matches('\n').count() as u8 + 1,
375 render: diagnostic_block_renderer(
376 diagnostic,
377 true,
378 self.build_settings.clone(),
379 ),
380 disposition: BlockDisposition::Below,
381 });
382 }
383 }
384
385 pending_range.take();
386 }
387
388 if let Some(entry) = resolved_entry {
389 pending_range = Some((entry.range.clone(), ix));
390 }
391 }
392
393 groups_to_add.push(group_state);
394 } else if let Some((group_ix, group_state)) = to_remove {
395 excerpts.remove_excerpts(group_state.excerpts.iter(), excerpts_cx);
396 group_ixs_to_remove.push(group_ix);
397 blocks_to_remove.extend(group_state.blocks.iter().copied());
398 } else if let Some((_, group)) = to_keep {
399 prev_excerpt_id = group.excerpts.last().unwrap().clone();
400 first_excerpt_id.get_or_insert_with(|| prev_excerpt_id.clone());
401 }
402 }
403
404 excerpts.snapshot(excerpts_cx)
405 });
406
407 self.editor.update(cx, |editor, cx| {
408 blocks_to_remove.extend(path_state.header);
409 editor.remove_blocks(blocks_to_remove, cx);
410 let header_block = first_excerpt_id.map(|excerpt_id| BlockProperties {
411 position: excerpts_snapshot.anchor_in_excerpt(excerpt_id, language::Anchor::min()),
412 height: 2,
413 render: path_header_renderer(buffer, self.build_settings.clone()),
414 disposition: BlockDisposition::Above,
415 });
416 let block_ids = editor.insert_blocks(
417 blocks_to_add
418 .into_iter()
419 .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 .chain(header_block.into_iter()),
429 cx,
430 );
431
432 let mut block_ids = block_ids.into_iter();
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 path_state.header = block_ids.next();
437 });
438
439 for ix in group_ixs_to_remove.into_iter().rev() {
440 path_state.diagnostic_groups.remove(ix);
441 }
442 path_state.diagnostic_groups.extend(groups_to_add);
443 path_state.diagnostic_groups.sort_unstable_by(|a, b| {
444 let range_a = &a.primary_diagnostic.range;
445 let range_b = &b.primary_diagnostic.range;
446 range_a
447 .start
448 .cmp(&range_b.start, &snapshot)
449 .unwrap()
450 .then_with(|| range_a.end.cmp(&range_b.end, &snapshot).unwrap())
451 });
452
453 if path_state.diagnostic_groups.is_empty() {
454 self.path_states.remove(path_ix);
455 }
456
457 self.editor.update(cx, |editor, cx| {
458 let groups;
459 let mut selections;
460 let new_excerpt_ids_by_selection_id;
461 if was_empty {
462 groups = self.path_states.first()?.diagnostic_groups.as_slice();
463 new_excerpt_ids_by_selection_id = [(0, ExcerptId::min())].into_iter().collect();
464 selections = vec![Selection {
465 id: 0,
466 start: 0,
467 end: 0,
468 reversed: false,
469 goal: SelectionGoal::None,
470 }];
471 } else {
472 groups = self.path_states.get(path_ix)?.diagnostic_groups.as_slice();
473 new_excerpt_ids_by_selection_id = editor.refresh_selections(cx);
474 selections = editor.local_selections::<usize>(cx);
475 }
476
477 // If any selection has lost its position, move it to start of the next primary diagnostic.
478 for selection in &mut selections {
479 if let Some(new_excerpt_id) = new_excerpt_ids_by_selection_id.get(&selection.id) {
480 let group_ix = match groups.binary_search_by(|probe| {
481 probe.excerpts.last().unwrap().cmp(&new_excerpt_id)
482 }) {
483 Ok(ix) | Err(ix) => ix,
484 };
485 if let Some(group) = groups.get(group_ix) {
486 let offset = excerpts_snapshot
487 .anchor_in_excerpt(
488 group.excerpts[group.primary_excerpt_ix].clone(),
489 group.primary_diagnostic.range.start.clone(),
490 )
491 .to_offset(&excerpts_snapshot);
492 selection.start = offset;
493 selection.end = offset;
494 }
495 }
496 }
497 editor.update_selections(selections, None, cx);
498 Some(())
499 });
500
501 if self.path_states.is_empty() {
502 if self.editor.is_focused(cx) {
503 cx.focus_self();
504 }
505 } else {
506 if cx.handle().is_focused(cx) {
507 cx.focus(&self.editor);
508 }
509 }
510 cx.notify();
511 }
512}
513
514impl workspace::Item for ProjectDiagnostics {
515 type View = ProjectDiagnosticsEditor;
516
517 fn build_view(
518 handle: ModelHandle<Self>,
519 workspace: &Workspace,
520 _: Rc<NavHistory>,
521 cx: &mut ViewContext<Self::View>,
522 ) -> Self::View {
523 ProjectDiagnosticsEditor::new(handle, workspace.weak_handle(), workspace.settings(), cx)
524 }
525
526 fn project_entry(&self) -> Option<project::ProjectEntry> {
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_entry(&self, _: &AppContext) -> Option<project::ProjectEntry> {
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>) -> 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>,
569 _: PathBuf,
570 _: &mut ViewContext<Self>,
571 ) -> Task<Result<()>> {
572 unreachable!()
573 }
574
575 fn should_activate_item_on_event(event: &Self::Event) -> bool {
576 Editor::should_activate_item_on_event(event)
577 }
578
579 fn should_update_tab_on_event(event: &Event) -> bool {
580 matches!(
581 event,
582 Event::Saved | Event::Dirtied | Event::FileHandleChanged
583 )
584 }
585
586 fn clone_on_split(&self, cx: &mut ViewContext<Self>) -> Option<Self>
587 where
588 Self: Sized,
589 {
590 Some(ProjectDiagnosticsEditor::new(
591 self.model.clone(),
592 self.workspace.clone(),
593 self.settings.clone(),
594 cx,
595 ))
596 }
597
598 fn act_as_type(
599 &self,
600 type_id: TypeId,
601 self_handle: &ViewHandle<Self>,
602 _: &AppContext,
603 ) -> Option<AnyViewHandle> {
604 if type_id == TypeId::of::<Self>() {
605 Some(self_handle.into())
606 } else if type_id == TypeId::of::<Editor>() {
607 Some((&self.editor).into())
608 } else {
609 None
610 }
611 }
612}
613
614fn path_header_renderer(buffer: ModelHandle<Buffer>, build_settings: BuildSettings) -> RenderBlock {
615 Arc::new(move |cx| {
616 let settings = build_settings(cx);
617 let file_path = if let Some(file) = buffer.read(&**cx).file() {
618 file.path().to_string_lossy().to_string()
619 } else {
620 "untitled".to_string()
621 };
622 let mut text_style = settings.style.text.clone();
623 let style = settings.style.diagnostic_path_header;
624 text_style.color = style.text;
625 Label::new(file_path, text_style)
626 .aligned()
627 .left()
628 .contained()
629 .with_style(style.header)
630 .with_padding_left(cx.line_number_x)
631 .expanded()
632 .named("path header block")
633 })
634}
635
636fn diagnostic_header_renderer(
637 diagnostic: Diagnostic,
638 is_valid: bool,
639 build_settings: BuildSettings,
640) -> RenderBlock {
641 Arc::new(move |cx| {
642 let settings = build_settings(cx);
643 let mut text_style = settings.style.text.clone();
644 let diagnostic_style = diagnostic_style(diagnostic.severity, is_valid, &settings.style);
645 text_style.color = diagnostic_style.text;
646 Text::new(diagnostic.message.clone(), text_style)
647 .with_soft_wrap(false)
648 .aligned()
649 .left()
650 .contained()
651 .with_style(diagnostic_style.header)
652 .with_padding_left(cx.line_number_x)
653 .expanded()
654 .named("diagnostic header")
655 })
656}
657
658fn context_header_renderer(build_settings: BuildSettings) -> RenderBlock {
659 Arc::new(move |cx| {
660 let settings = build_settings(cx);
661 let text_style = settings.style.text.clone();
662 Label::new("…".to_string(), text_style)
663 .contained()
664 .with_padding_left(cx.line_number_x)
665 .named("collapsed context")
666 })
667}
668
669fn compare_diagnostics<L: language::ToOffset, R: language::ToOffset>(
670 lhs: &DiagnosticEntry<L>,
671 rhs: &DiagnosticEntry<R>,
672 snapshot: &language::BufferSnapshot,
673) -> Ordering {
674 lhs.range
675 .start
676 .to_offset(&snapshot)
677 .cmp(&rhs.range.start.to_offset(snapshot))
678 .then_with(|| {
679 lhs.range
680 .end
681 .to_offset(&snapshot)
682 .cmp(&rhs.range.end.to_offset(snapshot))
683 })
684 .then_with(|| lhs.diagnostic.message.cmp(&rhs.diagnostic.message))
685}
686
687#[cfg(test)]
688mod tests {
689 use super::*;
690 use editor::{display_map::BlockContext, DisplayPoint, EditorSnapshot};
691 use gpui::TestAppContext;
692 use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16};
693 use serde_json::json;
694 use std::sync::Arc;
695 use unindent::Unindent as _;
696 use workspace::WorkspaceParams;
697
698 #[gpui::test]
699 async fn test_diagnostics(mut cx: TestAppContext) {
700 let params = cx.update(WorkspaceParams::test);
701 let project = params.project.clone();
702 let workspace = cx.add_view(0, |cx| Workspace::new(¶ms, cx));
703
704 params
705 .fs
706 .as_fake()
707 .insert_tree(
708 "/test",
709 json!({
710 "consts.rs": "
711 const a: i32 = 'a';
712 const b: i32 = c;
713 "
714 .unindent(),
715
716 "main.rs": "
717 fn main() {
718 let x = vec![];
719 let y = vec![];
720 a(x);
721 b(y);
722 // comment 1
723 // comment 2
724 c(y);
725 d(x);
726 }
727 "
728 .unindent(),
729 }),
730 )
731 .await;
732
733 let worktree = project
734 .update(&mut cx, |project, cx| {
735 project.add_local_worktree("/test", cx)
736 })
737 .await
738 .unwrap();
739 let worktree_id = worktree.read_with(&cx, |tree, _| tree.id());
740
741 // Create some diagnostics
742 worktree.update(&mut cx, |worktree, cx| {
743 worktree
744 .as_local_mut()
745 .unwrap()
746 .update_diagnostic_entries(
747 Arc::from("/test/main.rs".as_ref()),
748 None,
749 vec![
750 DiagnosticEntry {
751 range: PointUtf16::new(1, 8)..PointUtf16::new(1, 9),
752 diagnostic: Diagnostic {
753 message:
754 "move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait"
755 .to_string(),
756 severity: DiagnosticSeverity::INFORMATION,
757 is_primary: false,
758 is_disk_based: true,
759 group_id: 1,
760 ..Default::default()
761 },
762 },
763 DiagnosticEntry {
764 range: PointUtf16::new(2, 8)..PointUtf16::new(2, 9),
765 diagnostic: Diagnostic {
766 message:
767 "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait"
768 .to_string(),
769 severity: DiagnosticSeverity::INFORMATION,
770 is_primary: false,
771 is_disk_based: true,
772 group_id: 0,
773 ..Default::default()
774 },
775 },
776 DiagnosticEntry {
777 range: PointUtf16::new(3, 6)..PointUtf16::new(3, 7),
778 diagnostic: Diagnostic {
779 message: "value moved here".to_string(),
780 severity: DiagnosticSeverity::INFORMATION,
781 is_primary: false,
782 is_disk_based: true,
783 group_id: 1,
784 ..Default::default()
785 },
786 },
787 DiagnosticEntry {
788 range: PointUtf16::new(4, 6)..PointUtf16::new(4, 7),
789 diagnostic: Diagnostic {
790 message: "value moved here".to_string(),
791 severity: DiagnosticSeverity::INFORMATION,
792 is_primary: false,
793 is_disk_based: true,
794 group_id: 0,
795 ..Default::default()
796 },
797 },
798 DiagnosticEntry {
799 range: PointUtf16::new(7, 6)..PointUtf16::new(7, 7),
800 diagnostic: Diagnostic {
801 message: "use of moved value\nvalue used here after move".to_string(),
802 severity: DiagnosticSeverity::ERROR,
803 is_primary: true,
804 is_disk_based: true,
805 group_id: 0,
806 ..Default::default()
807 },
808 },
809 DiagnosticEntry {
810 range: PointUtf16::new(8, 6)..PointUtf16::new(8, 7),
811 diagnostic: Diagnostic {
812 message: "use of moved value\nvalue used here after move".to_string(),
813 severity: DiagnosticSeverity::ERROR,
814 is_primary: true,
815 is_disk_based: true,
816 group_id: 1,
817 ..Default::default()
818 },
819 },
820 ],
821 cx,
822 )
823 .unwrap();
824 });
825
826 // Open the project diagnostics view while there are already diagnostics.
827 let model = cx.add_model(|_| ProjectDiagnostics::new(project.clone()));
828 let view = cx.add_view(0, |cx| {
829 ProjectDiagnosticsEditor::new(model, workspace.downgrade(), params.settings, cx)
830 });
831
832 view.next_notification(&cx).await;
833 view.update(&mut cx, |view, cx| {
834 let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
835
836 assert_eq!(
837 editor_blocks(&editor, cx),
838 [
839 (0, "path header block".into()),
840 (2, "diagnostic header".into()),
841 (15, "diagnostic header".into()),
842 (24, "collapsed context".into()),
843 ]
844 );
845 assert_eq!(
846 editor.text(),
847 concat!(
848 //
849 // main.rs
850 //
851 "\n", // filename
852 "\n", // padding
853 // diagnostic group 1
854 "\n", // primary message
855 "\n", // padding
856 " let x = vec![];\n",
857 " let y = vec![];\n",
858 "\n", // supporting diagnostic
859 " a(x);\n",
860 " b(y);\n",
861 "\n", // supporting diagnostic
862 " // comment 1\n",
863 " // comment 2\n",
864 " c(y);\n",
865 "\n", // supporting diagnostic
866 " d(x);\n",
867 // diagnostic group 2
868 "\n", // primary message
869 "\n", // padding
870 "fn main() {\n",
871 " let x = vec![];\n",
872 "\n", // supporting diagnostic
873 " let y = vec![];\n",
874 " a(x);\n",
875 "\n", // supporting diagnostic
876 " b(y);\n",
877 "\n", // context ellipsis
878 " c(y);\n",
879 " d(x);\n",
880 "\n", // supporting diagnostic
881 "}"
882 )
883 );
884
885 // Cursor is at the first diagnostic
886 view.editor.update(cx, |editor, cx| {
887 assert_eq!(
888 editor.selected_display_ranges(cx),
889 [DisplayPoint::new(12, 6)..DisplayPoint::new(12, 6)]
890 );
891 });
892 });
893
894 // Diagnostics are added for another earlier path.
895 worktree.update(&mut cx, |worktree, cx| {
896 worktree
897 .as_local_mut()
898 .unwrap()
899 .update_diagnostic_entries(
900 Arc::from("/test/consts.rs".as_ref()),
901 None,
902 vec![DiagnosticEntry {
903 range: PointUtf16::new(0, 15)..PointUtf16::new(0, 15),
904 diagnostic: Diagnostic {
905 message: "mismatched types\nexpected `usize`, found `char`".to_string(),
906 severity: DiagnosticSeverity::ERROR,
907 is_primary: true,
908 is_disk_based: true,
909 group_id: 0,
910 ..Default::default()
911 },
912 }],
913 cx,
914 )
915 .unwrap();
916 });
917 project.update(&mut cx, |_, cx| {
918 cx.emit(project::Event::DiagnosticsUpdated(ProjectPath {
919 worktree_id,
920 path: Arc::from("/test/consts.rs".as_ref()),
921 }));
922 cx.emit(project::Event::DiskBasedDiagnosticsFinished);
923 });
924
925 view.next_notification(&cx).await;
926 view.update(&mut cx, |view, cx| {
927 let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
928
929 assert_eq!(
930 editor_blocks(&editor, cx),
931 [
932 (0, "path header block".into()),
933 (2, "diagnostic header".into()),
934 (7, "path header block".into()),
935 (9, "diagnostic header".into()),
936 (22, "diagnostic header".into()),
937 (31, "collapsed context".into()),
938 ]
939 );
940 assert_eq!(
941 editor.text(),
942 concat!(
943 //
944 // consts.rs
945 //
946 "\n", // filename
947 "\n", // padding
948 // diagnostic group 1
949 "\n", // primary message
950 "\n", // padding
951 "const a: i32 = 'a';\n",
952 "\n", // supporting diagnostic
953 "const b: i32 = c;\n",
954 //
955 // main.rs
956 //
957 "\n", // filename
958 "\n", // padding
959 // diagnostic group 1
960 "\n", // primary message
961 "\n", // padding
962 " let x = vec![];\n",
963 " let y = vec![];\n",
964 "\n", // supporting diagnostic
965 " a(x);\n",
966 " b(y);\n",
967 "\n", // supporting diagnostic
968 " // comment 1\n",
969 " // comment 2\n",
970 " c(y);\n",
971 "\n", // supporting diagnostic
972 " d(x);\n",
973 // diagnostic group 2
974 "\n", // primary message
975 "\n", // filename
976 "fn main() {\n",
977 " let x = vec![];\n",
978 "\n", // supporting diagnostic
979 " let y = vec![];\n",
980 " a(x);\n",
981 "\n", // supporting diagnostic
982 " b(y);\n",
983 "\n", // context ellipsis
984 " c(y);\n",
985 " d(x);\n",
986 "\n", // supporting diagnostic
987 "}"
988 )
989 );
990
991 // Cursor keeps its position.
992 view.editor.update(cx, |editor, cx| {
993 assert_eq!(
994 editor.selected_display_ranges(cx),
995 [DisplayPoint::new(19, 6)..DisplayPoint::new(19, 6)]
996 );
997 });
998 });
999
1000 // Diagnostics are added to the first path
1001 worktree.update(&mut cx, |worktree, cx| {
1002 worktree
1003 .as_local_mut()
1004 .unwrap()
1005 .update_diagnostic_entries(
1006 Arc::from("/test/consts.rs".as_ref()),
1007 None,
1008 vec![
1009 DiagnosticEntry {
1010 range: PointUtf16::new(0, 15)..PointUtf16::new(0, 15),
1011 diagnostic: Diagnostic {
1012 message: "mismatched types\nexpected `usize`, found `char`"
1013 .to_string(),
1014 severity: DiagnosticSeverity::ERROR,
1015 is_primary: true,
1016 is_disk_based: true,
1017 group_id: 0,
1018 ..Default::default()
1019 },
1020 },
1021 DiagnosticEntry {
1022 range: PointUtf16::new(1, 15)..PointUtf16::new(1, 15),
1023 diagnostic: Diagnostic {
1024 message: "unresolved name `c`".to_string(),
1025 severity: DiagnosticSeverity::ERROR,
1026 is_primary: true,
1027 is_disk_based: true,
1028 group_id: 1,
1029 ..Default::default()
1030 },
1031 },
1032 ],
1033 cx,
1034 )
1035 .unwrap();
1036 });
1037 project.update(&mut cx, |_, cx| {
1038 cx.emit(project::Event::DiagnosticsUpdated(ProjectPath {
1039 worktree_id,
1040 path: Arc::from("/test/consts.rs".as_ref()),
1041 }));
1042 cx.emit(project::Event::DiskBasedDiagnosticsFinished);
1043 });
1044
1045 view.next_notification(&cx).await;
1046 view.update(&mut cx, |view, cx| {
1047 let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
1048
1049 assert_eq!(
1050 editor_blocks(&editor, cx),
1051 [
1052 (0, "path header block".into()),
1053 (2, "diagnostic header".into()),
1054 (7, "diagnostic header".into()),
1055 (12, "path header block".into()),
1056 (14, "diagnostic header".into()),
1057 (27, "diagnostic header".into()),
1058 (36, "collapsed context".into()),
1059 ]
1060 );
1061 assert_eq!(
1062 editor.text(),
1063 concat!(
1064 //
1065 // consts.rs
1066 //
1067 "\n", // filename
1068 "\n", // padding
1069 // diagnostic group 1
1070 "\n", // primary message
1071 "\n", // padding
1072 "const a: i32 = 'a';\n",
1073 "\n", // supporting diagnostic
1074 "const b: i32 = c;\n",
1075 // diagnostic group 2
1076 "\n", // primary message
1077 "\n", // padding
1078 "const a: i32 = 'a';\n",
1079 "const b: i32 = c;\n",
1080 "\n", // supporting diagnostic
1081 //
1082 // main.rs
1083 //
1084 "\n", // filename
1085 "\n", // padding
1086 // diagnostic group 1
1087 "\n", // primary message
1088 "\n", // padding
1089 " let x = vec![];\n",
1090 " let y = vec![];\n",
1091 "\n", // supporting diagnostic
1092 " a(x);\n",
1093 " b(y);\n",
1094 "\n", // supporting diagnostic
1095 " // comment 1\n",
1096 " // comment 2\n",
1097 " c(y);\n",
1098 "\n", // supporting diagnostic
1099 " d(x);\n",
1100 // diagnostic group 2
1101 "\n", // primary message
1102 "\n", // filename
1103 "fn main() {\n",
1104 " let x = vec![];\n",
1105 "\n", // supporting diagnostic
1106 " let y = vec![];\n",
1107 " a(x);\n",
1108 "\n", // supporting diagnostic
1109 " b(y);\n",
1110 "\n", // context ellipsis
1111 " c(y);\n",
1112 " d(x);\n",
1113 "\n", // supporting diagnostic
1114 "}"
1115 )
1116 );
1117 });
1118 }
1119
1120 fn editor_blocks(editor: &EditorSnapshot, cx: &AppContext) -> Vec<(u32, String)> {
1121 editor
1122 .blocks_in_range(0..editor.max_point().row())
1123 .filter_map(|(row, block)| {
1124 block
1125 .render(&BlockContext {
1126 cx,
1127 anchor_x: 0.,
1128 line_number_x: 0.,
1129 })
1130 .name()
1131 .map(|s| (row, s.to_string()))
1132 })
1133 .collect()
1134 }
1135}