1pub mod items;
2
3use anyhow::Result;
4use collections::{BTreeSet, HashMap, HashSet};
5use editor::{
6 diagnostic_block_renderer,
7 display_map::{BlockDisposition, BlockId, BlockProperties, RenderBlock},
8 highlight_diagnostic_message,
9 items::BufferItemHandle,
10 Autoscroll, BuildSettings, Editor, ExcerptId, ExcerptProperties, MultiBuffer, ToOffset,
11};
12use gpui::{
13 action, elements::*, keymap::Binding, AnyViewHandle, AppContext, Entity, ModelHandle,
14 MutableAppContext, RenderContext, Task, View, ViewContext, ViewHandle, WeakViewHandle,
15};
16use language::{
17 Bias, Buffer, Diagnostic, DiagnosticEntry, DiagnosticSeverity, Point, Selection, SelectionGoal,
18};
19use postage::watch;
20use project::{Project, ProjectPath};
21use std::{
22 any::{Any, TypeId},
23 cmp::Ordering,
24 mem,
25 ops::Range,
26 path::PathBuf,
27 sync::Arc,
28};
29use util::TryFutureExt;
30use workspace::{ItemNavHistory, Workspace};
31
32action!(Deploy);
33action!(OpenExcerpts);
34
35const CONTEXT_LINE_COUNT: u32 = 1;
36
37pub fn init(cx: &mut MutableAppContext) {
38 cx.add_bindings([
39 Binding::new("alt-shift-D", Deploy, Some("Workspace")),
40 Binding::new(
41 "alt-shift-D",
42 OpenExcerpts,
43 Some("ProjectDiagnosticsEditor"),
44 ),
45 ]);
46 cx.add_action(ProjectDiagnosticsEditor::deploy);
47 cx.add_action(ProjectDiagnosticsEditor::open_excerpts);
48}
49
50type Event = editor::Event;
51
52struct ProjectDiagnostics {
53 project: ModelHandle<Project>,
54}
55
56struct ProjectDiagnosticsEditor {
57 model: ModelHandle<ProjectDiagnostics>,
58 workspace: WeakViewHandle<Workspace>,
59 editor: ViewHandle<Editor>,
60 excerpts: ModelHandle<MultiBuffer>,
61 path_states: Vec<PathState>,
62 paths_to_update: BTreeSet<ProjectPath>,
63 build_settings: BuildSettings,
64 settings: watch::Receiver<workspace::Settings>,
65}
66
67struct PathState {
68 path: ProjectPath,
69 header: Option<BlockId>,
70 diagnostic_groups: Vec<DiagnosticGroupState>,
71}
72
73struct DiagnosticGroupState {
74 primary_diagnostic: DiagnosticEntry<language::Anchor>,
75 primary_excerpt_ix: usize,
76 excerpts: Vec<ExcerptId>,
77 blocks: HashSet<BlockId>,
78 block_count: usize,
79}
80
81impl ProjectDiagnostics {
82 fn new(project: ModelHandle<Project>) -> Self {
83 Self { project }
84 }
85}
86
87impl Entity for ProjectDiagnostics {
88 type Event = ();
89}
90
91impl Entity for ProjectDiagnosticsEditor {
92 type Event = Event;
93}
94
95impl View for ProjectDiagnosticsEditor {
96 fn ui_name() -> &'static str {
97 "ProjectDiagnosticsEditor"
98 }
99
100 fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
101 if self.path_states.is_empty() {
102 let theme = &self.settings.borrow().theme.project_diagnostics;
103 Label::new(
104 "No problems detected in the project".to_string(),
105 theme.empty_message.clone(),
106 )
107 .aligned()
108 .contained()
109 .with_style(theme.container)
110 .boxed()
111 } else {
112 ChildView::new(self.editor.id()).boxed()
113 }
114 }
115
116 fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
117 if !self.path_states.is_empty() {
118 cx.focus(&self.editor);
119 }
120 }
121}
122
123impl ProjectDiagnosticsEditor {
124 fn new(
125 model: ModelHandle<ProjectDiagnostics>,
126 workspace: WeakViewHandle<Workspace>,
127 settings: watch::Receiver<workspace::Settings>,
128 cx: &mut ViewContext<Self>,
129 ) -> Self {
130 let project = model.read(cx).project.clone();
131 cx.subscribe(&project, |this, _, event, cx| match event {
132 project::Event::DiskBasedDiagnosticsFinished => {
133 let paths = mem::take(&mut this.paths_to_update);
134 this.update_excerpts(paths, cx);
135 }
136 project::Event::DiagnosticsUpdated(path) => {
137 this.paths_to_update.insert(path.clone());
138 }
139 _ => {}
140 })
141 .detach();
142
143 let excerpts = cx.add_model(|cx| MultiBuffer::new(project.read(cx).replica_id()));
144 let build_settings = editor::settings_builder(excerpts.downgrade(), settings.clone());
145 let editor =
146 cx.add_view(|cx| Editor::for_buffer(excerpts.clone(), build_settings.clone(), cx));
147 cx.subscribe(&editor, |_, _, event, cx| cx.emit(*event))
148 .detach();
149
150 let paths_to_update = project
151 .read(cx)
152 .diagnostic_summaries(cx)
153 .map(|e| e.0)
154 .collect();
155 let this = Self {
156 model,
157 workspace,
158 excerpts,
159 editor,
160 build_settings,
161 settings,
162 path_states: Default::default(),
163 paths_to_update: Default::default(),
164 };
165 this.update_excerpts(paths_to_update, cx);
166 this
167 }
168
169 fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext<Workspace>) {
170 if let Some(existing) = workspace.item_of_type::<ProjectDiagnostics>(cx) {
171 workspace.activate_item(&existing, cx);
172 } else {
173 let diagnostics =
174 cx.add_model(|_| ProjectDiagnostics::new(workspace.project().clone()));
175 workspace.open_item(diagnostics, cx);
176 }
177 }
178
179 fn open_excerpts(&mut self, _: &OpenExcerpts, cx: &mut ViewContext<Self>) {
180 if let Some(workspace) = self.workspace.upgrade(cx) {
181 let editor = self.editor.read(cx);
182 let excerpts = self.excerpts.read(cx);
183 let mut new_selections_by_buffer = HashMap::default();
184
185 for selection in editor.local_selections::<usize>(cx) {
186 for (buffer, mut range) in
187 excerpts.excerpted_buffers(selection.start..selection.end, cx)
188 {
189 if selection.reversed {
190 mem::swap(&mut range.start, &mut range.end);
191 }
192 new_selections_by_buffer
193 .entry(buffer)
194 .or_insert(Vec::new())
195 .push(range)
196 }
197 }
198
199 // We defer the pane interaction because we ourselves are a workspace item
200 // and activating a new item causes the pane to call a method on us reentrantly,
201 // which panics if we're on the stack.
202 workspace.defer(cx, |workspace, cx| {
203 for (buffer, ranges) in new_selections_by_buffer {
204 let buffer = BufferItemHandle(buffer);
205 if !workspace.activate_pane_for_item(&buffer, cx) {
206 workspace.activate_next_pane(cx);
207 }
208 let editor = workspace
209 .open_item(buffer, cx)
210 .downcast::<Editor>()
211 .unwrap();
212 editor.update(cx, |editor, cx| {
213 editor.select_ranges(ranges, Some(Autoscroll::Center), cx)
214 });
215 }
216 });
217 }
218 }
219
220 fn update_excerpts(&self, paths: BTreeSet<ProjectPath>, cx: &mut ViewContext<Self>) {
221 let project = self.model.read(cx).project.clone();
222 cx.spawn(|this, mut cx| {
223 async move {
224 for path in paths {
225 let buffer = project
226 .update(&mut cx, |project, cx| project.open_buffer(path.clone(), cx))
227 .await?;
228 this.update(&mut cx, |view, cx| view.populate_excerpts(path, buffer, cx))
229 }
230 Result::<_, anyhow::Error>::Ok(())
231 }
232 .log_err()
233 })
234 .detach();
235 }
236
237 fn populate_excerpts(
238 &mut self,
239 path: ProjectPath,
240 buffer: ModelHandle<Buffer>,
241 cx: &mut ViewContext<Self>,
242 ) {
243 let was_empty = self.path_states.is_empty();
244 let snapshot = buffer.read(cx).snapshot();
245 let path_ix = match self.path_states.binary_search_by_key(&&path, |e| &e.path) {
246 Ok(ix) => ix,
247 Err(ix) => {
248 self.path_states.insert(
249 ix,
250 PathState {
251 path: path.clone(),
252 header: None,
253 diagnostic_groups: Default::default(),
254 },
255 );
256 ix
257 }
258 };
259
260 let mut prev_excerpt_id = if path_ix > 0 {
261 let prev_path_last_group = &self.path_states[path_ix - 1]
262 .diagnostic_groups
263 .last()
264 .unwrap();
265 prev_path_last_group.excerpts.last().unwrap().clone()
266 } else {
267 ExcerptId::min()
268 };
269
270 let path_state = &mut self.path_states[path_ix];
271 let mut groups_to_add = Vec::new();
272 let mut group_ixs_to_remove = Vec::new();
273 let mut blocks_to_add = Vec::new();
274 let mut blocks_to_remove = HashSet::default();
275 let mut first_excerpt_id = None;
276 let excerpts_snapshot = self.excerpts.update(cx, |excerpts, excerpts_cx| {
277 let mut old_groups = path_state.diagnostic_groups.iter().enumerate().peekable();
278 let mut new_groups = snapshot
279 .diagnostic_groups()
280 .into_iter()
281 .filter(|group| group.entries[group.primary_ix].diagnostic.is_disk_based)
282 .peekable();
283
284 loop {
285 let mut to_insert = None;
286 let mut to_remove = None;
287 let mut to_keep = None;
288 match (old_groups.peek(), new_groups.peek()) {
289 (None, None) => break,
290 (None, Some(_)) => to_insert = new_groups.next(),
291 (Some(_), None) => to_remove = old_groups.next(),
292 (Some((_, old_group)), Some(new_group)) => {
293 let old_primary = &old_group.primary_diagnostic;
294 let new_primary = &new_group.entries[new_group.primary_ix];
295 match compare_diagnostics(old_primary, new_primary, &snapshot) {
296 Ordering::Less => to_remove = old_groups.next(),
297 Ordering::Equal => {
298 to_keep = old_groups.next();
299 new_groups.next();
300 }
301 Ordering::Greater => to_insert = new_groups.next(),
302 }
303 }
304 }
305
306 if let Some(group) = to_insert {
307 let mut group_state = DiagnosticGroupState {
308 primary_diagnostic: group.entries[group.primary_ix].clone(),
309 primary_excerpt_ix: 0,
310 excerpts: Default::default(),
311 blocks: Default::default(),
312 block_count: 0,
313 };
314 let mut pending_range: Option<(Range<Point>, usize)> = None;
315 let mut is_first_excerpt_for_group = true;
316 for (ix, entry) in group.entries.iter().map(Some).chain([None]).enumerate() {
317 let resolved_entry = entry.map(|e| e.resolve::<Point>(&snapshot));
318 if let Some((range, start_ix)) = &mut pending_range {
319 if let Some(entry) = resolved_entry.as_ref() {
320 if entry.range.start.row
321 <= range.end.row + 1 + CONTEXT_LINE_COUNT * 2
322 {
323 range.end = range.end.max(entry.range.end);
324 continue;
325 }
326 }
327
328 let excerpt_start =
329 Point::new(range.start.row.saturating_sub(CONTEXT_LINE_COUNT), 0);
330 let excerpt_end = snapshot.clip_point(
331 Point::new(range.end.row + CONTEXT_LINE_COUNT, u32::MAX),
332 Bias::Left,
333 );
334 let excerpt_id = excerpts.insert_excerpt_after(
335 &prev_excerpt_id,
336 ExcerptProperties {
337 buffer: &buffer,
338 range: excerpt_start..excerpt_end,
339 },
340 excerpts_cx,
341 );
342
343 prev_excerpt_id = excerpt_id.clone();
344 first_excerpt_id.get_or_insert_with(|| prev_excerpt_id.clone());
345 group_state.excerpts.push(excerpt_id.clone());
346 let header_position = (excerpt_id.clone(), language::Anchor::min());
347
348 if is_first_excerpt_for_group {
349 is_first_excerpt_for_group = false;
350 let mut primary =
351 group.entries[group.primary_ix].diagnostic.clone();
352 primary.message =
353 primary.message.split('\n').next().unwrap().to_string();
354 group_state.block_count += 1;
355 blocks_to_add.push(BlockProperties {
356 position: header_position,
357 height: 2,
358 render: diagnostic_header_renderer(
359 primary,
360 self.build_settings.clone(),
361 ),
362 disposition: BlockDisposition::Above,
363 });
364 } else {
365 group_state.block_count += 1;
366 blocks_to_add.push(BlockProperties {
367 position: header_position,
368 height: 1,
369 render: context_header_renderer(self.build_settings.clone()),
370 disposition: BlockDisposition::Above,
371 });
372 }
373
374 for entry in &group.entries[*start_ix..ix] {
375 let mut diagnostic = entry.diagnostic.clone();
376 if diagnostic.is_primary {
377 group_state.primary_excerpt_ix = group_state.excerpts.len() - 1;
378 diagnostic.message =
379 entry.diagnostic.message.split('\n').skip(1).collect();
380 }
381
382 if !diagnostic.message.is_empty() {
383 group_state.block_count += 1;
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_remove {
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.iter().copied());
410 } else if let Some((_, group)) = to_keep {
411 prev_excerpt_id = group.excerpts.last().unwrap().clone();
412 first_excerpt_id.get_or_insert_with(|| prev_excerpt_id.clone());
413 }
414 }
415
416 excerpts.snapshot(excerpts_cx)
417 });
418
419 self.editor.update(cx, |editor, cx| {
420 blocks_to_remove.extend(path_state.header);
421 editor.remove_blocks(blocks_to_remove, cx);
422 let header_block = first_excerpt_id.map(|excerpt_id| BlockProperties {
423 position: excerpts_snapshot.anchor_in_excerpt(excerpt_id, language::Anchor::min()),
424 height: 2,
425 render: path_header_renderer(buffer, self.build_settings.clone()),
426 disposition: BlockDisposition::Above,
427 });
428 let block_ids = editor.insert_blocks(
429 blocks_to_add
430 .into_iter()
431 .map(|block| {
432 let (excerpt_id, text_anchor) = block.position;
433 BlockProperties {
434 position: excerpts_snapshot.anchor_in_excerpt(excerpt_id, text_anchor),
435 height: block.height,
436 render: block.render,
437 disposition: block.disposition,
438 }
439 })
440 .chain(header_block.into_iter()),
441 cx,
442 );
443
444 let mut block_ids = block_ids.into_iter();
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 path_state.header = block_ids.next();
449 });
450
451 for ix in group_ixs_to_remove.into_iter().rev() {
452 path_state.diagnostic_groups.remove(ix);
453 }
454 path_state.diagnostic_groups.extend(groups_to_add);
455 path_state.diagnostic_groups.sort_unstable_by(|a, b| {
456 let range_a = &a.primary_diagnostic.range;
457 let range_b = &b.primary_diagnostic.range;
458 range_a
459 .start
460 .cmp(&range_b.start, &snapshot)
461 .unwrap()
462 .then_with(|| range_a.end.cmp(&range_b.end, &snapshot).unwrap())
463 });
464
465 if path_state.diagnostic_groups.is_empty() {
466 self.path_states.remove(path_ix);
467 }
468
469 self.editor.update(cx, |editor, cx| {
470 let groups;
471 let mut selections;
472 let new_excerpt_ids_by_selection_id;
473 if was_empty {
474 groups = self.path_states.first()?.diagnostic_groups.as_slice();
475 new_excerpt_ids_by_selection_id = [(0, ExcerptId::min())].into_iter().collect();
476 selections = vec![Selection {
477 id: 0,
478 start: 0,
479 end: 0,
480 reversed: false,
481 goal: SelectionGoal::None,
482 }];
483 } else {
484 groups = self.path_states.get(path_ix)?.diagnostic_groups.as_slice();
485 new_excerpt_ids_by_selection_id = editor.refresh_selections(cx);
486 selections = editor.local_selections::<usize>(cx);
487 }
488
489 // If any selection has lost its position, move it to start of the next primary diagnostic.
490 for selection in &mut selections {
491 if let Some(new_excerpt_id) = new_excerpt_ids_by_selection_id.get(&selection.id) {
492 let group_ix = match groups.binary_search_by(|probe| {
493 probe.excerpts.last().unwrap().cmp(&new_excerpt_id)
494 }) {
495 Ok(ix) | Err(ix) => ix,
496 };
497 if let Some(group) = groups.get(group_ix) {
498 let offset = excerpts_snapshot
499 .anchor_in_excerpt(
500 group.excerpts[group.primary_excerpt_ix].clone(),
501 group.primary_diagnostic.range.start.clone(),
502 )
503 .to_offset(&excerpts_snapshot);
504 selection.start = offset;
505 selection.end = offset;
506 }
507 }
508 }
509 editor.update_selections(selections, None, cx);
510 Some(())
511 });
512
513 if self.path_states.is_empty() {
514 if self.editor.is_focused(cx) {
515 cx.focus_self();
516 }
517 } else {
518 if cx.handle().is_focused(cx) {
519 cx.focus(&self.editor);
520 }
521 }
522 cx.notify();
523 }
524}
525
526impl workspace::Item for ProjectDiagnostics {
527 type View = ProjectDiagnosticsEditor;
528
529 fn build_view(
530 handle: ModelHandle<Self>,
531 workspace: &Workspace,
532 nav_history: ItemNavHistory,
533 cx: &mut ViewContext<Self::View>,
534 ) -> Self::View {
535 let diagnostics = ProjectDiagnosticsEditor::new(
536 handle,
537 workspace.weak_handle(),
538 workspace.settings(),
539 cx,
540 );
541 diagnostics
542 .editor
543 .update(cx, |editor, _| editor.set_nav_history(Some(nav_history)));
544 diagnostics
545 }
546
547 fn project_path(&self) -> Option<project::ProjectPath> {
548 None
549 }
550}
551
552impl workspace::ItemView for ProjectDiagnosticsEditor {
553 type ItemHandle = ModelHandle<ProjectDiagnostics>;
554
555 fn item_handle(&self, _: &AppContext) -> Self::ItemHandle {
556 self.model.clone()
557 }
558
559 fn title(&self, _: &AppContext) -> String {
560 "Project Diagnostics".to_string()
561 }
562
563 fn project_path(&self, _: &AppContext) -> Option<project::ProjectPath> {
564 None
565 }
566
567 fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) {
568 self.editor
569 .update(cx, |editor, cx| editor.navigate(data, cx));
570 }
571
572 fn is_dirty(&self, cx: &AppContext) -> bool {
573 self.excerpts.read(cx).read(cx).is_dirty()
574 }
575
576 fn has_conflict(&self, cx: &AppContext) -> bool {
577 self.excerpts.read(cx).read(cx).has_conflict()
578 }
579
580 fn can_save(&self, _: &AppContext) -> bool {
581 true
582 }
583
584 fn save(&mut self, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
585 self.excerpts.update(cx, |excerpts, cx| excerpts.save(cx))
586 }
587
588 fn can_save_as(&self, _: &AppContext) -> bool {
589 false
590 }
591
592 fn save_as(
593 &mut self,
594 _: ModelHandle<Project>,
595 _: PathBuf,
596 _: &mut ViewContext<Self>,
597 ) -> Task<Result<()>> {
598 unreachable!()
599 }
600
601 fn should_activate_item_on_event(event: &Self::Event) -> bool {
602 Editor::should_activate_item_on_event(event)
603 }
604
605 fn should_update_tab_on_event(event: &Event) -> bool {
606 matches!(
607 event,
608 Event::Saved | Event::Dirtied | Event::FileHandleChanged
609 )
610 }
611
612 fn clone_on_split(&self, cx: &mut ViewContext<Self>) -> Option<Self>
613 where
614 Self: Sized,
615 {
616 let diagnostics = ProjectDiagnosticsEditor::new(
617 self.model.clone(),
618 self.workspace.clone(),
619 self.settings.clone(),
620 cx,
621 );
622 diagnostics.editor.update(cx, |editor, cx| {
623 let nav_history = self
624 .editor
625 .read(cx)
626 .nav_history()
627 .map(|nav_history| ItemNavHistory::new(nav_history.history(), &cx.handle()));
628 editor.set_nav_history(nav_history);
629 });
630 Some(diagnostics)
631 }
632
633 fn act_as_type(
634 &self,
635 type_id: TypeId,
636 self_handle: &ViewHandle<Self>,
637 _: &AppContext,
638 ) -> Option<AnyViewHandle> {
639 if type_id == TypeId::of::<Self>() {
640 Some(self_handle.into())
641 } else if type_id == TypeId::of::<Editor>() {
642 Some((&self.editor).into())
643 } else {
644 None
645 }
646 }
647
648 fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
649 self.editor.update(cx, |editor, cx| editor.deactivated(cx));
650 }
651}
652
653fn path_header_renderer(buffer: ModelHandle<Buffer>, build_settings: BuildSettings) -> RenderBlock {
654 Arc::new(move |cx| {
655 let settings = build_settings(cx);
656 let style = settings.style.diagnostic_path_header;
657
658 let mut filename = None;
659 let mut path = None;
660 if let Some(file) = buffer.read(&**cx).file() {
661 filename = file
662 .path()
663 .file_name()
664 .map(|f| f.to_string_lossy().to_string());
665 path = file
666 .path()
667 .parent()
668 .map(|p| p.to_string_lossy().to_string() + "/");
669 }
670
671 Flex::row()
672 .with_child(
673 Label::new(
674 filename.unwrap_or_else(|| "untitled".to_string()),
675 style.filename.text.clone(),
676 )
677 .contained()
678 .with_style(style.filename.container)
679 .boxed(),
680 )
681 .with_children(path.map(|path| {
682 Label::new(path, style.path.text.clone())
683 .contained()
684 .with_style(style.path.container)
685 .boxed()
686 }))
687 .aligned()
688 .left()
689 .contained()
690 .with_style(style.container)
691 .with_padding_left(cx.line_number_x)
692 .expanded()
693 .named("path header block")
694 })
695}
696
697fn diagnostic_header_renderer(
698 diagnostic: Diagnostic,
699 build_settings: BuildSettings,
700) -> RenderBlock {
701 let (message, highlights) = highlight_diagnostic_message(&diagnostic.message);
702 Arc::new(move |cx| {
703 let settings = build_settings(cx);
704 let style = &settings.style.diagnostic_header;
705 let icon = if diagnostic.severity == DiagnosticSeverity::ERROR {
706 Svg::new("icons/diagnostic-error-10.svg")
707 .with_color(settings.style.error_diagnostic.message.text.color)
708 } else {
709 Svg::new("icons/diagnostic-warning-10.svg")
710 .with_color(settings.style.warning_diagnostic.message.text.color)
711 };
712
713 Flex::row()
714 .with_child(
715 icon.constrained()
716 .with_height(style.icon.width)
717 .aligned()
718 .contained()
719 .with_style(style.icon.container)
720 .boxed(),
721 )
722 .with_child(
723 Label::new(message.clone(), style.message.label.clone())
724 .with_highlights(highlights.clone())
725 .contained()
726 .with_style(style.message.container)
727 .aligned()
728 .boxed(),
729 )
730 .with_children(diagnostic.code.clone().map(|code| {
731 Label::new(code, style.code.text.clone())
732 .contained()
733 .with_style(style.code.container)
734 .aligned()
735 .boxed()
736 }))
737 .contained()
738 .with_style(style.container)
739 .with_padding_left(cx.line_number_x)
740 .expanded()
741 .named("diagnostic header")
742 })
743}
744
745fn context_header_renderer(build_settings: BuildSettings) -> RenderBlock {
746 Arc::new(move |cx| {
747 let settings = build_settings(cx);
748 let text_style = settings.style.text.clone();
749 Label::new("…".to_string(), text_style)
750 .contained()
751 .with_padding_left(cx.line_number_x)
752 .named("collapsed context")
753 })
754}
755
756fn compare_diagnostics<L: language::ToOffset, R: language::ToOffset>(
757 lhs: &DiagnosticEntry<L>,
758 rhs: &DiagnosticEntry<R>,
759 snapshot: &language::BufferSnapshot,
760) -> Ordering {
761 lhs.range
762 .start
763 .to_offset(&snapshot)
764 .cmp(&rhs.range.start.to_offset(snapshot))
765 .then_with(|| {
766 lhs.range
767 .end
768 .to_offset(&snapshot)
769 .cmp(&rhs.range.end.to_offset(snapshot))
770 })
771 .then_with(|| lhs.diagnostic.message.cmp(&rhs.diagnostic.message))
772}
773
774#[cfg(test)]
775mod tests {
776 use super::*;
777 use editor::{display_map::BlockContext, DisplayPoint, EditorSnapshot};
778 use gpui::TestAppContext;
779 use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16};
780 use serde_json::json;
781 use unindent::Unindent as _;
782 use workspace::WorkspaceParams;
783
784 #[gpui::test]
785 async fn test_diagnostics(mut cx: TestAppContext) {
786 let params = cx.update(WorkspaceParams::test);
787 let project = params.project.clone();
788 let workspace = cx.add_view(0, |cx| Workspace::new(¶ms, cx));
789
790 params
791 .fs
792 .as_fake()
793 .insert_tree(
794 "/test",
795 json!({
796 "consts.rs": "
797 const a: i32 = 'a';
798 const b: i32 = c;
799 "
800 .unindent(),
801
802 "main.rs": "
803 fn main() {
804 let x = vec![];
805 let y = vec![];
806 a(x);
807 b(y);
808 // comment 1
809 // comment 2
810 c(y);
811 d(x);
812 }
813 "
814 .unindent(),
815 }),
816 )
817 .await;
818
819 project
820 .update(&mut cx, |project, cx| {
821 project.find_or_create_local_worktree("/test", false, cx)
822 })
823 .await
824 .unwrap();
825
826 // Create some diagnostics
827 project.update(&mut cx, |project, cx| {
828 project
829 .update_diagnostic_entries(
830 PathBuf::from("/test/main.rs"),
831 None,
832 vec![
833 DiagnosticEntry {
834 range: PointUtf16::new(1, 8)..PointUtf16::new(1, 9),
835 diagnostic: Diagnostic {
836 message:
837 "move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait"
838 .to_string(),
839 severity: DiagnosticSeverity::INFORMATION,
840 is_primary: false,
841 is_disk_based: true,
842 group_id: 1,
843 ..Default::default()
844 },
845 },
846 DiagnosticEntry {
847 range: PointUtf16::new(2, 8)..PointUtf16::new(2, 9),
848 diagnostic: Diagnostic {
849 message:
850 "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait"
851 .to_string(),
852 severity: DiagnosticSeverity::INFORMATION,
853 is_primary: false,
854 is_disk_based: true,
855 group_id: 0,
856 ..Default::default()
857 },
858 },
859 DiagnosticEntry {
860 range: PointUtf16::new(3, 6)..PointUtf16::new(3, 7),
861 diagnostic: Diagnostic {
862 message: "value moved here".to_string(),
863 severity: DiagnosticSeverity::INFORMATION,
864 is_primary: false,
865 is_disk_based: true,
866 group_id: 1,
867 ..Default::default()
868 },
869 },
870 DiagnosticEntry {
871 range: PointUtf16::new(4, 6)..PointUtf16::new(4, 7),
872 diagnostic: Diagnostic {
873 message: "value moved here".to_string(),
874 severity: DiagnosticSeverity::INFORMATION,
875 is_primary: false,
876 is_disk_based: true,
877 group_id: 0,
878 ..Default::default()
879 },
880 },
881 DiagnosticEntry {
882 range: PointUtf16::new(7, 6)..PointUtf16::new(7, 7),
883 diagnostic: Diagnostic {
884 message: "use of moved value\nvalue used here after move".to_string(),
885 severity: DiagnosticSeverity::ERROR,
886 is_primary: true,
887 is_disk_based: true,
888 group_id: 0,
889 ..Default::default()
890 },
891 },
892 DiagnosticEntry {
893 range: PointUtf16::new(8, 6)..PointUtf16::new(8, 7),
894 diagnostic: Diagnostic {
895 message: "use of moved value\nvalue used here after move".to_string(),
896 severity: DiagnosticSeverity::ERROR,
897 is_primary: true,
898 is_disk_based: true,
899 group_id: 1,
900 ..Default::default()
901 },
902 },
903 ],
904 cx,
905 )
906 .unwrap();
907 });
908
909 // Open the project diagnostics view while there are already diagnostics.
910 let model = cx.add_model(|_| ProjectDiagnostics::new(project.clone()));
911 let view = cx.add_view(0, |cx| {
912 ProjectDiagnosticsEditor::new(model, workspace.downgrade(), params.settings, cx)
913 });
914
915 view.next_notification(&cx).await;
916 view.update(&mut cx, |view, cx| {
917 let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
918
919 assert_eq!(
920 editor_blocks(&editor, cx),
921 [
922 (0, "path header block".into()),
923 (2, "diagnostic header".into()),
924 (15, "diagnostic header".into()),
925 (24, "collapsed context".into()),
926 ]
927 );
928 assert_eq!(
929 editor.text(),
930 concat!(
931 //
932 // main.rs
933 //
934 "\n", // filename
935 "\n", // padding
936 // diagnostic group 1
937 "\n", // primary message
938 "\n", // padding
939 " let x = vec![];\n",
940 " let y = vec![];\n",
941 "\n", // supporting diagnostic
942 " a(x);\n",
943 " b(y);\n",
944 "\n", // supporting diagnostic
945 " // comment 1\n",
946 " // comment 2\n",
947 " c(y);\n",
948 "\n", // supporting diagnostic
949 " d(x);\n",
950 // diagnostic group 2
951 "\n", // primary message
952 "\n", // padding
953 "fn main() {\n",
954 " let x = vec![];\n",
955 "\n", // supporting diagnostic
956 " let y = vec![];\n",
957 " a(x);\n",
958 "\n", // supporting diagnostic
959 " b(y);\n",
960 "\n", // context ellipsis
961 " c(y);\n",
962 " d(x);\n",
963 "\n", // supporting diagnostic
964 "}"
965 )
966 );
967
968 // Cursor is at the first diagnostic
969 view.editor.update(cx, |editor, cx| {
970 assert_eq!(
971 editor.selected_display_ranges(cx),
972 [DisplayPoint::new(12, 6)..DisplayPoint::new(12, 6)]
973 );
974 });
975 });
976
977 // Diagnostics are added for another earlier path.
978 project.update(&mut cx, |project, cx| {
979 project.disk_based_diagnostics_started(cx);
980 project
981 .update_diagnostic_entries(
982 PathBuf::from("/test/consts.rs"),
983 None,
984 vec![DiagnosticEntry {
985 range: PointUtf16::new(0, 15)..PointUtf16::new(0, 15),
986 diagnostic: Diagnostic {
987 message: "mismatched types\nexpected `usize`, found `char`".to_string(),
988 severity: DiagnosticSeverity::ERROR,
989 is_primary: true,
990 is_disk_based: true,
991 group_id: 0,
992 ..Default::default()
993 },
994 }],
995 cx,
996 )
997 .unwrap();
998 project.disk_based_diagnostics_finished(cx);
999 });
1000
1001 view.next_notification(&cx).await;
1002 view.update(&mut cx, |view, cx| {
1003 let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
1004
1005 assert_eq!(
1006 editor_blocks(&editor, cx),
1007 [
1008 (0, "path header block".into()),
1009 (2, "diagnostic header".into()),
1010 (7, "path header block".into()),
1011 (9, "diagnostic header".into()),
1012 (22, "diagnostic header".into()),
1013 (31, "collapsed context".into()),
1014 ]
1015 );
1016 assert_eq!(
1017 editor.text(),
1018 concat!(
1019 //
1020 // consts.rs
1021 //
1022 "\n", // filename
1023 "\n", // padding
1024 // diagnostic group 1
1025 "\n", // primary message
1026 "\n", // padding
1027 "const a: i32 = 'a';\n",
1028 "\n", // supporting diagnostic
1029 "const b: i32 = c;\n",
1030 //
1031 // main.rs
1032 //
1033 "\n", // filename
1034 "\n", // padding
1035 // diagnostic group 1
1036 "\n", // primary message
1037 "\n", // padding
1038 " let x = vec![];\n",
1039 " let y = vec![];\n",
1040 "\n", // supporting diagnostic
1041 " a(x);\n",
1042 " b(y);\n",
1043 "\n", // supporting diagnostic
1044 " // comment 1\n",
1045 " // comment 2\n",
1046 " c(y);\n",
1047 "\n", // supporting diagnostic
1048 " d(x);\n",
1049 // diagnostic group 2
1050 "\n", // primary message
1051 "\n", // filename
1052 "fn main() {\n",
1053 " let x = vec![];\n",
1054 "\n", // supporting diagnostic
1055 " let y = vec![];\n",
1056 " a(x);\n",
1057 "\n", // supporting diagnostic
1058 " b(y);\n",
1059 "\n", // context ellipsis
1060 " c(y);\n",
1061 " d(x);\n",
1062 "\n", // supporting diagnostic
1063 "}"
1064 )
1065 );
1066
1067 // Cursor keeps its position.
1068 view.editor.update(cx, |editor, cx| {
1069 assert_eq!(
1070 editor.selected_display_ranges(cx),
1071 [DisplayPoint::new(19, 6)..DisplayPoint::new(19, 6)]
1072 );
1073 });
1074 });
1075
1076 // Diagnostics are added to the first path
1077 project.update(&mut cx, |project, cx| {
1078 project.disk_based_diagnostics_started(cx);
1079 project
1080 .update_diagnostic_entries(
1081 PathBuf::from("/test/consts.rs"),
1082 None,
1083 vec![
1084 DiagnosticEntry {
1085 range: PointUtf16::new(0, 15)..PointUtf16::new(0, 15),
1086 diagnostic: Diagnostic {
1087 message: "mismatched types\nexpected `usize`, found `char`"
1088 .to_string(),
1089 severity: DiagnosticSeverity::ERROR,
1090 is_primary: true,
1091 is_disk_based: true,
1092 group_id: 0,
1093 ..Default::default()
1094 },
1095 },
1096 DiagnosticEntry {
1097 range: PointUtf16::new(1, 15)..PointUtf16::new(1, 15),
1098 diagnostic: Diagnostic {
1099 message: "unresolved name `c`".to_string(),
1100 severity: DiagnosticSeverity::ERROR,
1101 is_primary: true,
1102 is_disk_based: true,
1103 group_id: 1,
1104 ..Default::default()
1105 },
1106 },
1107 ],
1108 cx,
1109 )
1110 .unwrap();
1111 project.disk_based_diagnostics_finished(cx);
1112 });
1113
1114 view.next_notification(&cx).await;
1115 view.update(&mut cx, |view, cx| {
1116 let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
1117
1118 assert_eq!(
1119 editor_blocks(&editor, cx),
1120 [
1121 (0, "path header block".into()),
1122 (2, "diagnostic header".into()),
1123 (7, "diagnostic header".into()),
1124 (12, "path header block".into()),
1125 (14, "diagnostic header".into()),
1126 (27, "diagnostic header".into()),
1127 (36, "collapsed context".into()),
1128 ]
1129 );
1130 assert_eq!(
1131 editor.text(),
1132 concat!(
1133 //
1134 // consts.rs
1135 //
1136 "\n", // filename
1137 "\n", // padding
1138 // diagnostic group 1
1139 "\n", // primary message
1140 "\n", // padding
1141 "const a: i32 = 'a';\n",
1142 "\n", // supporting diagnostic
1143 "const b: i32 = c;\n",
1144 // diagnostic group 2
1145 "\n", // primary message
1146 "\n", // padding
1147 "const a: i32 = 'a';\n",
1148 "const b: i32 = c;\n",
1149 "\n", // supporting diagnostic
1150 //
1151 // main.rs
1152 //
1153 "\n", // filename
1154 "\n", // padding
1155 // diagnostic group 1
1156 "\n", // primary message
1157 "\n", // padding
1158 " let x = vec![];\n",
1159 " let y = vec![];\n",
1160 "\n", // supporting diagnostic
1161 " a(x);\n",
1162 " b(y);\n",
1163 "\n", // supporting diagnostic
1164 " // comment 1\n",
1165 " // comment 2\n",
1166 " c(y);\n",
1167 "\n", // supporting diagnostic
1168 " d(x);\n",
1169 // diagnostic group 2
1170 "\n", // primary message
1171 "\n", // filename
1172 "fn main() {\n",
1173 " let x = vec![];\n",
1174 "\n", // supporting diagnostic
1175 " let y = vec![];\n",
1176 " a(x);\n",
1177 "\n", // supporting diagnostic
1178 " b(y);\n",
1179 "\n", // context ellipsis
1180 " c(y);\n",
1181 " d(x);\n",
1182 "\n", // supporting diagnostic
1183 "}"
1184 )
1185 );
1186 });
1187 }
1188
1189 fn editor_blocks(editor: &EditorSnapshot, cx: &AppContext) -> Vec<(u32, String)> {
1190 editor
1191 .blocks_in_range(0..editor.max_point().row())
1192 .filter_map(|(row, block)| {
1193 block
1194 .render(&BlockContext {
1195 cx,
1196 anchor_x: 0.,
1197 line_number_x: 0.,
1198 })
1199 .name()
1200 .map(|s| (row, s.to_string()))
1201 })
1202 .collect()
1203 }
1204}