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