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