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