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
591fn path_header_renderer(buffer: ModelHandle<Buffer>, build_settings: BuildSettings) -> RenderBlock {
592 Arc::new(move |cx| {
593 let settings = build_settings(cx);
594 let file_path = if let Some(file) = buffer.read(&**cx).file() {
595 file.path().to_string_lossy().to_string()
596 } else {
597 "untitled".to_string()
598 };
599 let mut text_style = settings.style.text.clone();
600 let style = settings.style.diagnostic_path_header;
601 text_style.color = style.text;
602 Label::new(file_path, text_style)
603 .aligned()
604 .left()
605 .contained()
606 .with_style(style.header)
607 .with_padding_left(cx.line_number_x)
608 .expanded()
609 .named("path header block")
610 })
611}
612
613fn diagnostic_header_renderer(
614 diagnostic: Diagnostic,
615 is_valid: bool,
616 build_settings: BuildSettings,
617) -> RenderBlock {
618 Arc::new(move |cx| {
619 let settings = build_settings(cx);
620 let mut text_style = settings.style.text.clone();
621 let diagnostic_style = diagnostic_style(diagnostic.severity, is_valid, &settings.style);
622 text_style.color = diagnostic_style.text;
623 Text::new(diagnostic.message.clone(), text_style)
624 .with_soft_wrap(false)
625 .aligned()
626 .left()
627 .contained()
628 .with_style(diagnostic_style.header)
629 .with_padding_left(cx.line_number_x)
630 .expanded()
631 .named("diagnostic header")
632 })
633}
634
635fn context_header_renderer(build_settings: BuildSettings) -> RenderBlock {
636 Arc::new(move |cx| {
637 let settings = build_settings(cx);
638 let text_style = settings.style.text.clone();
639 Label::new("…".to_string(), text_style)
640 .contained()
641 .with_padding_left(cx.line_number_x)
642 .named("collapsed context")
643 })
644}
645
646fn compare_diagnostics<L: language::ToOffset, R: language::ToOffset>(
647 lhs: &DiagnosticEntry<L>,
648 rhs: &DiagnosticEntry<R>,
649 snapshot: &language::BufferSnapshot,
650) -> Ordering {
651 lhs.range
652 .start
653 .to_offset(&snapshot)
654 .cmp(&rhs.range.start.to_offset(snapshot))
655 .then_with(|| {
656 lhs.range
657 .end
658 .to_offset(&snapshot)
659 .cmp(&rhs.range.end.to_offset(snapshot))
660 })
661 .then_with(|| lhs.diagnostic.message.cmp(&rhs.diagnostic.message))
662}
663
664#[cfg(test)]
665mod tests {
666 use super::*;
667 use editor::{display_map::BlockContext, DisplayPoint, EditorSnapshot};
668 use gpui::TestAppContext;
669 use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16};
670 use project::worktree;
671 use serde_json::json;
672 use std::sync::Arc;
673 use unindent::Unindent as _;
674 use workspace::WorkspaceParams;
675
676 #[gpui::test]
677 async fn test_diagnostics(mut cx: TestAppContext) {
678 let params = cx.update(WorkspaceParams::test);
679 let project = params.project.clone();
680 let workspace = cx.add_view(0, |cx| Workspace::new(¶ms, cx));
681
682 params
683 .fs
684 .as_fake()
685 .insert_tree(
686 "/test",
687 json!({
688 "consts.rs": "
689 const a: i32 = 'a';
690 const b: i32 = c;
691 "
692 .unindent(),
693
694 "main.rs": "
695 fn main() {
696 let x = vec![];
697 let y = vec![];
698 a(x);
699 b(y);
700 // comment 1
701 // comment 2
702 c(y);
703 d(x);
704 }
705 "
706 .unindent(),
707 }),
708 )
709 .await;
710
711 let worktree = project
712 .update(&mut cx, |project, cx| {
713 project.add_local_worktree("/test", cx)
714 })
715 .await
716 .unwrap();
717
718 // Create some diagnostics
719 worktree.update(&mut cx, |worktree, cx| {
720 worktree
721 .update_diagnostic_entries(
722 Arc::from("/test/main.rs".as_ref()),
723 None,
724 vec![
725 DiagnosticEntry {
726 range: PointUtf16::new(1, 8)..PointUtf16::new(1, 9),
727 diagnostic: Diagnostic {
728 message:
729 "move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait"
730 .to_string(),
731 severity: DiagnosticSeverity::INFORMATION,
732 is_primary: false,
733 is_disk_based: true,
734 group_id: 1,
735 ..Default::default()
736 },
737 },
738 DiagnosticEntry {
739 range: PointUtf16::new(2, 8)..PointUtf16::new(2, 9),
740 diagnostic: Diagnostic {
741 message:
742 "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait"
743 .to_string(),
744 severity: DiagnosticSeverity::INFORMATION,
745 is_primary: false,
746 is_disk_based: true,
747 group_id: 0,
748 ..Default::default()
749 },
750 },
751 DiagnosticEntry {
752 range: PointUtf16::new(3, 6)..PointUtf16::new(3, 7),
753 diagnostic: Diagnostic {
754 message: "value moved here".to_string(),
755 severity: DiagnosticSeverity::INFORMATION,
756 is_primary: false,
757 is_disk_based: true,
758 group_id: 1,
759 ..Default::default()
760 },
761 },
762 DiagnosticEntry {
763 range: PointUtf16::new(4, 6)..PointUtf16::new(4, 7),
764 diagnostic: Diagnostic {
765 message: "value moved here".to_string(),
766 severity: DiagnosticSeverity::INFORMATION,
767 is_primary: false,
768 is_disk_based: true,
769 group_id: 0,
770 ..Default::default()
771 },
772 },
773 DiagnosticEntry {
774 range: PointUtf16::new(7, 6)..PointUtf16::new(7, 7),
775 diagnostic: Diagnostic {
776 message: "use of moved value\nvalue used here after move".to_string(),
777 severity: DiagnosticSeverity::ERROR,
778 is_primary: true,
779 is_disk_based: true,
780 group_id: 0,
781 ..Default::default()
782 },
783 },
784 DiagnosticEntry {
785 range: PointUtf16::new(8, 6)..PointUtf16::new(8, 7),
786 diagnostic: Diagnostic {
787 message: "use of moved value\nvalue used here after move".to_string(),
788 severity: DiagnosticSeverity::ERROR,
789 is_primary: true,
790 is_disk_based: true,
791 group_id: 1,
792 ..Default::default()
793 },
794 },
795 ],
796 cx,
797 )
798 .unwrap();
799 });
800
801 // Open the project diagnostics view while there are already diagnostics.
802 let model = cx.add_model(|_| ProjectDiagnostics::new(project.clone()));
803 let view = cx.add_view(0, |cx| {
804 ProjectDiagnosticsEditor::new(model, workspace.downgrade(), params.settings, cx)
805 });
806
807 view.next_notification(&cx).await;
808 view.update(&mut cx, |view, cx| {
809 let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
810
811 assert_eq!(
812 editor_blocks(&editor, cx),
813 [
814 (0, "path header block".into()),
815 (2, "diagnostic header".into()),
816 (15, "diagnostic header".into()),
817 (24, "collapsed context".into()),
818 ]
819 );
820 assert_eq!(
821 editor.text(),
822 concat!(
823 //
824 // main.rs
825 //
826 "\n", // filename
827 "\n", // padding
828 // diagnostic group 1
829 "\n", // primary message
830 "\n", // padding
831 " let x = vec![];\n",
832 " let y = vec![];\n",
833 "\n", // supporting diagnostic
834 " a(x);\n",
835 " b(y);\n",
836 "\n", // supporting diagnostic
837 " // comment 1\n",
838 " // comment 2\n",
839 " c(y);\n",
840 "\n", // supporting diagnostic
841 " d(x);\n",
842 // diagnostic group 2
843 "\n", // primary message
844 "\n", // padding
845 "fn main() {\n",
846 " let x = vec![];\n",
847 "\n", // supporting diagnostic
848 " let y = vec![];\n",
849 " a(x);\n",
850 "\n", // supporting diagnostic
851 " b(y);\n",
852 "\n", // context ellipsis
853 " c(y);\n",
854 " d(x);\n",
855 "\n", // supporting diagnostic
856 "}"
857 )
858 );
859
860 // Cursor is at the first diagnostic
861 view.editor.update(cx, |editor, cx| {
862 assert_eq!(
863 editor.selected_display_ranges(cx),
864 [DisplayPoint::new(12, 6)..DisplayPoint::new(12, 6)]
865 );
866 });
867 });
868
869 // Diagnostics are added for another earlier path.
870 worktree.update(&mut cx, |worktree, cx| {
871 worktree
872 .update_diagnostic_entries(
873 Arc::from("/test/consts.rs".as_ref()),
874 None,
875 vec![DiagnosticEntry {
876 range: PointUtf16::new(0, 15)..PointUtf16::new(0, 15),
877 diagnostic: Diagnostic {
878 message: "mismatched types\nexpected `usize`, found `char`".to_string(),
879 severity: DiagnosticSeverity::ERROR,
880 is_primary: true,
881 is_disk_based: true,
882 group_id: 0,
883 ..Default::default()
884 },
885 }],
886 cx,
887 )
888 .unwrap();
889 cx.emit(worktree::Event::DiskBasedDiagnosticsUpdated);
890 });
891
892 view.next_notification(&cx).await;
893 view.update(&mut cx, |view, cx| {
894 let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
895
896 assert_eq!(
897 editor_blocks(&editor, cx),
898 [
899 (0, "path header block".into()),
900 (2, "diagnostic header".into()),
901 (7, "path header block".into()),
902 (9, "diagnostic header".into()),
903 (22, "diagnostic header".into()),
904 (31, "collapsed context".into()),
905 ]
906 );
907 assert_eq!(
908 editor.text(),
909 concat!(
910 //
911 // consts.rs
912 //
913 "\n", // filename
914 "\n", // padding
915 // diagnostic group 1
916 "\n", // primary message
917 "\n", // padding
918 "const a: i32 = 'a';\n",
919 "\n", // supporting diagnostic
920 "const b: i32 = c;\n",
921 //
922 // main.rs
923 //
924 "\n", // filename
925 "\n", // padding
926 // diagnostic group 1
927 "\n", // primary message
928 "\n", // padding
929 " let x = vec![];\n",
930 " let y = vec![];\n",
931 "\n", // supporting diagnostic
932 " a(x);\n",
933 " b(y);\n",
934 "\n", // supporting diagnostic
935 " // comment 1\n",
936 " // comment 2\n",
937 " c(y);\n",
938 "\n", // supporting diagnostic
939 " d(x);\n",
940 // diagnostic group 2
941 "\n", // primary message
942 "\n", // filename
943 "fn main() {\n",
944 " let x = vec![];\n",
945 "\n", // supporting diagnostic
946 " let y = vec![];\n",
947 " a(x);\n",
948 "\n", // supporting diagnostic
949 " b(y);\n",
950 "\n", // context ellipsis
951 " c(y);\n",
952 " d(x);\n",
953 "\n", // supporting diagnostic
954 "}"
955 )
956 );
957
958 // Cursor keeps its position.
959 view.editor.update(cx, |editor, cx| {
960 assert_eq!(
961 editor.selected_display_ranges(cx),
962 [DisplayPoint::new(19, 6)..DisplayPoint::new(19, 6)]
963 );
964 });
965 });
966
967 // Diagnostics are added to the first path
968 worktree.update(&mut cx, |worktree, cx| {
969 worktree
970 .update_diagnostic_entries(
971 Arc::from("/test/consts.rs".as_ref()),
972 None,
973 vec![
974 DiagnosticEntry {
975 range: PointUtf16::new(0, 15)..PointUtf16::new(0, 15),
976 diagnostic: Diagnostic {
977 message: "mismatched types\nexpected `usize`, found `char`"
978 .to_string(),
979 severity: DiagnosticSeverity::ERROR,
980 is_primary: true,
981 is_disk_based: true,
982 group_id: 0,
983 ..Default::default()
984 },
985 },
986 DiagnosticEntry {
987 range: PointUtf16::new(1, 15)..PointUtf16::new(1, 15),
988 diagnostic: Diagnostic {
989 message: "unresolved name `c`".to_string(),
990 severity: DiagnosticSeverity::ERROR,
991 is_primary: true,
992 is_disk_based: true,
993 group_id: 1,
994 ..Default::default()
995 },
996 },
997 ],
998 cx,
999 )
1000 .unwrap();
1001 cx.emit(worktree::Event::DiskBasedDiagnosticsUpdated);
1002 });
1003
1004 view.next_notification(&cx).await;
1005 view.update(&mut cx, |view, cx| {
1006 let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
1007
1008 assert_eq!(
1009 editor_blocks(&editor, cx),
1010 [
1011 (0, "path header block".into()),
1012 (2, "diagnostic header".into()),
1013 (7, "diagnostic header".into()),
1014 (12, "path header block".into()),
1015 (14, "diagnostic header".into()),
1016 (27, "diagnostic header".into()),
1017 (36, "collapsed context".into()),
1018 ]
1019 );
1020 assert_eq!(
1021 editor.text(),
1022 concat!(
1023 //
1024 // consts.rs
1025 //
1026 "\n", // filename
1027 "\n", // padding
1028 // diagnostic group 1
1029 "\n", // primary message
1030 "\n", // padding
1031 "const a: i32 = 'a';\n",
1032 "\n", // supporting diagnostic
1033 "const b: i32 = c;\n",
1034 // diagnostic group 2
1035 "\n", // primary message
1036 "\n", // padding
1037 "const a: i32 = 'a';\n",
1038 "const b: i32 = c;\n",
1039 "\n", // supporting diagnostic
1040 //
1041 // main.rs
1042 //
1043 "\n", // filename
1044 "\n", // padding
1045 // diagnostic group 1
1046 "\n", // primary message
1047 "\n", // padding
1048 " let x = vec![];\n",
1049 " let y = vec![];\n",
1050 "\n", // supporting diagnostic
1051 " a(x);\n",
1052 " b(y);\n",
1053 "\n", // supporting diagnostic
1054 " // comment 1\n",
1055 " // comment 2\n",
1056 " c(y);\n",
1057 "\n", // supporting diagnostic
1058 " d(x);\n",
1059 // diagnostic group 2
1060 "\n", // primary message
1061 "\n", // filename
1062 "fn main() {\n",
1063 " let x = vec![];\n",
1064 "\n", // supporting diagnostic
1065 " let y = vec![];\n",
1066 " a(x);\n",
1067 "\n", // supporting diagnostic
1068 " b(y);\n",
1069 "\n", // context ellipsis
1070 " c(y);\n",
1071 " d(x);\n",
1072 "\n", // supporting diagnostic
1073 "}"
1074 )
1075 );
1076 });
1077 }
1078
1079 fn editor_blocks(editor: &EditorSnapshot, cx: &AppContext) -> Vec<(u32, String)> {
1080 editor
1081 .blocks_in_range(0..editor.max_point().row())
1082 .filter_map(|(row, block)| {
1083 block
1084 .render(&BlockContext {
1085 cx,
1086 anchor_x: 0.,
1087 line_number_x: 0.,
1088 })
1089 .name()
1090 .map(|s| (row, s.to_string()))
1091 })
1092 .collect()
1093 }
1094}