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.gutter_padding)
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_width = cx.em_width * style.icon_width_factor;
706 let icon = if diagnostic.severity == DiagnosticSeverity::ERROR {
707 Svg::new("icons/diagnostic-error-10.svg")
708 .with_color(settings.style.error_diagnostic.message.text.color)
709 } else {
710 Svg::new("icons/diagnostic-warning-10.svg")
711 .with_color(settings.style.warning_diagnostic.message.text.color)
712 };
713
714 Flex::row()
715 .with_child(
716 icon.constrained()
717 .with_width(icon_width)
718 .aligned()
719 .contained()
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 .with_margin_left(cx.gutter_padding)
728 .aligned()
729 .boxed(),
730 )
731 .with_children(diagnostic.code.clone().map(|code| {
732 Label::new(code, style.code.text.clone())
733 .contained()
734 .with_style(style.code.container)
735 .aligned()
736 .boxed()
737 }))
738 .contained()
739 .with_style(style.container)
740 .with_padding_left(cx.gutter_width - cx.gutter_padding - icon_width)
741 .expanded()
742 .named("diagnostic header")
743 })
744}
745
746fn context_header_renderer(build_settings: BuildSettings) -> RenderBlock {
747 Arc::new(move |cx| {
748 let settings = build_settings(cx);
749 let text_style = settings.style.text.clone();
750 Label::new("…".to_string(), text_style)
751 .contained()
752 .with_padding_left(cx.gutter_padding)
753 .named("collapsed context")
754 })
755}
756
757fn compare_diagnostics<L: language::ToOffset, R: language::ToOffset>(
758 lhs: &DiagnosticEntry<L>,
759 rhs: &DiagnosticEntry<R>,
760 snapshot: &language::BufferSnapshot,
761) -> Ordering {
762 lhs.range
763 .start
764 .to_offset(&snapshot)
765 .cmp(&rhs.range.start.to_offset(snapshot))
766 .then_with(|| {
767 lhs.range
768 .end
769 .to_offset(&snapshot)
770 .cmp(&rhs.range.end.to_offset(snapshot))
771 })
772 .then_with(|| lhs.diagnostic.message.cmp(&rhs.diagnostic.message))
773}
774
775#[cfg(test)]
776mod tests {
777 use super::*;
778 use editor::{display_map::BlockContext, DisplayPoint, EditorSnapshot};
779 use gpui::TestAppContext;
780 use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16};
781 use serde_json::json;
782 use unindent::Unindent as _;
783 use workspace::WorkspaceParams;
784
785 #[gpui::test]
786 async fn test_diagnostics(mut cx: TestAppContext) {
787 let params = cx.update(WorkspaceParams::test);
788 let project = params.project.clone();
789 let workspace = cx.add_view(0, |cx| Workspace::new(¶ms, cx));
790
791 params
792 .fs
793 .as_fake()
794 .insert_tree(
795 "/test",
796 json!({
797 "consts.rs": "
798 const a: i32 = 'a';
799 const b: i32 = c;
800 "
801 .unindent(),
802
803 "main.rs": "
804 fn main() {
805 let x = vec![];
806 let y = vec![];
807 a(x);
808 b(y);
809 // comment 1
810 // comment 2
811 c(y);
812 d(x);
813 }
814 "
815 .unindent(),
816 }),
817 )
818 .await;
819
820 project
821 .update(&mut cx, |project, cx| {
822 project.find_or_create_local_worktree("/test", false, cx)
823 })
824 .await
825 .unwrap();
826
827 // Create some diagnostics
828 project.update(&mut cx, |project, cx| {
829 project
830 .update_diagnostic_entries(
831 PathBuf::from("/test/main.rs"),
832 None,
833 vec![
834 DiagnosticEntry {
835 range: PointUtf16::new(1, 8)..PointUtf16::new(1, 9),
836 diagnostic: Diagnostic {
837 message:
838 "move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait"
839 .to_string(),
840 severity: DiagnosticSeverity::INFORMATION,
841 is_primary: false,
842 is_disk_based: true,
843 group_id: 1,
844 ..Default::default()
845 },
846 },
847 DiagnosticEntry {
848 range: PointUtf16::new(2, 8)..PointUtf16::new(2, 9),
849 diagnostic: Diagnostic {
850 message:
851 "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait"
852 .to_string(),
853 severity: DiagnosticSeverity::INFORMATION,
854 is_primary: false,
855 is_disk_based: true,
856 group_id: 0,
857 ..Default::default()
858 },
859 },
860 DiagnosticEntry {
861 range: PointUtf16::new(3, 6)..PointUtf16::new(3, 7),
862 diagnostic: Diagnostic {
863 message: "value moved here".to_string(),
864 severity: DiagnosticSeverity::INFORMATION,
865 is_primary: false,
866 is_disk_based: true,
867 group_id: 1,
868 ..Default::default()
869 },
870 },
871 DiagnosticEntry {
872 range: PointUtf16::new(4, 6)..PointUtf16::new(4, 7),
873 diagnostic: Diagnostic {
874 message: "value moved here".to_string(),
875 severity: DiagnosticSeverity::INFORMATION,
876 is_primary: false,
877 is_disk_based: true,
878 group_id: 0,
879 ..Default::default()
880 },
881 },
882 DiagnosticEntry {
883 range: PointUtf16::new(7, 6)..PointUtf16::new(7, 7),
884 diagnostic: Diagnostic {
885 message: "use of moved value\nvalue used here after move".to_string(),
886 severity: DiagnosticSeverity::ERROR,
887 is_primary: true,
888 is_disk_based: true,
889 group_id: 0,
890 ..Default::default()
891 },
892 },
893 DiagnosticEntry {
894 range: PointUtf16::new(8, 6)..PointUtf16::new(8, 7),
895 diagnostic: Diagnostic {
896 message: "use of moved value\nvalue used here after move".to_string(),
897 severity: DiagnosticSeverity::ERROR,
898 is_primary: true,
899 is_disk_based: true,
900 group_id: 1,
901 ..Default::default()
902 },
903 },
904 ],
905 cx,
906 )
907 .unwrap();
908 });
909
910 // Open the project diagnostics view while there are already diagnostics.
911 let model = cx.add_model(|_| ProjectDiagnostics::new(project.clone()));
912 let view = cx.add_view(0, |cx| {
913 ProjectDiagnosticsEditor::new(model, workspace.downgrade(), params.settings, cx)
914 });
915
916 view.next_notification(&cx).await;
917 view.update(&mut cx, |view, cx| {
918 let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
919
920 assert_eq!(
921 editor_blocks(&editor, cx),
922 [
923 (0, "path header block".into()),
924 (2, "diagnostic header".into()),
925 (15, "diagnostic header".into()),
926 (24, "collapsed context".into()),
927 ]
928 );
929 assert_eq!(
930 editor.text(),
931 concat!(
932 //
933 // main.rs
934 //
935 "\n", // filename
936 "\n", // padding
937 // diagnostic group 1
938 "\n", // primary message
939 "\n", // padding
940 " let x = vec![];\n",
941 " let y = vec![];\n",
942 "\n", // supporting diagnostic
943 " a(x);\n",
944 " b(y);\n",
945 "\n", // supporting diagnostic
946 " // comment 1\n",
947 " // comment 2\n",
948 " c(y);\n",
949 "\n", // supporting diagnostic
950 " d(x);\n",
951 // diagnostic group 2
952 "\n", // primary message
953 "\n", // padding
954 "fn main() {\n",
955 " let x = vec![];\n",
956 "\n", // supporting diagnostic
957 " let y = vec![];\n",
958 " a(x);\n",
959 "\n", // supporting diagnostic
960 " b(y);\n",
961 "\n", // context ellipsis
962 " c(y);\n",
963 " d(x);\n",
964 "\n", // supporting diagnostic
965 "}"
966 )
967 );
968
969 // Cursor is at the first diagnostic
970 view.editor.update(cx, |editor, cx| {
971 assert_eq!(
972 editor.selected_display_ranges(cx),
973 [DisplayPoint::new(12, 6)..DisplayPoint::new(12, 6)]
974 );
975 });
976 });
977
978 // Diagnostics are added for another earlier path.
979 project.update(&mut cx, |project, cx| {
980 project.disk_based_diagnostics_started(cx);
981 project
982 .update_diagnostic_entries(
983 PathBuf::from("/test/consts.rs"),
984 None,
985 vec![DiagnosticEntry {
986 range: PointUtf16::new(0, 15)..PointUtf16::new(0, 15),
987 diagnostic: Diagnostic {
988 message: "mismatched types\nexpected `usize`, found `char`".to_string(),
989 severity: DiagnosticSeverity::ERROR,
990 is_primary: true,
991 is_disk_based: true,
992 group_id: 0,
993 ..Default::default()
994 },
995 }],
996 cx,
997 )
998 .unwrap();
999 project.disk_based_diagnostics_finished(cx);
1000 });
1001
1002 view.next_notification(&cx).await;
1003 view.update(&mut cx, |view, cx| {
1004 let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
1005
1006 assert_eq!(
1007 editor_blocks(&editor, cx),
1008 [
1009 (0, "path header block".into()),
1010 (2, "diagnostic header".into()),
1011 (7, "path header block".into()),
1012 (9, "diagnostic header".into()),
1013 (22, "diagnostic header".into()),
1014 (31, "collapsed context".into()),
1015 ]
1016 );
1017 assert_eq!(
1018 editor.text(),
1019 concat!(
1020 //
1021 // consts.rs
1022 //
1023 "\n", // filename
1024 "\n", // padding
1025 // diagnostic group 1
1026 "\n", // primary message
1027 "\n", // padding
1028 "const a: i32 = 'a';\n",
1029 "\n", // supporting diagnostic
1030 "const b: i32 = c;\n",
1031 //
1032 // main.rs
1033 //
1034 "\n", // filename
1035 "\n", // padding
1036 // diagnostic group 1
1037 "\n", // primary message
1038 "\n", // padding
1039 " let x = vec![];\n",
1040 " let y = vec![];\n",
1041 "\n", // supporting diagnostic
1042 " a(x);\n",
1043 " b(y);\n",
1044 "\n", // supporting diagnostic
1045 " // comment 1\n",
1046 " // comment 2\n",
1047 " c(y);\n",
1048 "\n", // supporting diagnostic
1049 " d(x);\n",
1050 // diagnostic group 2
1051 "\n", // primary message
1052 "\n", // filename
1053 "fn main() {\n",
1054 " let x = vec![];\n",
1055 "\n", // supporting diagnostic
1056 " let y = vec![];\n",
1057 " a(x);\n",
1058 "\n", // supporting diagnostic
1059 " b(y);\n",
1060 "\n", // context ellipsis
1061 " c(y);\n",
1062 " d(x);\n",
1063 "\n", // supporting diagnostic
1064 "}"
1065 )
1066 );
1067
1068 // Cursor keeps its position.
1069 view.editor.update(cx, |editor, cx| {
1070 assert_eq!(
1071 editor.selected_display_ranges(cx),
1072 [DisplayPoint::new(19, 6)..DisplayPoint::new(19, 6)]
1073 );
1074 });
1075 });
1076
1077 // Diagnostics are added to the first path
1078 project.update(&mut cx, |project, cx| {
1079 project.disk_based_diagnostics_started(cx);
1080 project
1081 .update_diagnostic_entries(
1082 PathBuf::from("/test/consts.rs"),
1083 None,
1084 vec![
1085 DiagnosticEntry {
1086 range: PointUtf16::new(0, 15)..PointUtf16::new(0, 15),
1087 diagnostic: Diagnostic {
1088 message: "mismatched types\nexpected `usize`, found `char`"
1089 .to_string(),
1090 severity: DiagnosticSeverity::ERROR,
1091 is_primary: true,
1092 is_disk_based: true,
1093 group_id: 0,
1094 ..Default::default()
1095 },
1096 },
1097 DiagnosticEntry {
1098 range: PointUtf16::new(1, 15)..PointUtf16::new(1, 15),
1099 diagnostic: Diagnostic {
1100 message: "unresolved name `c`".to_string(),
1101 severity: DiagnosticSeverity::ERROR,
1102 is_primary: true,
1103 is_disk_based: true,
1104 group_id: 1,
1105 ..Default::default()
1106 },
1107 },
1108 ],
1109 cx,
1110 )
1111 .unwrap();
1112 project.disk_based_diagnostics_finished(cx);
1113 });
1114
1115 view.next_notification(&cx).await;
1116 view.update(&mut cx, |view, cx| {
1117 let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
1118
1119 assert_eq!(
1120 editor_blocks(&editor, cx),
1121 [
1122 (0, "path header block".into()),
1123 (2, "diagnostic header".into()),
1124 (7, "diagnostic header".into()),
1125 (12, "path header block".into()),
1126 (14, "diagnostic header".into()),
1127 (27, "diagnostic header".into()),
1128 (36, "collapsed context".into()),
1129 ]
1130 );
1131 assert_eq!(
1132 editor.text(),
1133 concat!(
1134 //
1135 // consts.rs
1136 //
1137 "\n", // filename
1138 "\n", // padding
1139 // diagnostic group 1
1140 "\n", // primary message
1141 "\n", // padding
1142 "const a: i32 = 'a';\n",
1143 "\n", // supporting diagnostic
1144 "const b: i32 = c;\n",
1145 // diagnostic group 2
1146 "\n", // primary message
1147 "\n", // padding
1148 "const a: i32 = 'a';\n",
1149 "const b: i32 = c;\n",
1150 "\n", // supporting diagnostic
1151 //
1152 // main.rs
1153 //
1154 "\n", // filename
1155 "\n", // padding
1156 // diagnostic group 1
1157 "\n", // primary message
1158 "\n", // padding
1159 " let x = vec![];\n",
1160 " let y = vec![];\n",
1161 "\n", // supporting diagnostic
1162 " a(x);\n",
1163 " b(y);\n",
1164 "\n", // supporting diagnostic
1165 " // comment 1\n",
1166 " // comment 2\n",
1167 " c(y);\n",
1168 "\n", // supporting diagnostic
1169 " d(x);\n",
1170 // diagnostic group 2
1171 "\n", // primary message
1172 "\n", // filename
1173 "fn main() {\n",
1174 " let x = vec![];\n",
1175 "\n", // supporting diagnostic
1176 " let y = vec![];\n",
1177 " a(x);\n",
1178 "\n", // supporting diagnostic
1179 " b(y);\n",
1180 "\n", // context ellipsis
1181 " c(y);\n",
1182 " d(x);\n",
1183 "\n", // supporting diagnostic
1184 "}"
1185 )
1186 );
1187 });
1188 }
1189
1190 fn editor_blocks(editor: &EditorSnapshot, cx: &AppContext) -> Vec<(u32, String)> {
1191 editor
1192 .blocks_in_range(0..editor.max_point().row())
1193 .filter_map(|(row, block)| {
1194 block
1195 .render(&BlockContext {
1196 cx,
1197 anchor_x: 0.,
1198 gutter_padding: 0.,
1199 gutter_width: 0.,
1200 em_width: 0.,
1201 })
1202 .name()
1203 .map(|s| (row, s.to_string()))
1204 })
1205 .collect()
1206 }
1207}