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