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::{DiagnosticSummary, 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 summary: DiagnosticSummary,
51 excerpts: ModelHandle<MultiBuffer>,
52 path_states: Vec<PathState>,
53 paths_to_update: HashMap<WorktreeId, BTreeSet<ProjectPath>>,
54 build_settings: BuildSettings,
55 settings: watch::Receiver<workspace::Settings>,
56}
57
58struct PathState {
59 path: ProjectPath,
60 header: Option<BlockId>,
61 diagnostic_groups: Vec<DiagnosticGroupState>,
62}
63
64struct DiagnosticGroupState {
65 primary_diagnostic: DiagnosticEntry<language::Anchor>,
66 primary_excerpt_ix: usize,
67 excerpts: Vec<ExcerptId>,
68 blocks: HashSet<BlockId>,
69 block_count: usize,
70}
71
72impl ProjectDiagnostics {
73 fn new(project: ModelHandle<Project>) -> Self {
74 Self { project }
75 }
76}
77
78impl Entity for ProjectDiagnostics {
79 type Event = ();
80}
81
82impl Entity for ProjectDiagnosticsEditor {
83 type Event = Event;
84}
85
86impl View for ProjectDiagnosticsEditor {
87 fn ui_name() -> &'static str {
88 "ProjectDiagnosticsEditor"
89 }
90
91 fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
92 if self.path_states.is_empty() {
93 let theme = &self.settings.borrow().theme.project_diagnostics;
94 Label::new(
95 "No problems detected in the project".to_string(),
96 theme.empty_message.clone(),
97 )
98 .aligned()
99 .contained()
100 .with_style(theme.container)
101 .boxed()
102 } else {
103 ChildView::new(self.editor.id()).boxed()
104 }
105 }
106
107 fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
108 if !self.path_states.is_empty() {
109 cx.focus(&self.editor);
110 }
111 }
112}
113
114impl ProjectDiagnosticsEditor {
115 fn new(
116 model: ModelHandle<ProjectDiagnostics>,
117 workspace: WeakViewHandle<Workspace>,
118 settings: watch::Receiver<workspace::Settings>,
119 cx: &mut ViewContext<Self>,
120 ) -> Self {
121 let project = model.read(cx).project.clone();
122 cx.subscribe(&project, |this, _, event, cx| match event {
123 project::Event::DiskBasedDiagnosticsUpdated { worktree_id } => {
124 this.summary = this.model.read(cx).project.read(cx).diagnostic_summary(cx);
125 if let Some(paths) = this.paths_to_update.remove(&worktree_id) {
126 this.update_excerpts(paths, cx);
127 }
128 cx.emit(Event::TitleChanged)
129 }
130 project::Event::DiagnosticsUpdated(path) => {
131 this.paths_to_update
132 .entry(path.worktree_id)
133 .or_default()
134 .insert(path.clone());
135 }
136 _ => {}
137 })
138 .detach();
139
140 let excerpts = cx.add_model(|cx| MultiBuffer::new(project.read(cx).replica_id()));
141 let build_settings = editor::settings_builder(excerpts.downgrade(), settings.clone());
142 let editor =
143 cx.add_view(|cx| Editor::for_buffer(excerpts.clone(), build_settings.clone(), cx));
144 cx.subscribe(&editor, |_, _, event, cx| cx.emit(*event))
145 .detach();
146
147 let project = project.read(cx);
148 let paths_to_update = project.diagnostic_summaries(cx).map(|e| e.0).collect();
149 let this = Self {
150 model,
151 summary: project.diagnostic_summary(cx),
152 workspace,
153 excerpts,
154 editor,
155 build_settings,
156 settings,
157 path_states: Default::default(),
158 paths_to_update: Default::default(),
159 };
160 this.update_excerpts(paths_to_update, cx);
161 this
162 }
163
164 #[cfg(test)]
165 fn text(&self, cx: &AppContext) -> String {
166 self.editor.read(cx).text(cx)
167 }
168
169 fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext<Workspace>) {
170 if let Some(existing) = workspace.item_of_type::<ProjectDiagnostics>(cx) {
171 workspace.activate_item(&existing, cx);
172 } else {
173 let diagnostics =
174 cx.add_model(|_| ProjectDiagnostics::new(workspace.project().clone()));
175 workspace.open_item(diagnostics, cx);
176 }
177 }
178
179 fn open_excerpts(&mut self, _: &OpenExcerpts, cx: &mut ViewContext<Self>) {
180 if let Some(workspace) = self.workspace.upgrade(cx) {
181 let editor = self.editor.read(cx);
182 let excerpts = self.excerpts.read(cx);
183 let mut new_selections_by_buffer = HashMap::default();
184
185 for selection in editor.local_selections::<usize>(cx) {
186 for (buffer, mut range) in
187 excerpts.excerpted_buffers(selection.start..selection.end, cx)
188 {
189 if selection.reversed {
190 mem::swap(&mut range.start, &mut range.end);
191 }
192 new_selections_by_buffer
193 .entry(buffer)
194 .or_insert(Vec::new())
195 .push(range)
196 }
197 }
198
199 workspace.update(cx, |workspace, cx| {
200 for (buffer, ranges) in new_selections_by_buffer {
201 let buffer = BufferItemHandle(buffer);
202 if !workspace.activate_pane_for_item(&buffer, cx) {
203 workspace.activate_next_pane(cx);
204 }
205 let editor = workspace
206 .open_item(buffer, cx)
207 .to_any()
208 .downcast::<Editor>()
209 .unwrap();
210 editor.update(cx, |editor, cx| {
211 editor.select_ranges(ranges, Some(Autoscroll::Center), cx)
212 });
213 }
214 });
215 }
216 }
217
218 fn update_excerpts(&self, paths: BTreeSet<ProjectPath>, cx: &mut ViewContext<Self>) {
219 let project = self.model.read(cx).project.clone();
220 cx.spawn(|this, mut cx| {
221 async move {
222 for path in paths {
223 let buffer = project
224 .update(&mut cx, |project, cx| project.open_buffer(path.clone(), cx))
225 .await?;
226 this.update(&mut cx, |view, cx| view.populate_excerpts(path, buffer, cx))
227 }
228 Result::<_, anyhow::Error>::Ok(())
229 }
230 .log_err()
231 })
232 .detach();
233 }
234
235 fn populate_excerpts(
236 &mut self,
237 path: ProjectPath,
238 buffer: ModelHandle<Buffer>,
239 cx: &mut ViewContext<Self>,
240 ) {
241 let was_empty = self.path_states.is_empty();
242 let snapshot = buffer.read(cx).snapshot();
243 let path_ix = match self.path_states.binary_search_by_key(&&path, |e| &e.path) {
244 Ok(ix) => ix,
245 Err(ix) => {
246 self.path_states.insert(
247 ix,
248 PathState {
249 path: path.clone(),
250 header: None,
251 diagnostic_groups: Default::default(),
252 },
253 );
254 ix
255 }
256 };
257
258 let mut prev_excerpt_id = if path_ix > 0 {
259 let prev_path_last_group = &self.path_states[path_ix - 1]
260 .diagnostic_groups
261 .last()
262 .unwrap();
263 prev_path_last_group.excerpts.last().unwrap().clone()
264 } else {
265 ExcerptId::min()
266 };
267
268 let path_state = &mut self.path_states[path_ix];
269 let mut groups_to_add = Vec::new();
270 let mut group_ixs_to_remove = Vec::new();
271 let mut blocks_to_add = Vec::new();
272 let mut blocks_to_remove = HashSet::default();
273 let mut first_excerpt_id = None;
274 let excerpts_snapshot = self.excerpts.update(cx, |excerpts, excerpts_cx| {
275 let mut old_groups = path_state.diagnostic_groups.iter().enumerate().peekable();
276 let mut new_groups = snapshot
277 .diagnostic_groups()
278 .into_iter()
279 .filter(|group| group.entries[group.primary_ix].diagnostic.is_disk_based)
280 .peekable();
281
282 loop {
283 let mut to_insert = None;
284 let mut to_remove = None;
285 let mut to_keep = None;
286 match (old_groups.peek(), new_groups.peek()) {
287 (None, None) => break,
288 (None, Some(_)) => to_insert = new_groups.next(),
289 (Some(_), None) => to_remove = old_groups.next(),
290 (Some((_, old_group)), Some(new_group)) => {
291 let old_primary = &old_group.primary_diagnostic;
292 let new_primary = &new_group.entries[new_group.primary_ix];
293 match compare_diagnostics(old_primary, new_primary, &snapshot) {
294 Ordering::Less => to_remove = old_groups.next(),
295 Ordering::Equal => {
296 to_keep = old_groups.next();
297 new_groups.next();
298 }
299 Ordering::Greater => to_insert = new_groups.next(),
300 }
301 }
302 }
303
304 if let Some(group) = to_insert {
305 let mut group_state = DiagnosticGroupState {
306 primary_diagnostic: group.entries[group.primary_ix].clone(),
307 primary_excerpt_ix: 0,
308 excerpts: Default::default(),
309 blocks: Default::default(),
310 block_count: 0,
311 };
312 let mut pending_range: Option<(Range<Point>, usize)> = None;
313 let mut is_first_excerpt_for_group = true;
314 for (ix, entry) in group.entries.iter().map(Some).chain([None]).enumerate() {
315 let resolved_entry = entry.map(|e| e.resolve::<Point>(&snapshot));
316 if let Some((range, start_ix)) = &mut pending_range {
317 if let Some(entry) = resolved_entry.as_ref() {
318 if entry.range.start.row
319 <= range.end.row + 1 + CONTEXT_LINE_COUNT * 2
320 {
321 range.end = range.end.max(entry.range.end);
322 continue;
323 }
324 }
325
326 let excerpt_start =
327 Point::new(range.start.row.saturating_sub(CONTEXT_LINE_COUNT), 0);
328 let excerpt_end = snapshot.clip_point(
329 Point::new(range.end.row + CONTEXT_LINE_COUNT, u32::MAX),
330 Bias::Left,
331 );
332 let excerpt_id = excerpts.insert_excerpt_after(
333 &prev_excerpt_id,
334 ExcerptProperties {
335 buffer: &buffer,
336 range: excerpt_start..excerpt_end,
337 },
338 excerpts_cx,
339 );
340
341 prev_excerpt_id = excerpt_id.clone();
342 first_excerpt_id.get_or_insert_with(|| prev_excerpt_id.clone());
343 group_state.excerpts.push(excerpt_id.clone());
344 let header_position = (excerpt_id.clone(), language::Anchor::min());
345
346 if is_first_excerpt_for_group {
347 is_first_excerpt_for_group = false;
348 let primary = &group.entries[group.primary_ix].diagnostic;
349 let mut header = primary.clone();
350 header.message =
351 primary.message.split('\n').next().unwrap().to_string();
352 group_state.block_count += 1;
353 blocks_to_add.push(BlockProperties {
354 position: header_position,
355 height: 2,
356 render: diagnostic_header_renderer(
357 header,
358 true,
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 cx: &mut ViewContext<Self::View>,
532 ) -> Self::View {
533 ProjectDiagnosticsEditor::new(handle, workspace.weak_handle(), workspace.settings(), cx)
534 }
535
536 fn project_path(&self) -> Option<project::ProjectPath> {
537 None
538 }
539}
540
541impl workspace::ItemView for ProjectDiagnosticsEditor {
542 type ItemHandle = ModelHandle<ProjectDiagnostics>;
543
544 fn item_handle(&self, _: &AppContext) -> Self::ItemHandle {
545 self.model.clone()
546 }
547
548 fn tab_content(&self, style: &theme::Tab, _: &AppContext) -> ElementBox {
549 let theme = &self.settings.borrow().theme.project_diagnostics;
550 let icon_width = theme.tab_icon_width;
551 let icon_spacing = theme.tab_icon_spacing;
552 let summary_spacing = theme.tab_summary_spacing;
553 Flex::row()
554 .with_children([
555 Svg::new("icons/no.svg")
556 .with_color(style.label.text.color)
557 .constrained()
558 .with_width(icon_width)
559 .aligned()
560 .contained()
561 .with_margin_right(icon_spacing)
562 .named("no-icon"),
563 Label::new(self.summary.error_count.to_string(), style.label.clone())
564 .aligned()
565 .boxed(),
566 Svg::new("icons/warning.svg")
567 .with_color(style.label.text.color)
568 .constrained()
569 .with_width(icon_width)
570 .aligned()
571 .contained()
572 .with_margin_left(summary_spacing)
573 .with_margin_right(icon_spacing)
574 .named("warn-icon"),
575 Label::new(self.summary.warning_count.to_string(), style.label.clone())
576 .aligned()
577 .boxed(),
578 ])
579 .boxed()
580 }
581
582 fn project_path(&self, _: &AppContext) -> Option<project::ProjectPath> {
583 None
584 }
585
586 fn is_dirty(&self, cx: &AppContext) -> bool {
587 self.excerpts.read(cx).read(cx).is_dirty()
588 }
589
590 fn has_conflict(&self, cx: &AppContext) -> bool {
591 self.excerpts.read(cx).read(cx).has_conflict()
592 }
593
594 fn can_save(&self, _: &AppContext) -> bool {
595 true
596 }
597
598 fn save(&mut self, cx: &mut ViewContext<Self>) -> Result<Task<Result<()>>> {
599 self.excerpts.update(cx, |excerpts, cx| excerpts.save(cx))
600 }
601
602 fn can_save_as(&self, _: &AppContext) -> bool {
603 false
604 }
605
606 fn save_as(
607 &mut self,
608 _: ModelHandle<project::Worktree>,
609 _: &std::path::Path,
610 _: &mut ViewContext<Self>,
611 ) -> Task<Result<()>> {
612 unreachable!()
613 }
614
615 fn should_activate_item_on_event(event: &Self::Event) -> bool {
616 Editor::should_activate_item_on_event(event)
617 }
618
619 fn should_update_tab_on_event(event: &Event) -> bool {
620 matches!(event, Event::Saved | Event::Dirtied | Event::TitleChanged)
621 }
622}
623
624fn path_header_renderer(buffer: ModelHandle<Buffer>, build_settings: BuildSettings) -> RenderBlock {
625 Arc::new(move |cx| {
626 let settings = build_settings(cx);
627 let file_path = if let Some(file) = buffer.read(&**cx).file() {
628 file.path().to_string_lossy().to_string()
629 } else {
630 "untitled".to_string()
631 };
632 let mut text_style = settings.style.text.clone();
633 let style = settings.style.diagnostic_path_header;
634 text_style.color = style.text;
635 Label::new(file_path, text_style)
636 .aligned()
637 .left()
638 .contained()
639 .with_style(style.header)
640 .with_padding_left(cx.line_number_x)
641 .expanded()
642 .boxed()
643 })
644}
645
646fn diagnostic_header_renderer(
647 diagnostic: Diagnostic,
648 is_valid: bool,
649 build_settings: BuildSettings,
650) -> RenderBlock {
651 Arc::new(move |cx| {
652 let settings = build_settings(cx);
653 let mut text_style = settings.style.text.clone();
654 let diagnostic_style = diagnostic_style(diagnostic.severity, is_valid, &settings.style);
655 text_style.color = diagnostic_style.text;
656 Text::new(diagnostic.message.clone(), text_style)
657 .with_soft_wrap(false)
658 .aligned()
659 .left()
660 .contained()
661 .with_style(diagnostic_style.header)
662 .with_padding_left(cx.line_number_x)
663 .expanded()
664 .boxed()
665 })
666}
667
668fn context_header_renderer(build_settings: BuildSettings) -> RenderBlock {
669 Arc::new(move |cx| {
670 let settings = build_settings(cx);
671 let text_style = settings.style.text.clone();
672 Label::new("…".to_string(), text_style)
673 .contained()
674 .with_padding_left(cx.line_number_x)
675 .boxed()
676 })
677}
678
679fn compare_diagnostics<L: language::ToOffset, R: language::ToOffset>(
680 lhs: &DiagnosticEntry<L>,
681 rhs: &DiagnosticEntry<R>,
682 snapshot: &language::BufferSnapshot,
683) -> Ordering {
684 lhs.range
685 .start
686 .to_offset(&snapshot)
687 .cmp(&rhs.range.start.to_offset(snapshot))
688 .then_with(|| {
689 lhs.range
690 .end
691 .to_offset(&snapshot)
692 .cmp(&rhs.range.end.to_offset(snapshot))
693 })
694 .then_with(|| lhs.diagnostic.message.cmp(&rhs.diagnostic.message))
695}
696
697#[cfg(test)]
698mod tests {
699 use super::*;
700 use client::{http::ServerResponse, test::FakeHttpClient, Client, UserStore};
701 use editor::DisplayPoint;
702 use gpui::TestAppContext;
703 use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, LanguageRegistry, PointUtf16};
704 use project::{worktree, FakeFs};
705 use serde_json::json;
706 use std::sync::Arc;
707 use unindent::Unindent as _;
708 use workspace::WorkspaceParams;
709
710 #[gpui::test]
711 async fn test_diagnostics(mut cx: TestAppContext) {
712 let workspace_params = cx.update(WorkspaceParams::test);
713 let settings = workspace_params.settings.clone();
714 let http_client = FakeHttpClient::new(|_| async move { Ok(ServerResponse::new(404)) });
715 let client = Client::new(http_client.clone());
716 let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
717 let fs = Arc::new(FakeFs::new());
718
719 let project = cx.update(|cx| {
720 Project::local(
721 client.clone(),
722 user_store,
723 Arc::new(LanguageRegistry::new()),
724 fs.clone(),
725 cx,
726 )
727 });
728
729 fs.insert_tree(
730 "/test",
731 json!({
732 "a.rs": "
733 const a: i32 = 'a';
734 ".unindent(),
735
736 "main.rs": "
737 fn main() {
738 let x = vec![];
739 let y = vec![];
740 a(x);
741 b(y);
742 // comment 1
743 // comment 2
744 c(y);
745 d(x);
746 }
747 "
748 .unindent(),
749 }),
750 )
751 .await;
752
753 let worktree = project
754 .update(&mut cx, |project, cx| {
755 project.add_local_worktree("/test", cx)
756 })
757 .await
758 .unwrap();
759
760 worktree.update(&mut cx, |worktree, cx| {
761 worktree
762 .update_diagnostic_entries(
763 Arc::from("/test/main.rs".as_ref()),
764 None,
765 vec![
766 DiagnosticEntry {
767 range: PointUtf16::new(1, 8)..PointUtf16::new(1, 9),
768 diagnostic: Diagnostic {
769 message:
770 "move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait"
771 .to_string(),
772 severity: DiagnosticSeverity::INFORMATION,
773 is_primary: false,
774 is_disk_based: true,
775 group_id: 1,
776 ..Default::default()
777 },
778 },
779 DiagnosticEntry {
780 range: PointUtf16::new(2, 8)..PointUtf16::new(2, 9),
781 diagnostic: Diagnostic {
782 message:
783 "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait"
784 .to_string(),
785 severity: DiagnosticSeverity::INFORMATION,
786 is_primary: false,
787 is_disk_based: true,
788 group_id: 0,
789 ..Default::default()
790 },
791 },
792 DiagnosticEntry {
793 range: PointUtf16::new(3, 6)..PointUtf16::new(3, 7),
794 diagnostic: Diagnostic {
795 message: "value moved here".to_string(),
796 severity: DiagnosticSeverity::INFORMATION,
797 is_primary: false,
798 is_disk_based: true,
799 group_id: 1,
800 ..Default::default()
801 },
802 },
803 DiagnosticEntry {
804 range: PointUtf16::new(4, 6)..PointUtf16::new(4, 7),
805 diagnostic: Diagnostic {
806 message: "value moved here".to_string(),
807 severity: DiagnosticSeverity::INFORMATION,
808 is_primary: false,
809 is_disk_based: true,
810 group_id: 0,
811 ..Default::default()
812 },
813 },
814 DiagnosticEntry {
815 range: PointUtf16::new(7, 6)..PointUtf16::new(7, 7),
816 diagnostic: Diagnostic {
817 message: "use of moved value\nvalue used here after move".to_string(),
818 severity: DiagnosticSeverity::ERROR,
819 is_primary: true,
820 is_disk_based: true,
821 group_id: 0,
822 ..Default::default()
823 },
824 },
825 DiagnosticEntry {
826 range: PointUtf16::new(8, 6)..PointUtf16::new(8, 7),
827 diagnostic: Diagnostic {
828 message: "use of moved value\nvalue used here after move".to_string(),
829 severity: DiagnosticSeverity::ERROR,
830 is_primary: true,
831 is_disk_based: true,
832 group_id: 1,
833 ..Default::default()
834 },
835 },
836 ],
837 cx,
838 )
839 .unwrap();
840 });
841
842 let model = cx.add_model(|_| ProjectDiagnostics::new(project.clone()));
843 let workspace = cx.add_view(0, |cx| Workspace::new(&workspace_params, cx));
844
845 let view = cx.add_view(0, |cx| {
846 ProjectDiagnosticsEditor::new(model, workspace.downgrade(), settings, cx)
847 });
848
849 view.condition(&mut cx, |view, cx| view.text(cx).contains("fn main()"))
850 .await;
851
852 view.update(&mut cx, |view, cx| {
853 let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
854
855 assert_eq!(
856 editor.text(),
857 concat!(
858 //
859 // main.rs
860 //
861 "\n", // filename
862 "\n", // padding
863 // diagnostic group 1
864 "\n", // primary message
865 "\n", // padding
866 " let x = vec![];\n",
867 " let y = vec![];\n",
868 "\n", // supporting diagnostic
869 " a(x);\n",
870 " b(y);\n",
871 "\n", // supporting diagnostic
872 " // comment 1\n",
873 " // comment 2\n",
874 " c(y);\n",
875 "\n", // supporting diagnostic
876 " d(x);\n",
877 // diagnostic group 2
878 "\n", // primary message
879 "\n", // padding
880 "fn main() {\n",
881 " let x = vec![];\n",
882 "\n", // supporting diagnostic
883 " let y = vec![];\n",
884 " a(x);\n",
885 "\n", // supporting diagnostic
886 " b(y);\n",
887 "\n", // context ellipsis
888 " c(y);\n",
889 " d(x);\n",
890 "\n", // supporting diagnostic
891 "}"
892 )
893 );
894
895 view.editor.update(cx, |editor, cx| {
896 assert_eq!(
897 editor.selected_display_ranges(cx),
898 [DisplayPoint::new(12, 6)..DisplayPoint::new(12, 6)]
899 );
900 });
901 });
902
903 worktree.update(&mut cx, |worktree, cx| {
904 worktree
905 .update_diagnostic_entries(
906 Arc::from("/test/a.rs".as_ref()),
907 None,
908 vec![DiagnosticEntry {
909 range: PointUtf16::new(0, 15)..PointUtf16::new(0, 15),
910 diagnostic: Diagnostic {
911 message: "mismatched types\nexpected `usize`, found `char`".to_string(),
912 severity: DiagnosticSeverity::ERROR,
913 is_primary: true,
914 is_disk_based: true,
915 group_id: 0,
916 ..Default::default()
917 },
918 }],
919 cx,
920 )
921 .unwrap();
922 cx.emit(worktree::Event::DiskBasedDiagnosticsUpdated);
923 });
924
925 view.condition(&mut cx, |view, cx| view.text(cx).contains("const a"))
926 .await;
927
928 view.update(&mut cx, |view, cx| {
929 let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
930
931 assert_eq!(
932 editor.text(),
933 concat!(
934 //
935 // a.rs
936 //
937 "\n", // filename
938 "\n", // padding
939 // diagnostic group 1
940 "\n", // primary message
941 "\n", // padding
942 "const a: i32 = 'a';\n",
943 "\n", // supporting diagnostic
944 "\n", // context line
945 //
946 // main.rs
947 //
948 "\n", // filename
949 "\n", // padding
950 // diagnostic group 1
951 "\n", // primary message
952 "\n", // padding
953 " let x = vec![];\n",
954 " let y = vec![];\n",
955 "\n", // supporting diagnostic
956 " a(x);\n",
957 " b(y);\n",
958 "\n", // supporting diagnostic
959 " // comment 1\n",
960 " // comment 2\n",
961 " c(y);\n",
962 "\n", // supporting diagnostic
963 " d(x);\n",
964 // diagnostic group 2
965 "\n", // primary message
966 "\n", // filename
967 "fn main() {\n",
968 " let x = vec![];\n",
969 "\n", // supporting diagnostic
970 " let y = vec![];\n",
971 " a(x);\n",
972 "\n", // supporting diagnostic
973 " b(y);\n",
974 "\n", // context ellipsis
975 " c(y);\n",
976 " d(x);\n",
977 "\n", // supporting diagnostic
978 "}"
979 )
980 );
981 });
982 }
983}