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, path::PathBuf, rc::Rc, sync::Arc};
19use util::TryFutureExt;
20use workspace::{NavHistory, 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 _: Rc<NavHistory>,
526 cx: &mut ViewContext<Self::View>,
527 ) -> Self::View {
528 ProjectDiagnosticsEditor::new(handle, workspace.weak_handle(), workspace.settings(), cx)
529 }
530
531 fn project_entry(&self) -> Option<project::ProjectEntry> {
532 None
533 }
534}
535
536impl workspace::ItemView for ProjectDiagnosticsEditor {
537 type ItemHandle = ModelHandle<ProjectDiagnostics>;
538
539 fn item_handle(&self, _: &AppContext) -> Self::ItemHandle {
540 self.model.clone()
541 }
542
543 fn title(&self, _: &AppContext) -> String {
544 "Project Diagnostics".to_string()
545 }
546
547 fn project_entry(&self, _: &AppContext) -> Option<project::ProjectEntry> {
548 None
549 }
550
551 fn is_dirty(&self, cx: &AppContext) -> bool {
552 self.excerpts.read(cx).read(cx).is_dirty()
553 }
554
555 fn has_conflict(&self, cx: &AppContext) -> bool {
556 self.excerpts.read(cx).read(cx).has_conflict()
557 }
558
559 fn can_save(&self, _: &AppContext) -> bool {
560 true
561 }
562
563 fn save(&mut self, cx: &mut ViewContext<Self>) -> Result<Task<Result<()>>> {
564 self.excerpts.update(cx, |excerpts, cx| excerpts.save(cx))
565 }
566
567 fn can_save_as(&self, _: &AppContext) -> bool {
568 false
569 }
570
571 fn save_as(
572 &mut self,
573 _: ModelHandle<Project>,
574 _: PathBuf,
575 _: &mut ViewContext<Self>,
576 ) -> Task<Result<()>> {
577 unreachable!()
578 }
579
580 fn should_activate_item_on_event(event: &Self::Event) -> bool {
581 Editor::should_activate_item_on_event(event)
582 }
583
584 fn should_update_tab_on_event(event: &Event) -> bool {
585 matches!(
586 event,
587 Event::Saved | Event::Dirtied | Event::FileHandleChanged
588 )
589 }
590
591 fn clone_on_split(&self, cx: &mut ViewContext<Self>) -> Option<Self>
592 where
593 Self: Sized,
594 {
595 Some(ProjectDiagnosticsEditor::new(
596 self.model.clone(),
597 self.workspace.clone(),
598 self.settings.clone(),
599 cx,
600 ))
601 }
602}
603
604fn path_header_renderer(buffer: ModelHandle<Buffer>, build_settings: BuildSettings) -> RenderBlock {
605 Arc::new(move |cx| {
606 let settings = build_settings(cx);
607 let file_path = if let Some(file) = buffer.read(&**cx).file() {
608 file.path().to_string_lossy().to_string()
609 } else {
610 "untitled".to_string()
611 };
612 let mut text_style = settings.style.text.clone();
613 let style = settings.style.diagnostic_path_header;
614 text_style.color = style.text;
615 Label::new(file_path, text_style)
616 .aligned()
617 .left()
618 .contained()
619 .with_style(style.header)
620 .with_padding_left(cx.line_number_x)
621 .expanded()
622 .named("path header block")
623 })
624}
625
626fn diagnostic_header_renderer(
627 diagnostic: Diagnostic,
628 is_valid: bool,
629 build_settings: BuildSettings,
630) -> RenderBlock {
631 Arc::new(move |cx| {
632 let settings = build_settings(cx);
633 let mut text_style = settings.style.text.clone();
634 let diagnostic_style = diagnostic_style(diagnostic.severity, is_valid, &settings.style);
635 text_style.color = diagnostic_style.text;
636 Text::new(diagnostic.message.clone(), text_style)
637 .with_soft_wrap(false)
638 .aligned()
639 .left()
640 .contained()
641 .with_style(diagnostic_style.header)
642 .with_padding_left(cx.line_number_x)
643 .expanded()
644 .named("diagnostic header")
645 })
646}
647
648fn context_header_renderer(build_settings: BuildSettings) -> RenderBlock {
649 Arc::new(move |cx| {
650 let settings = build_settings(cx);
651 let text_style = settings.style.text.clone();
652 Label::new("…".to_string(), text_style)
653 .contained()
654 .with_padding_left(cx.line_number_x)
655 .named("collapsed context")
656 })
657}
658
659fn compare_diagnostics<L: language::ToOffset, R: language::ToOffset>(
660 lhs: &DiagnosticEntry<L>,
661 rhs: &DiagnosticEntry<R>,
662 snapshot: &language::BufferSnapshot,
663) -> Ordering {
664 lhs.range
665 .start
666 .to_offset(&snapshot)
667 .cmp(&rhs.range.start.to_offset(snapshot))
668 .then_with(|| {
669 lhs.range
670 .end
671 .to_offset(&snapshot)
672 .cmp(&rhs.range.end.to_offset(snapshot))
673 })
674 .then_with(|| lhs.diagnostic.message.cmp(&rhs.diagnostic.message))
675}
676
677#[cfg(test)]
678mod tests {
679 use super::*;
680 use editor::{display_map::BlockContext, DisplayPoint, EditorSnapshot};
681 use gpui::TestAppContext;
682 use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16};
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 let worktree_id = worktree.read_with(&cx, |tree, _| tree.id());
730
731 // Create some diagnostics
732 worktree.update(&mut cx, |worktree, cx| {
733 worktree
734 .as_local_mut()
735 .unwrap()
736 .update_diagnostic_entries(
737 Arc::from("/test/main.rs".as_ref()),
738 None,
739 vec![
740 DiagnosticEntry {
741 range: PointUtf16::new(1, 8)..PointUtf16::new(1, 9),
742 diagnostic: Diagnostic {
743 message:
744 "move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait"
745 .to_string(),
746 severity: DiagnosticSeverity::INFORMATION,
747 is_primary: false,
748 is_disk_based: true,
749 group_id: 1,
750 ..Default::default()
751 },
752 },
753 DiagnosticEntry {
754 range: PointUtf16::new(2, 8)..PointUtf16::new(2, 9),
755 diagnostic: Diagnostic {
756 message:
757 "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait"
758 .to_string(),
759 severity: DiagnosticSeverity::INFORMATION,
760 is_primary: false,
761 is_disk_based: true,
762 group_id: 0,
763 ..Default::default()
764 },
765 },
766 DiagnosticEntry {
767 range: PointUtf16::new(3, 6)..PointUtf16::new(3, 7),
768 diagnostic: Diagnostic {
769 message: "value moved here".to_string(),
770 severity: DiagnosticSeverity::INFORMATION,
771 is_primary: false,
772 is_disk_based: true,
773 group_id: 1,
774 ..Default::default()
775 },
776 },
777 DiagnosticEntry {
778 range: PointUtf16::new(4, 6)..PointUtf16::new(4, 7),
779 diagnostic: Diagnostic {
780 message: "value moved here".to_string(),
781 severity: DiagnosticSeverity::INFORMATION,
782 is_primary: false,
783 is_disk_based: true,
784 group_id: 0,
785 ..Default::default()
786 },
787 },
788 DiagnosticEntry {
789 range: PointUtf16::new(7, 6)..PointUtf16::new(7, 7),
790 diagnostic: Diagnostic {
791 message: "use of moved value\nvalue used here after move".to_string(),
792 severity: DiagnosticSeverity::ERROR,
793 is_primary: true,
794 is_disk_based: true,
795 group_id: 0,
796 ..Default::default()
797 },
798 },
799 DiagnosticEntry {
800 range: PointUtf16::new(8, 6)..PointUtf16::new(8, 7),
801 diagnostic: Diagnostic {
802 message: "use of moved value\nvalue used here after move".to_string(),
803 severity: DiagnosticSeverity::ERROR,
804 is_primary: true,
805 is_disk_based: true,
806 group_id: 1,
807 ..Default::default()
808 },
809 },
810 ],
811 cx,
812 )
813 .unwrap();
814 });
815
816 // Open the project diagnostics view while there are already diagnostics.
817 let model = cx.add_model(|_| ProjectDiagnostics::new(project.clone()));
818 let view = cx.add_view(0, |cx| {
819 ProjectDiagnosticsEditor::new(model, workspace.downgrade(), params.settings, cx)
820 });
821
822 view.next_notification(&cx).await;
823 view.update(&mut cx, |view, cx| {
824 let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
825
826 assert_eq!(
827 editor_blocks(&editor, cx),
828 [
829 (0, "path header block".into()),
830 (2, "diagnostic header".into()),
831 (15, "diagnostic header".into()),
832 (24, "collapsed context".into()),
833 ]
834 );
835 assert_eq!(
836 editor.text(),
837 concat!(
838 //
839 // main.rs
840 //
841 "\n", // filename
842 "\n", // padding
843 // diagnostic group 1
844 "\n", // primary message
845 "\n", // padding
846 " let x = vec![];\n",
847 " let y = vec![];\n",
848 "\n", // supporting diagnostic
849 " a(x);\n",
850 " b(y);\n",
851 "\n", // supporting diagnostic
852 " // comment 1\n",
853 " // comment 2\n",
854 " c(y);\n",
855 "\n", // supporting diagnostic
856 " d(x);\n",
857 // diagnostic group 2
858 "\n", // primary message
859 "\n", // padding
860 "fn main() {\n",
861 " let x = vec![];\n",
862 "\n", // supporting diagnostic
863 " let y = vec![];\n",
864 " a(x);\n",
865 "\n", // supporting diagnostic
866 " b(y);\n",
867 "\n", // context ellipsis
868 " c(y);\n",
869 " d(x);\n",
870 "\n", // supporting diagnostic
871 "}"
872 )
873 );
874
875 // Cursor is at the first diagnostic
876 view.editor.update(cx, |editor, cx| {
877 assert_eq!(
878 editor.selected_display_ranges(cx),
879 [DisplayPoint::new(12, 6)..DisplayPoint::new(12, 6)]
880 );
881 });
882 });
883
884 // Diagnostics are added for another earlier path.
885 worktree.update(&mut cx, |worktree, cx| {
886 worktree
887 .as_local_mut()
888 .unwrap()
889 .update_diagnostic_entries(
890 Arc::from("/test/consts.rs".as_ref()),
891 None,
892 vec![DiagnosticEntry {
893 range: PointUtf16::new(0, 15)..PointUtf16::new(0, 15),
894 diagnostic: Diagnostic {
895 message: "mismatched types\nexpected `usize`, found `char`".to_string(),
896 severity: DiagnosticSeverity::ERROR,
897 is_primary: true,
898 is_disk_based: true,
899 group_id: 0,
900 ..Default::default()
901 },
902 }],
903 cx,
904 )
905 .unwrap();
906 });
907 project.update(&mut cx, |_, cx| {
908 cx.emit(project::Event::DiagnosticsUpdated(ProjectPath {
909 worktree_id,
910 path: Arc::from("/test/consts.rs".as_ref()),
911 }));
912 cx.emit(project::Event::DiskBasedDiagnosticsUpdated { worktree_id });
913 });
914
915 view.next_notification(&cx).await;
916 view.update(&mut cx, |view, cx| {
917 let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
918
919 assert_eq!(
920 editor_blocks(&editor, cx),
921 [
922 (0, "path header block".into()),
923 (2, "diagnostic header".into()),
924 (7, "path header block".into()),
925 (9, "diagnostic header".into()),
926 (22, "diagnostic header".into()),
927 (31, "collapsed context".into()),
928 ]
929 );
930 assert_eq!(
931 editor.text(),
932 concat!(
933 //
934 // consts.rs
935 //
936 "\n", // filename
937 "\n", // padding
938 // diagnostic group 1
939 "\n", // primary message
940 "\n", // padding
941 "const a: i32 = 'a';\n",
942 "\n", // supporting diagnostic
943 "const b: i32 = c;\n",
944 //
945 // main.rs
946 //
947 "\n", // filename
948 "\n", // padding
949 // diagnostic group 1
950 "\n", // primary message
951 "\n", // padding
952 " let x = vec![];\n",
953 " let y = vec![];\n",
954 "\n", // supporting diagnostic
955 " a(x);\n",
956 " b(y);\n",
957 "\n", // supporting diagnostic
958 " // comment 1\n",
959 " // comment 2\n",
960 " c(y);\n",
961 "\n", // supporting diagnostic
962 " d(x);\n",
963 // diagnostic group 2
964 "\n", // primary message
965 "\n", // filename
966 "fn main() {\n",
967 " let x = vec![];\n",
968 "\n", // supporting diagnostic
969 " let y = vec![];\n",
970 " a(x);\n",
971 "\n", // supporting diagnostic
972 " b(y);\n",
973 "\n", // context ellipsis
974 " c(y);\n",
975 " d(x);\n",
976 "\n", // supporting diagnostic
977 "}"
978 )
979 );
980
981 // Cursor keeps its position.
982 view.editor.update(cx, |editor, cx| {
983 assert_eq!(
984 editor.selected_display_ranges(cx),
985 [DisplayPoint::new(19, 6)..DisplayPoint::new(19, 6)]
986 );
987 });
988 });
989
990 // Diagnostics are added to the first path
991 worktree.update(&mut cx, |worktree, cx| {
992 worktree
993 .as_local_mut()
994 .unwrap()
995 .update_diagnostic_entries(
996 Arc::from("/test/consts.rs".as_ref()),
997 None,
998 vec![
999 DiagnosticEntry {
1000 range: PointUtf16::new(0, 15)..PointUtf16::new(0, 15),
1001 diagnostic: Diagnostic {
1002 message: "mismatched types\nexpected `usize`, found `char`"
1003 .to_string(),
1004 severity: DiagnosticSeverity::ERROR,
1005 is_primary: true,
1006 is_disk_based: true,
1007 group_id: 0,
1008 ..Default::default()
1009 },
1010 },
1011 DiagnosticEntry {
1012 range: PointUtf16::new(1, 15)..PointUtf16::new(1, 15),
1013 diagnostic: Diagnostic {
1014 message: "unresolved name `c`".to_string(),
1015 severity: DiagnosticSeverity::ERROR,
1016 is_primary: true,
1017 is_disk_based: true,
1018 group_id: 1,
1019 ..Default::default()
1020 },
1021 },
1022 ],
1023 cx,
1024 )
1025 .unwrap();
1026 });
1027 project.update(&mut cx, |_, cx| {
1028 cx.emit(project::Event::DiagnosticsUpdated(ProjectPath {
1029 worktree_id,
1030 path: Arc::from("/test/consts.rs".as_ref()),
1031 }));
1032 cx.emit(project::Event::DiskBasedDiagnosticsUpdated { worktree_id });
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, "diagnostic header".into()),
1045 (12, "path header block".into()),
1046 (14, "diagnostic header".into()),
1047 (27, "diagnostic header".into()),
1048 (36, "collapsed context".into()),
1049 ]
1050 );
1051 assert_eq!(
1052 editor.text(),
1053 concat!(
1054 //
1055 // consts.rs
1056 //
1057 "\n", // filename
1058 "\n", // padding
1059 // diagnostic group 1
1060 "\n", // primary message
1061 "\n", // padding
1062 "const a: i32 = 'a';\n",
1063 "\n", // supporting diagnostic
1064 "const b: i32 = c;\n",
1065 // diagnostic group 2
1066 "\n", // primary message
1067 "\n", // padding
1068 "const a: i32 = 'a';\n",
1069 "const b: i32 = c;\n",
1070 "\n", // supporting diagnostic
1071 //
1072 // main.rs
1073 //
1074 "\n", // filename
1075 "\n", // padding
1076 // diagnostic group 1
1077 "\n", // primary message
1078 "\n", // padding
1079 " let x = vec![];\n",
1080 " let y = vec![];\n",
1081 "\n", // supporting diagnostic
1082 " a(x);\n",
1083 " b(y);\n",
1084 "\n", // supporting diagnostic
1085 " // comment 1\n",
1086 " // comment 2\n",
1087 " c(y);\n",
1088 "\n", // supporting diagnostic
1089 " d(x);\n",
1090 // diagnostic group 2
1091 "\n", // primary message
1092 "\n", // filename
1093 "fn main() {\n",
1094 " let x = vec![];\n",
1095 "\n", // supporting diagnostic
1096 " let y = vec![];\n",
1097 " a(x);\n",
1098 "\n", // supporting diagnostic
1099 " b(y);\n",
1100 "\n", // context ellipsis
1101 " c(y);\n",
1102 " d(x);\n",
1103 "\n", // supporting diagnostic
1104 "}"
1105 )
1106 );
1107 });
1108 }
1109
1110 fn editor_blocks(editor: &EditorSnapshot, cx: &AppContext) -> Vec<(u32, String)> {
1111 editor
1112 .blocks_in_range(0..editor.max_point().row())
1113 .filter_map(|(row, block)| {
1114 block
1115 .render(&BlockContext {
1116 cx,
1117 anchor_x: 0.,
1118 line_number_x: 0.,
1119 })
1120 .name()
1121 .map(|s| (row, s.to_string()))
1122 })
1123 .collect()
1124 }
1125}