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