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::{
19 any::{Any, TypeId},
20 cmp::Ordering,
21 mem,
22 ops::Range,
23 path::PathBuf,
24 sync::Arc,
25};
26use util::TryFutureExt;
27use workspace::{ItemNavHistory, Workspace};
28
29action!(Deploy);
30action!(OpenExcerpts);
31
32const CONTEXT_LINE_COUNT: u32 = 1;
33
34pub fn init(cx: &mut MutableAppContext) {
35 cx.add_bindings([
36 Binding::new("alt-shift-D", Deploy, Some("Workspace")),
37 Binding::new(
38 "alt-shift-D",
39 OpenExcerpts,
40 Some("ProjectDiagnosticsEditor"),
41 ),
42 ]);
43 cx.add_action(ProjectDiagnosticsEditor::deploy);
44 cx.add_action(ProjectDiagnosticsEditor::open_excerpts);
45}
46
47type Event = editor::Event;
48
49struct ProjectDiagnostics {
50 project: ModelHandle<Project>,
51}
52
53struct ProjectDiagnosticsEditor {
54 model: ModelHandle<ProjectDiagnostics>,
55 workspace: WeakViewHandle<Workspace>,
56 editor: ViewHandle<Editor>,
57 excerpts: ModelHandle<MultiBuffer>,
58 path_states: Vec<PathState>,
59 paths_to_update: BTreeSet<ProjectPath>,
60 build_settings: BuildSettings,
61 settings: watch::Receiver<workspace::Settings>,
62}
63
64struct PathState {
65 path: ProjectPath,
66 header: Option<BlockId>,
67 diagnostic_groups: Vec<DiagnosticGroupState>,
68}
69
70struct DiagnosticGroupState {
71 primary_diagnostic: DiagnosticEntry<language::Anchor>,
72 primary_excerpt_ix: usize,
73 excerpts: Vec<ExcerptId>,
74 blocks: HashSet<BlockId>,
75 block_count: usize,
76}
77
78impl ProjectDiagnostics {
79 fn new(project: ModelHandle<Project>) -> Self {
80 Self { project }
81 }
82}
83
84impl Entity for ProjectDiagnostics {
85 type Event = ();
86}
87
88impl Entity for ProjectDiagnosticsEditor {
89 type Event = Event;
90}
91
92impl View for ProjectDiagnosticsEditor {
93 fn ui_name() -> &'static str {
94 "ProjectDiagnosticsEditor"
95 }
96
97 fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
98 if self.path_states.is_empty() {
99 let theme = &self.settings.borrow().theme.project_diagnostics;
100 Label::new(
101 "No problems detected in the project".to_string(),
102 theme.empty_message.clone(),
103 )
104 .aligned()
105 .contained()
106 .with_style(theme.container)
107 .boxed()
108 } else {
109 ChildView::new(self.editor.id()).boxed()
110 }
111 }
112
113 fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
114 if !self.path_states.is_empty() {
115 cx.focus(&self.editor);
116 }
117 }
118}
119
120impl ProjectDiagnosticsEditor {
121 fn new(
122 model: ModelHandle<ProjectDiagnostics>,
123 workspace: WeakViewHandle<Workspace>,
124 settings: watch::Receiver<workspace::Settings>,
125 cx: &mut ViewContext<Self>,
126 ) -> Self {
127 let project = model.read(cx).project.clone();
128 cx.subscribe(&project, |this, _, event, cx| match event {
129 project::Event::DiskBasedDiagnosticsFinished => {
130 let paths = mem::take(&mut this.paths_to_update);
131 this.update_excerpts(paths, cx);
132 }
133 project::Event::DiagnosticsUpdated(path) => {
134 this.paths_to_update.insert(path.clone());
135 }
136 _ => {}
137 })
138 .detach();
139
140 let excerpts = cx.add_model(|cx| MultiBuffer::new(project.read(cx).replica_id()));
141 let build_settings = editor::settings_builder(excerpts.downgrade(), settings.clone());
142 let editor =
143 cx.add_view(|cx| Editor::for_buffer(excerpts.clone(), build_settings.clone(), cx));
144 cx.subscribe(&editor, |_, _, event, cx| cx.emit(*event))
145 .detach();
146
147 let paths_to_update = project
148 .read(cx)
149 .diagnostic_summaries(cx)
150 .map(|e| e.0)
151 .collect();
152 let this = Self {
153 model,
154 workspace,
155 excerpts,
156 editor,
157 build_settings,
158 settings,
159 path_states: Default::default(),
160 paths_to_update: Default::default(),
161 };
162 this.update_excerpts(paths_to_update, cx);
163 this
164 }
165
166 fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext<Workspace>) {
167 if let Some(existing) = workspace.item_of_type::<ProjectDiagnostics>(cx) {
168 workspace.activate_item(&existing, cx);
169 } else {
170 let diagnostics =
171 cx.add_model(|_| ProjectDiagnostics::new(workspace.project().clone()));
172 workspace.open_item(diagnostics, cx);
173 }
174 }
175
176 fn open_excerpts(&mut self, _: &OpenExcerpts, cx: &mut ViewContext<Self>) {
177 if let Some(workspace) = self.workspace.upgrade(cx) {
178 let editor = self.editor.read(cx);
179 let excerpts = self.excerpts.read(cx);
180 let mut new_selections_by_buffer = HashMap::default();
181
182 for selection in editor.local_selections::<usize>(cx) {
183 for (buffer, mut range) in
184 excerpts.excerpted_buffers(selection.start..selection.end, cx)
185 {
186 if selection.reversed {
187 mem::swap(&mut range.start, &mut range.end);
188 }
189 new_selections_by_buffer
190 .entry(buffer)
191 .or_insert(Vec::new())
192 .push(range)
193 }
194 }
195
196 // We defer the pane interaction because we ourselves are a workspace item
197 // and activating a new item causes the pane to call a method on us reentrantly,
198 // which panics if we're on the stack.
199 workspace.defer(cx, |workspace, cx| {
200 for (buffer, ranges) in new_selections_by_buffer {
201 let buffer = BufferItemHandle(buffer);
202 if !workspace.activate_pane_for_item(&buffer, cx) {
203 workspace.activate_next_pane(cx);
204 }
205 let editor = workspace
206 .open_item(buffer, cx)
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 block_ids = editor.insert_blocks(
427 blocks_to_add
428 .into_iter()
429 .map(|block| {
430 let (excerpt_id, text_anchor) = block.position;
431 BlockProperties {
432 position: excerpts_snapshot.anchor_in_excerpt(excerpt_id, text_anchor),
433 height: block.height,
434 render: block.render,
435 disposition: block.disposition,
436 }
437 })
438 .chain(header_block.into_iter()),
439 cx,
440 );
441
442 let mut block_ids = block_ids.into_iter();
443 for group_state in &mut groups_to_add {
444 group_state.blocks = block_ids.by_ref().take(group_state.block_count).collect();
445 }
446 path_state.header = block_ids.next();
447 });
448
449 for ix in group_ixs_to_remove.into_iter().rev() {
450 path_state.diagnostic_groups.remove(ix);
451 }
452 path_state.diagnostic_groups.extend(groups_to_add);
453 path_state.diagnostic_groups.sort_unstable_by(|a, b| {
454 let range_a = &a.primary_diagnostic.range;
455 let range_b = &b.primary_diagnostic.range;
456 range_a
457 .start
458 .cmp(&range_b.start, &snapshot)
459 .unwrap()
460 .then_with(|| range_a.end.cmp(&range_b.end, &snapshot).unwrap())
461 });
462
463 if path_state.diagnostic_groups.is_empty() {
464 self.path_states.remove(path_ix);
465 }
466
467 self.editor.update(cx, |editor, cx| {
468 let groups;
469 let mut selections;
470 let new_excerpt_ids_by_selection_id;
471 if was_empty {
472 groups = self.path_states.first()?.diagnostic_groups.as_slice();
473 new_excerpt_ids_by_selection_id = [(0, ExcerptId::min())].into_iter().collect();
474 selections = vec![Selection {
475 id: 0,
476 start: 0,
477 end: 0,
478 reversed: false,
479 goal: SelectionGoal::None,
480 }];
481 } else {
482 groups = self.path_states.get(path_ix)?.diagnostic_groups.as_slice();
483 new_excerpt_ids_by_selection_id = editor.refresh_selections(cx);
484 selections = editor.local_selections::<usize>(cx);
485 }
486
487 // If any selection has lost its position, move it to start of the next primary diagnostic.
488 for selection in &mut selections {
489 if let Some(new_excerpt_id) = new_excerpt_ids_by_selection_id.get(&selection.id) {
490 let group_ix = match groups.binary_search_by(|probe| {
491 probe.excerpts.last().unwrap().cmp(&new_excerpt_id)
492 }) {
493 Ok(ix) | Err(ix) => ix,
494 };
495 if let Some(group) = groups.get(group_ix) {
496 let offset = excerpts_snapshot
497 .anchor_in_excerpt(
498 group.excerpts[group.primary_excerpt_ix].clone(),
499 group.primary_diagnostic.range.start.clone(),
500 )
501 .to_offset(&excerpts_snapshot);
502 selection.start = offset;
503 selection.end = offset;
504 }
505 }
506 }
507 editor.update_selections(selections, None, cx);
508 Some(())
509 });
510
511 if self.path_states.is_empty() {
512 if self.editor.is_focused(cx) {
513 cx.focus_self();
514 }
515 } else {
516 if cx.handle().is_focused(cx) {
517 cx.focus(&self.editor);
518 }
519 }
520 cx.notify();
521 }
522}
523
524impl workspace::Item for ProjectDiagnostics {
525 type View = ProjectDiagnosticsEditor;
526
527 fn build_view(
528 handle: ModelHandle<Self>,
529 workspace: &Workspace,
530 nav_history: ItemNavHistory,
531 cx: &mut ViewContext<Self::View>,
532 ) -> Self::View {
533 let diagnostics = ProjectDiagnosticsEditor::new(
534 handle,
535 workspace.weak_handle(),
536 workspace.settings(),
537 cx,
538 );
539 diagnostics
540 .editor
541 .update(cx, |editor, _| editor.set_nav_history(Some(nav_history)));
542 diagnostics
543 }
544
545 fn project_path(&self) -> Option<project::ProjectPath> {
546 None
547 }
548}
549
550impl workspace::ItemView for ProjectDiagnosticsEditor {
551 type ItemHandle = ModelHandle<ProjectDiagnostics>;
552
553 fn item_handle(&self, _: &AppContext) -> Self::ItemHandle {
554 self.model.clone()
555 }
556
557 fn title(&self, _: &AppContext) -> String {
558 "Project Diagnostics".to_string()
559 }
560
561 fn project_path(&self, _: &AppContext) -> Option<project::ProjectPath> {
562 None
563 }
564
565 fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) {
566 self.editor
567 .update(cx, |editor, cx| editor.navigate(data, cx));
568 }
569
570 fn is_dirty(&self, cx: &AppContext) -> bool {
571 self.excerpts.read(cx).read(cx).is_dirty()
572 }
573
574 fn has_conflict(&self, cx: &AppContext) -> bool {
575 self.excerpts.read(cx).read(cx).has_conflict()
576 }
577
578 fn can_save(&self, _: &AppContext) -> bool {
579 true
580 }
581
582 fn save(&mut self, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
583 self.excerpts.update(cx, |excerpts, cx| excerpts.save(cx))
584 }
585
586 fn can_save_as(&self, _: &AppContext) -> bool {
587 false
588 }
589
590 fn save_as(
591 &mut self,
592 _: ModelHandle<Project>,
593 _: PathBuf,
594 _: &mut ViewContext<Self>,
595 ) -> Task<Result<()>> {
596 unreachable!()
597 }
598
599 fn should_activate_item_on_event(event: &Self::Event) -> bool {
600 Editor::should_activate_item_on_event(event)
601 }
602
603 fn should_update_tab_on_event(event: &Event) -> bool {
604 matches!(
605 event,
606 Event::Saved | Event::Dirtied | Event::FileHandleChanged
607 )
608 }
609
610 fn clone_on_split(&self, cx: &mut ViewContext<Self>) -> Option<Self>
611 where
612 Self: Sized,
613 {
614 let diagnostics = ProjectDiagnosticsEditor::new(
615 self.model.clone(),
616 self.workspace.clone(),
617 self.settings.clone(),
618 cx,
619 );
620 diagnostics.editor.update(cx, |editor, cx| {
621 let nav_history = self
622 .editor
623 .read(cx)
624 .nav_history()
625 .map(|nav_history| ItemNavHistory::new(nav_history.history(), &cx.handle()));
626 editor.set_nav_history(nav_history);
627 });
628 Some(diagnostics)
629 }
630
631 fn act_as_type(
632 &self,
633 type_id: TypeId,
634 self_handle: &ViewHandle<Self>,
635 _: &AppContext,
636 ) -> Option<AnyViewHandle> {
637 if type_id == TypeId::of::<Self>() {
638 Some(self_handle.into())
639 } else if type_id == TypeId::of::<Editor>() {
640 Some((&self.editor).into())
641 } else {
642 None
643 }
644 }
645
646 fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
647 self.editor.update(cx, |editor, cx| editor.deactivated(cx));
648 }
649}
650
651fn path_header_renderer(buffer: ModelHandle<Buffer>, build_settings: BuildSettings) -> RenderBlock {
652 Arc::new(move |cx| {
653 let settings = build_settings(cx);
654 let file_path = if let Some(file) = buffer.read(&**cx).file() {
655 file.path().to_string_lossy().to_string()
656 } else {
657 "untitled".to_string()
658 };
659 let mut text_style = settings.style.text.clone();
660 let style = settings.style.diagnostic_path_header;
661 text_style.color = style.text;
662 Label::new(file_path, text_style)
663 .aligned()
664 .left()
665 .contained()
666 .with_style(style.header)
667 .with_padding_left(cx.line_number_x)
668 .expanded()
669 .named("path header block")
670 })
671}
672
673fn diagnostic_header_renderer(
674 diagnostic: Diagnostic,
675 is_valid: bool,
676 build_settings: BuildSettings,
677) -> RenderBlock {
678 Arc::new(move |cx| {
679 let settings = build_settings(cx);
680 let mut text_style = settings.style.text.clone();
681 let diagnostic_style = diagnostic_style(diagnostic.severity, is_valid, &settings.style);
682 text_style.color = diagnostic_style.text;
683 Text::new(diagnostic.message.clone(), text_style)
684 .with_soft_wrap(false)
685 .aligned()
686 .left()
687 .contained()
688 .with_style(diagnostic_style.header)
689 .with_padding_left(cx.line_number_x)
690 .expanded()
691 .named("diagnostic header")
692 })
693}
694
695fn context_header_renderer(build_settings: BuildSettings) -> RenderBlock {
696 Arc::new(move |cx| {
697 let settings = build_settings(cx);
698 let text_style = settings.style.text.clone();
699 Label::new("…".to_string(), text_style)
700 .contained()
701 .with_padding_left(cx.line_number_x)
702 .named("collapsed context")
703 })
704}
705
706fn compare_diagnostics<L: language::ToOffset, R: language::ToOffset>(
707 lhs: &DiagnosticEntry<L>,
708 rhs: &DiagnosticEntry<R>,
709 snapshot: &language::BufferSnapshot,
710) -> Ordering {
711 lhs.range
712 .start
713 .to_offset(&snapshot)
714 .cmp(&rhs.range.start.to_offset(snapshot))
715 .then_with(|| {
716 lhs.range
717 .end
718 .to_offset(&snapshot)
719 .cmp(&rhs.range.end.to_offset(snapshot))
720 })
721 .then_with(|| lhs.diagnostic.message.cmp(&rhs.diagnostic.message))
722}
723
724#[cfg(test)]
725mod tests {
726 use super::*;
727 use editor::{display_map::BlockContext, DisplayPoint, EditorSnapshot};
728 use gpui::TestAppContext;
729 use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16};
730 use serde_json::json;
731 use unindent::Unindent as _;
732 use workspace::WorkspaceParams;
733
734 #[gpui::test]
735 async fn test_diagnostics(mut cx: TestAppContext) {
736 let params = cx.update(WorkspaceParams::test);
737 let project = params.project.clone();
738 let workspace = cx.add_view(0, |cx| Workspace::new(¶ms, cx));
739
740 params
741 .fs
742 .as_fake()
743 .insert_tree(
744 "/test",
745 json!({
746 "consts.rs": "
747 const a: i32 = 'a';
748 const b: i32 = c;
749 "
750 .unindent(),
751
752 "main.rs": "
753 fn main() {
754 let x = vec![];
755 let y = vec![];
756 a(x);
757 b(y);
758 // comment 1
759 // comment 2
760 c(y);
761 d(x);
762 }
763 "
764 .unindent(),
765 }),
766 )
767 .await;
768
769 project
770 .update(&mut cx, |project, cx| {
771 project.find_or_create_local_worktree("/test", false, cx)
772 })
773 .await
774 .unwrap();
775
776 // Create some diagnostics
777 project.update(&mut cx, |project, cx| {
778 project
779 .update_diagnostic_entries(
780 PathBuf::from("/test/main.rs"),
781 None,
782 vec![
783 DiagnosticEntry {
784 range: PointUtf16::new(1, 8)..PointUtf16::new(1, 9),
785 diagnostic: Diagnostic {
786 message:
787 "move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait"
788 .to_string(),
789 severity: DiagnosticSeverity::INFORMATION,
790 is_primary: false,
791 is_disk_based: true,
792 group_id: 1,
793 ..Default::default()
794 },
795 },
796 DiagnosticEntry {
797 range: PointUtf16::new(2, 8)..PointUtf16::new(2, 9),
798 diagnostic: Diagnostic {
799 message:
800 "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait"
801 .to_string(),
802 severity: DiagnosticSeverity::INFORMATION,
803 is_primary: false,
804 is_disk_based: true,
805 group_id: 0,
806 ..Default::default()
807 },
808 },
809 DiagnosticEntry {
810 range: PointUtf16::new(3, 6)..PointUtf16::new(3, 7),
811 diagnostic: Diagnostic {
812 message: "value moved here".to_string(),
813 severity: DiagnosticSeverity::INFORMATION,
814 is_primary: false,
815 is_disk_based: true,
816 group_id: 1,
817 ..Default::default()
818 },
819 },
820 DiagnosticEntry {
821 range: PointUtf16::new(4, 6)..PointUtf16::new(4, 7),
822 diagnostic: Diagnostic {
823 message: "value moved here".to_string(),
824 severity: DiagnosticSeverity::INFORMATION,
825 is_primary: false,
826 is_disk_based: true,
827 group_id: 0,
828 ..Default::default()
829 },
830 },
831 DiagnosticEntry {
832 range: PointUtf16::new(7, 6)..PointUtf16::new(7, 7),
833 diagnostic: Diagnostic {
834 message: "use of moved value\nvalue used here after move".to_string(),
835 severity: DiagnosticSeverity::ERROR,
836 is_primary: true,
837 is_disk_based: true,
838 group_id: 0,
839 ..Default::default()
840 },
841 },
842 DiagnosticEntry {
843 range: PointUtf16::new(8, 6)..PointUtf16::new(8, 7),
844 diagnostic: Diagnostic {
845 message: "use of moved value\nvalue used here after move".to_string(),
846 severity: DiagnosticSeverity::ERROR,
847 is_primary: true,
848 is_disk_based: true,
849 group_id: 1,
850 ..Default::default()
851 },
852 },
853 ],
854 cx,
855 )
856 .unwrap();
857 });
858
859 // Open the project diagnostics view while there are already diagnostics.
860 let model = cx.add_model(|_| ProjectDiagnostics::new(project.clone()));
861 let view = cx.add_view(0, |cx| {
862 ProjectDiagnosticsEditor::new(model, workspace.downgrade(), params.settings, cx)
863 });
864
865 view.next_notification(&cx).await;
866 view.update(&mut cx, |view, cx| {
867 let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
868
869 assert_eq!(
870 editor_blocks(&editor, cx),
871 [
872 (0, "path header block".into()),
873 (2, "diagnostic header".into()),
874 (15, "diagnostic header".into()),
875 (24, "collapsed context".into()),
876 ]
877 );
878 assert_eq!(
879 editor.text(),
880 concat!(
881 //
882 // main.rs
883 //
884 "\n", // filename
885 "\n", // padding
886 // diagnostic group 1
887 "\n", // primary message
888 "\n", // padding
889 " let x = vec![];\n",
890 " let y = vec![];\n",
891 "\n", // supporting diagnostic
892 " a(x);\n",
893 " b(y);\n",
894 "\n", // supporting diagnostic
895 " // comment 1\n",
896 " // comment 2\n",
897 " c(y);\n",
898 "\n", // supporting diagnostic
899 " d(x);\n",
900 // diagnostic group 2
901 "\n", // primary message
902 "\n", // padding
903 "fn main() {\n",
904 " let x = vec![];\n",
905 "\n", // supporting diagnostic
906 " let y = vec![];\n",
907 " a(x);\n",
908 "\n", // supporting diagnostic
909 " b(y);\n",
910 "\n", // context ellipsis
911 " c(y);\n",
912 " d(x);\n",
913 "\n", // supporting diagnostic
914 "}"
915 )
916 );
917
918 // Cursor is at the first diagnostic
919 view.editor.update(cx, |editor, cx| {
920 assert_eq!(
921 editor.selected_display_ranges(cx),
922 [DisplayPoint::new(12, 6)..DisplayPoint::new(12, 6)]
923 );
924 });
925 });
926
927 // Diagnostics are added for another earlier path.
928 project.update(&mut cx, |project, cx| {
929 project.disk_based_diagnostics_started(cx);
930 project
931 .update_diagnostic_entries(
932 PathBuf::from("/test/consts.rs"),
933 None,
934 vec![DiagnosticEntry {
935 range: PointUtf16::new(0, 15)..PointUtf16::new(0, 15),
936 diagnostic: Diagnostic {
937 message: "mismatched types\nexpected `usize`, found `char`".to_string(),
938 severity: DiagnosticSeverity::ERROR,
939 is_primary: true,
940 is_disk_based: true,
941 group_id: 0,
942 ..Default::default()
943 },
944 }],
945 cx,
946 )
947 .unwrap();
948 project.disk_based_diagnostics_finished(cx);
949 });
950
951 view.next_notification(&cx).await;
952 view.update(&mut cx, |view, cx| {
953 let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
954
955 assert_eq!(
956 editor_blocks(&editor, cx),
957 [
958 (0, "path header block".into()),
959 (2, "diagnostic header".into()),
960 (7, "path header block".into()),
961 (9, "diagnostic header".into()),
962 (22, "diagnostic header".into()),
963 (31, "collapsed context".into()),
964 ]
965 );
966 assert_eq!(
967 editor.text(),
968 concat!(
969 //
970 // consts.rs
971 //
972 "\n", // filename
973 "\n", // padding
974 // diagnostic group 1
975 "\n", // primary message
976 "\n", // padding
977 "const a: i32 = 'a';\n",
978 "\n", // supporting diagnostic
979 "const b: i32 = c;\n",
980 //
981 // main.rs
982 //
983 "\n", // filename
984 "\n", // padding
985 // diagnostic group 1
986 "\n", // primary message
987 "\n", // padding
988 " let x = vec![];\n",
989 " let y = vec![];\n",
990 "\n", // supporting diagnostic
991 " a(x);\n",
992 " b(y);\n",
993 "\n", // supporting diagnostic
994 " // comment 1\n",
995 " // comment 2\n",
996 " c(y);\n",
997 "\n", // supporting diagnostic
998 " d(x);\n",
999 // diagnostic group 2
1000 "\n", // primary message
1001 "\n", // filename
1002 "fn main() {\n",
1003 " let x = vec![];\n",
1004 "\n", // supporting diagnostic
1005 " let y = vec![];\n",
1006 " a(x);\n",
1007 "\n", // supporting diagnostic
1008 " b(y);\n",
1009 "\n", // context ellipsis
1010 " c(y);\n",
1011 " d(x);\n",
1012 "\n", // supporting diagnostic
1013 "}"
1014 )
1015 );
1016
1017 // Cursor keeps its position.
1018 view.editor.update(cx, |editor, cx| {
1019 assert_eq!(
1020 editor.selected_display_ranges(cx),
1021 [DisplayPoint::new(19, 6)..DisplayPoint::new(19, 6)]
1022 );
1023 });
1024 });
1025
1026 // Diagnostics are added to the first path
1027 project.update(&mut cx, |project, cx| {
1028 project.disk_based_diagnostics_started(cx);
1029 project
1030 .update_diagnostic_entries(
1031 PathBuf::from("/test/consts.rs"),
1032 None,
1033 vec![
1034 DiagnosticEntry {
1035 range: PointUtf16::new(0, 15)..PointUtf16::new(0, 15),
1036 diagnostic: Diagnostic {
1037 message: "mismatched types\nexpected `usize`, found `char`"
1038 .to_string(),
1039 severity: DiagnosticSeverity::ERROR,
1040 is_primary: true,
1041 is_disk_based: true,
1042 group_id: 0,
1043 ..Default::default()
1044 },
1045 },
1046 DiagnosticEntry {
1047 range: PointUtf16::new(1, 15)..PointUtf16::new(1, 15),
1048 diagnostic: Diagnostic {
1049 message: "unresolved name `c`".to_string(),
1050 severity: DiagnosticSeverity::ERROR,
1051 is_primary: true,
1052 is_disk_based: true,
1053 group_id: 1,
1054 ..Default::default()
1055 },
1056 },
1057 ],
1058 cx,
1059 )
1060 .unwrap();
1061 project.disk_based_diagnostics_finished(cx);
1062 });
1063
1064 view.next_notification(&cx).await;
1065 view.update(&mut cx, |view, cx| {
1066 let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
1067
1068 assert_eq!(
1069 editor_blocks(&editor, cx),
1070 [
1071 (0, "path header block".into()),
1072 (2, "diagnostic header".into()),
1073 (7, "diagnostic header".into()),
1074 (12, "path header block".into()),
1075 (14, "diagnostic header".into()),
1076 (27, "diagnostic header".into()),
1077 (36, "collapsed context".into()),
1078 ]
1079 );
1080 assert_eq!(
1081 editor.text(),
1082 concat!(
1083 //
1084 // consts.rs
1085 //
1086 "\n", // filename
1087 "\n", // padding
1088 // diagnostic group 1
1089 "\n", // primary message
1090 "\n", // padding
1091 "const a: i32 = 'a';\n",
1092 "\n", // supporting diagnostic
1093 "const b: i32 = c;\n",
1094 // diagnostic group 2
1095 "\n", // primary message
1096 "\n", // padding
1097 "const a: i32 = 'a';\n",
1098 "const b: i32 = c;\n",
1099 "\n", // supporting diagnostic
1100 //
1101 // main.rs
1102 //
1103 "\n", // filename
1104 "\n", // padding
1105 // diagnostic group 1
1106 "\n", // primary message
1107 "\n", // padding
1108 " let x = vec![];\n",
1109 " let y = vec![];\n",
1110 "\n", // supporting diagnostic
1111 " a(x);\n",
1112 " b(y);\n",
1113 "\n", // supporting diagnostic
1114 " // comment 1\n",
1115 " // comment 2\n",
1116 " c(y);\n",
1117 "\n", // supporting diagnostic
1118 " d(x);\n",
1119 // diagnostic group 2
1120 "\n", // primary message
1121 "\n", // filename
1122 "fn main() {\n",
1123 " let x = vec![];\n",
1124 "\n", // supporting diagnostic
1125 " let y = vec![];\n",
1126 " a(x);\n",
1127 "\n", // supporting diagnostic
1128 " b(y);\n",
1129 "\n", // context ellipsis
1130 " c(y);\n",
1131 " d(x);\n",
1132 "\n", // supporting diagnostic
1133 "}"
1134 )
1135 );
1136 });
1137 }
1138
1139 fn editor_blocks(editor: &EditorSnapshot, cx: &AppContext) -> Vec<(u32, String)> {
1140 editor
1141 .blocks_in_range(0..editor.max_point().row())
1142 .filter_map(|(row, block)| {
1143 block
1144 .render(&BlockContext {
1145 cx,
1146 anchor_x: 0.,
1147 line_number_x: 0.,
1148 })
1149 .name()
1150 .map(|s| (row, s.to_string()))
1151 })
1152 .collect()
1153 }
1154}