1pub mod items;
2mod project_diagnostics_settings;
3mod toolbar_controls;
4
5use anyhow::Result;
6use collections::{BTreeSet, HashSet};
7use editor::{
8 diagnostic_block_renderer,
9 display_map::{BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock},
10 highlight_diagnostic_message,
11 scroll::autoscroll::Autoscroll,
12 Editor, ExcerptId, ExcerptRange, MultiBuffer, ToOffset,
13};
14use gpui::{
15 actions, elements::*, fonts::TextStyle, serde_json, AnyViewHandle, AppContext, Entity,
16 ModelHandle, Task, View, ViewContext, ViewHandle, WeakViewHandle,
17};
18use language::{
19 Anchor, Bias, Buffer, Diagnostic, DiagnosticEntry, DiagnosticSeverity, Point, Selection,
20 SelectionGoal,
21};
22use lsp::LanguageServerId;
23use project::{DiagnosticSummary, Project, ProjectPath};
24use project_diagnostics_settings::ProjectDiagnosticsSettings;
25use serde_json::json;
26use smallvec::SmallVec;
27use std::{
28 any::{Any, TypeId},
29 borrow::Cow,
30 cmp::Ordering,
31 ops::Range,
32 path::PathBuf,
33 sync::Arc,
34};
35use theme::ThemeSettings;
36pub use toolbar_controls::ToolbarControls;
37use util::TryFutureExt;
38use workspace::{
39 item::{BreadcrumbText, Item, ItemEvent, ItemHandle},
40 ItemNavHistory, Pane, PaneBackdrop, ToolbarItemLocation, Workspace,
41};
42
43actions!(diagnostics, [Deploy, ToggleWarnings]);
44
45const CONTEXT_LINE_COUNT: u32 = 1;
46
47pub fn init(cx: &mut AppContext) {
48 settings::register::<ProjectDiagnosticsSettings>(cx);
49 cx.add_action(ProjectDiagnosticsEditor::deploy);
50 cx.add_action(ProjectDiagnosticsEditor::toggle_warnings);
51 items::init(cx);
52}
53
54type Event = editor::Event;
55
56struct ProjectDiagnosticsEditor {
57 project: ModelHandle<Project>,
58 workspace: WeakViewHandle<Workspace>,
59 editor: ViewHandle<Editor>,
60 summary: DiagnosticSummary,
61 excerpts: ModelHandle<MultiBuffer>,
62 path_states: Vec<PathState>,
63 paths_to_update: BTreeSet<(ProjectPath, LanguageServerId)>,
64 include_warnings: bool,
65}
66
67struct PathState {
68 path: ProjectPath,
69 diagnostic_groups: Vec<DiagnosticGroupState>,
70}
71
72#[derive(Clone, Debug, PartialEq)]
73struct Jump {
74 path: ProjectPath,
75 position: Point,
76 anchor: Anchor,
77}
78
79struct DiagnosticGroupState {
80 language_server_id: LanguageServerId,
81 primary_diagnostic: DiagnosticEntry<language::Anchor>,
82 primary_excerpt_ix: usize,
83 excerpts: Vec<ExcerptId>,
84 blocks: HashSet<BlockId>,
85 block_count: usize,
86}
87
88impl Entity for ProjectDiagnosticsEditor {
89 type Event = Event;
90}
91
92impl View for ProjectDiagnosticsEditor {
93 fn ui_name() -> &'static str {
94 "ProjectDiagnosticsEditor"
95 }
96
97 fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
98 if self.path_states.is_empty() {
99 let theme = &theme::current(cx).project_diagnostics;
100 PaneBackdrop::new(
101 cx.view_id(),
102 Label::new("No problems in workspace", theme.empty_message.clone())
103 .aligned()
104 .contained()
105 .with_style(theme.container)
106 .into_any(),
107 )
108 .into_any()
109 } else {
110 ChildView::new(&self.editor, cx).into_any()
111 }
112 }
113
114 fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
115 if cx.is_self_focused() && !self.path_states.is_empty() {
116 cx.focus(&self.editor);
117 }
118 }
119
120 fn debug_json(&self, cx: &AppContext) -> serde_json::Value {
121 let project = self.project.read(cx);
122 json!({
123 "project": json!({
124 "language_servers": project.language_server_statuses().collect::<Vec<_>>(),
125 "summary": project.diagnostic_summary(cx),
126 }),
127 "summary": self.summary,
128 "paths_to_update": self.paths_to_update.iter().map(|(path, server_id)|
129 (path.path.to_string_lossy(), server_id.0)
130 ).collect::<Vec<_>>(),
131 "paths_states": self.path_states.iter().map(|state|
132 json!({
133 "path": state.path.path.to_string_lossy(),
134 "groups": state.diagnostic_groups.iter().map(|group|
135 json!({
136 "block_count": group.blocks.len(),
137 "excerpt_count": group.excerpts.len(),
138 })
139 ).collect::<Vec<_>>(),
140 })
141 ).collect::<Vec<_>>(),
142 })
143 }
144}
145
146impl ProjectDiagnosticsEditor {
147 fn new(
148 project_handle: ModelHandle<Project>,
149 workspace: WeakViewHandle<Workspace>,
150 cx: &mut ViewContext<Self>,
151 ) -> Self {
152 cx.subscribe(&project_handle, |this, _, event, cx| match event {
153 project::Event::DiskBasedDiagnosticsFinished { language_server_id } => {
154 this.update_excerpts(Some(*language_server_id), cx);
155 this.update_title(cx);
156 }
157 project::Event::DiagnosticsUpdated {
158 language_server_id,
159 path,
160 } => {
161 this.paths_to_update
162 .insert((path.clone(), *language_server_id));
163 }
164 _ => {}
165 })
166 .detach();
167
168 let excerpts = cx.add_model(|cx| MultiBuffer::new(project_handle.read(cx).replica_id()));
169 let editor = cx.add_view(|cx| {
170 let mut editor =
171 Editor::for_multibuffer(excerpts.clone(), Some(project_handle.clone()), cx);
172 editor.set_vertical_scroll_margin(5, cx);
173 editor
174 });
175 cx.subscribe(&editor, |this, _, event, cx| {
176 cx.emit(event.clone());
177 if event == &editor::Event::Focused && this.path_states.is_empty() {
178 cx.focus_self()
179 }
180 })
181 .detach();
182
183 let project = project_handle.read(cx);
184 let paths_to_update = project
185 .diagnostic_summaries(cx)
186 .map(|(path, server_id, _)| (path, server_id))
187 .collect();
188 let summary = project.diagnostic_summary(cx);
189 let mut this = Self {
190 project: project_handle,
191 summary,
192 workspace,
193 excerpts,
194 editor,
195 path_states: Default::default(),
196 paths_to_update,
197 include_warnings: settings::get::<ProjectDiagnosticsSettings>(cx).include_warnings,
198 };
199 this.update_excerpts(None, cx);
200 this
201 }
202
203 fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext<Workspace>) {
204 if let Some(existing) = workspace.item_of_type::<ProjectDiagnosticsEditor>(cx) {
205 workspace.activate_item(&existing, cx);
206 } else {
207 let workspace_handle = cx.weak_handle();
208 let diagnostics = cx.add_view(|cx| {
209 ProjectDiagnosticsEditor::new(workspace.project().clone(), workspace_handle, cx)
210 });
211 workspace.add_item(Box::new(diagnostics), cx);
212 }
213 }
214
215 fn toggle_warnings(&mut self, _: &ToggleWarnings, cx: &mut ViewContext<Self>) {
216 self.include_warnings = !self.include_warnings;
217 self.paths_to_update = self
218 .project
219 .read(cx)
220 .diagnostic_summaries(cx)
221 .map(|(path, server_id, _)| (path, server_id))
222 .collect();
223 self.update_excerpts(None, cx);
224 cx.notify();
225 }
226
227 fn update_excerpts(
228 &mut self,
229 language_server_id: Option<LanguageServerId>,
230 cx: &mut ViewContext<Self>,
231 ) {
232 let mut paths = Vec::new();
233 self.paths_to_update.retain(|(path, server_id)| {
234 if language_server_id
235 .map_or(true, |language_server_id| language_server_id == *server_id)
236 {
237 paths.push(path.clone());
238 false
239 } else {
240 true
241 }
242 });
243 let project = self.project.clone();
244 cx.spawn(|this, mut cx| {
245 async move {
246 for path in paths {
247 let buffer = project
248 .update(&mut cx, |project, cx| project.open_buffer(path.clone(), cx))
249 .await?;
250 this.update(&mut cx, |this, cx| {
251 this.populate_excerpts(path, language_server_id, buffer, cx)
252 })?;
253 }
254 Result::<_, anyhow::Error>::Ok(())
255 }
256 .log_err()
257 })
258 .detach();
259 }
260
261 fn populate_excerpts(
262 &mut self,
263 path: ProjectPath,
264 language_server_id: Option<LanguageServerId>,
265 buffer: ModelHandle<Buffer>,
266 cx: &mut ViewContext<Self>,
267 ) {
268 let was_empty = self.path_states.is_empty();
269 let snapshot = buffer.read(cx).snapshot();
270 let path_ix = match self.path_states.binary_search_by_key(&&path, |e| &e.path) {
271 Ok(ix) => ix,
272 Err(ix) => {
273 self.path_states.insert(
274 ix,
275 PathState {
276 path: path.clone(),
277 diagnostic_groups: Default::default(),
278 },
279 );
280 ix
281 }
282 };
283
284 let mut prev_excerpt_id = if path_ix > 0 {
285 let prev_path_last_group = &self.path_states[path_ix - 1]
286 .diagnostic_groups
287 .last()
288 .unwrap();
289 prev_path_last_group.excerpts.last().unwrap().clone()
290 } else {
291 ExcerptId::min()
292 };
293
294 let path_state = &mut self.path_states[path_ix];
295 let mut groups_to_add = Vec::new();
296 let mut group_ixs_to_remove = Vec::new();
297 let mut blocks_to_add = Vec::new();
298 let mut blocks_to_remove = HashSet::default();
299 let mut first_excerpt_id = None;
300 let max_severity = if self.include_warnings {
301 DiagnosticSeverity::WARNING
302 } else {
303 DiagnosticSeverity::ERROR
304 };
305 let excerpts_snapshot = self.excerpts.update(cx, |excerpts, excerpts_cx| {
306 let mut old_groups = path_state.diagnostic_groups.iter().enumerate().peekable();
307 let mut new_groups = snapshot
308 .diagnostic_groups(language_server_id)
309 .into_iter()
310 .filter(|(_, group)| {
311 group.entries[group.primary_ix].diagnostic.severity <= max_severity
312 })
313 .peekable();
314 loop {
315 let mut to_insert = None;
316 let mut to_remove = None;
317 let mut to_keep = None;
318 match (old_groups.peek(), new_groups.peek()) {
319 (None, None) => break,
320 (None, Some(_)) => to_insert = new_groups.next(),
321 (Some((_, old_group)), None) => {
322 if language_server_id.map_or(true, |id| id == old_group.language_server_id)
323 {
324 to_remove = old_groups.next();
325 } else {
326 to_keep = old_groups.next();
327 }
328 }
329 (Some((_, old_group)), Some((_, new_group))) => {
330 let old_primary = &old_group.primary_diagnostic;
331 let new_primary = &new_group.entries[new_group.primary_ix];
332 match compare_diagnostics(old_primary, new_primary, &snapshot) {
333 Ordering::Less => {
334 if language_server_id
335 .map_or(true, |id| id == old_group.language_server_id)
336 {
337 to_remove = old_groups.next();
338 } else {
339 to_keep = old_groups.next();
340 }
341 }
342 Ordering::Equal => {
343 to_keep = old_groups.next();
344 new_groups.next();
345 }
346 Ordering::Greater => to_insert = new_groups.next(),
347 }
348 }
349 }
350
351 if let Some((language_server_id, group)) = to_insert {
352 let mut group_state = DiagnosticGroupState {
353 language_server_id,
354 primary_diagnostic: group.entries[group.primary_ix].clone(),
355 primary_excerpt_ix: 0,
356 excerpts: Default::default(),
357 blocks: Default::default(),
358 block_count: 0,
359 };
360 let mut pending_range: Option<(Range<Point>, usize)> = None;
361 let mut is_first_excerpt_for_group = true;
362 for (ix, entry) in group.entries.iter().map(Some).chain([None]).enumerate() {
363 let resolved_entry = entry.map(|e| e.resolve::<Point>(&snapshot));
364 if let Some((range, start_ix)) = &mut pending_range {
365 if let Some(entry) = resolved_entry.as_ref() {
366 if entry.range.start.row
367 <= range.end.row + 1 + CONTEXT_LINE_COUNT * 2
368 {
369 range.end = range.end.max(entry.range.end);
370 continue;
371 }
372 }
373
374 let excerpt_start =
375 Point::new(range.start.row.saturating_sub(CONTEXT_LINE_COUNT), 0);
376 let excerpt_end = snapshot.clip_point(
377 Point::new(range.end.row + CONTEXT_LINE_COUNT, u32::MAX),
378 Bias::Left,
379 );
380 let excerpt_id = excerpts
381 .insert_excerpts_after(
382 prev_excerpt_id,
383 buffer.clone(),
384 [ExcerptRange {
385 context: excerpt_start..excerpt_end,
386 primary: Some(range.clone()),
387 }],
388 excerpts_cx,
389 )
390 .pop()
391 .unwrap();
392
393 prev_excerpt_id = excerpt_id.clone();
394 first_excerpt_id.get_or_insert_with(|| prev_excerpt_id.clone());
395 group_state.excerpts.push(excerpt_id.clone());
396 let header_position = (excerpt_id.clone(), language::Anchor::MIN);
397
398 if is_first_excerpt_for_group {
399 is_first_excerpt_for_group = false;
400 let mut primary =
401 group.entries[group.primary_ix].diagnostic.clone();
402 primary.message =
403 primary.message.split('\n').next().unwrap().to_string();
404 group_state.block_count += 1;
405 blocks_to_add.push(BlockProperties {
406 position: header_position,
407 height: 2,
408 style: BlockStyle::Sticky,
409 render: diagnostic_header_renderer(primary),
410 disposition: BlockDisposition::Above,
411 });
412 }
413
414 for entry in &group.entries[*start_ix..ix] {
415 let mut diagnostic = entry.diagnostic.clone();
416 if diagnostic.is_primary {
417 group_state.primary_excerpt_ix = group_state.excerpts.len() - 1;
418 diagnostic.message =
419 entry.diagnostic.message.split('\n').skip(1).collect();
420 }
421
422 if !diagnostic.message.is_empty() {
423 group_state.block_count += 1;
424 blocks_to_add.push(BlockProperties {
425 position: (excerpt_id.clone(), entry.range.start),
426 height: diagnostic.message.matches('\n').count() as u8 + 1,
427 style: BlockStyle::Fixed,
428 render: diagnostic_block_renderer(diagnostic, true),
429 disposition: BlockDisposition::Below,
430 });
431 }
432 }
433
434 pending_range.take();
435 }
436
437 if let Some(entry) = resolved_entry {
438 pending_range = Some((entry.range.clone(), ix));
439 }
440 }
441
442 groups_to_add.push(group_state);
443 } else if let Some((group_ix, group_state)) = to_remove {
444 excerpts.remove_excerpts(group_state.excerpts.iter().copied(), excerpts_cx);
445 group_ixs_to_remove.push(group_ix);
446 blocks_to_remove.extend(group_state.blocks.iter().copied());
447 } else if let Some((_, group)) = to_keep {
448 prev_excerpt_id = group.excerpts.last().unwrap().clone();
449 first_excerpt_id.get_or_insert_with(|| prev_excerpt_id.clone());
450 }
451 }
452
453 excerpts.snapshot(excerpts_cx)
454 });
455
456 self.editor.update(cx, |editor, cx| {
457 editor.remove_blocks(blocks_to_remove, None, cx);
458 let block_ids = editor.insert_blocks(
459 blocks_to_add.into_iter().map(|block| {
460 let (excerpt_id, text_anchor) = block.position;
461 BlockProperties {
462 position: excerpts_snapshot.anchor_in_excerpt(excerpt_id, text_anchor),
463 height: block.height,
464 style: block.style,
465 render: block.render,
466 disposition: block.disposition,
467 }
468 }),
469 Some(Autoscroll::fit()),
470 cx,
471 );
472
473 let mut block_ids = block_ids.into_iter();
474 for group_state in &mut groups_to_add {
475 group_state.blocks = block_ids.by_ref().take(group_state.block_count).collect();
476 }
477 });
478
479 for ix in group_ixs_to_remove.into_iter().rev() {
480 path_state.diagnostic_groups.remove(ix);
481 }
482 path_state.diagnostic_groups.extend(groups_to_add);
483 path_state.diagnostic_groups.sort_unstable_by(|a, b| {
484 let range_a = &a.primary_diagnostic.range;
485 let range_b = &b.primary_diagnostic.range;
486 range_a
487 .start
488 .cmp(&range_b.start, &snapshot)
489 .then_with(|| range_a.end.cmp(&range_b.end, &snapshot))
490 });
491
492 if path_state.diagnostic_groups.is_empty() {
493 self.path_states.remove(path_ix);
494 }
495
496 self.editor.update(cx, |editor, cx| {
497 let groups;
498 let mut selections;
499 let new_excerpt_ids_by_selection_id;
500 if was_empty {
501 groups = self.path_states.first()?.diagnostic_groups.as_slice();
502 new_excerpt_ids_by_selection_id = [(0, ExcerptId::min())].into_iter().collect();
503 selections = vec![Selection {
504 id: 0,
505 start: 0,
506 end: 0,
507 reversed: false,
508 goal: SelectionGoal::None,
509 }];
510 } else {
511 groups = self.path_states.get(path_ix)?.diagnostic_groups.as_slice();
512 new_excerpt_ids_by_selection_id =
513 editor.change_selections(Some(Autoscroll::fit()), cx, |s| s.refresh());
514 selections = editor.selections.all::<usize>(cx);
515 }
516
517 // If any selection has lost its position, move it to start of the next primary diagnostic.
518 let snapshot = editor.snapshot(cx);
519 for selection in &mut selections {
520 if let Some(new_excerpt_id) = new_excerpt_ids_by_selection_id.get(&selection.id) {
521 let group_ix = match groups.binary_search_by(|probe| {
522 probe
523 .excerpts
524 .last()
525 .unwrap()
526 .cmp(new_excerpt_id, &snapshot.buffer_snapshot)
527 }) {
528 Ok(ix) | Err(ix) => ix,
529 };
530 if let Some(group) = groups.get(group_ix) {
531 let offset = excerpts_snapshot
532 .anchor_in_excerpt(
533 group.excerpts[group.primary_excerpt_ix].clone(),
534 group.primary_diagnostic.range.start,
535 )
536 .to_offset(&excerpts_snapshot);
537 selection.start = offset;
538 selection.end = offset;
539 }
540 }
541 }
542 editor.change_selections(None, cx, |s| {
543 s.select(selections);
544 });
545 Some(())
546 });
547
548 if self.path_states.is_empty() {
549 if self.editor.is_focused(cx) {
550 cx.focus_self();
551 }
552 } else if cx.handle().is_focused(cx) {
553 cx.focus(&self.editor);
554 }
555 cx.notify();
556 }
557
558 fn update_title(&mut self, cx: &mut ViewContext<Self>) {
559 self.summary = self.project.read(cx).diagnostic_summary(cx);
560 cx.emit(Event::TitleChanged);
561 }
562}
563
564impl Item for ProjectDiagnosticsEditor {
565 fn tab_content<T: 'static>(
566 &self,
567 _detail: Option<usize>,
568 style: &theme::Tab,
569 cx: &AppContext,
570 ) -> AnyElement<T> {
571 render_summary(
572 &self.summary,
573 &style.label.text,
574 &theme::current(cx).project_diagnostics,
575 )
576 }
577
578 fn for_each_project_item(&self, cx: &AppContext, f: &mut dyn FnMut(usize, &dyn project::Item)) {
579 self.editor.for_each_project_item(cx, f)
580 }
581
582 fn is_singleton(&self, _: &AppContext) -> bool {
583 false
584 }
585
586 fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext<Self>) {
587 self.editor
588 .update(cx, |editor, cx| editor.added_to_workspace(workspace, cx));
589 }
590
591 fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> bool {
592 self.editor
593 .update(cx, |editor, cx| editor.navigate(data, cx))
594 }
595
596 fn tab_tooltip_text(&self, _: &AppContext) -> Option<Cow<str>> {
597 Some("Project Diagnostics".into())
598 }
599
600 fn is_dirty(&self, cx: &AppContext) -> bool {
601 self.excerpts.read(cx).is_dirty(cx)
602 }
603
604 fn has_conflict(&self, cx: &AppContext) -> bool {
605 self.excerpts.read(cx).has_conflict(cx)
606 }
607
608 fn can_save(&self, _: &AppContext) -> bool {
609 true
610 }
611
612 fn save(
613 &mut self,
614 project: ModelHandle<Project>,
615 cx: &mut ViewContext<Self>,
616 ) -> Task<Result<()>> {
617 self.editor.save(project, cx)
618 }
619
620 fn reload(
621 &mut self,
622 project: ModelHandle<Project>,
623 cx: &mut ViewContext<Self>,
624 ) -> Task<Result<()>> {
625 self.editor.reload(project, cx)
626 }
627
628 fn save_as(
629 &mut self,
630 _: ModelHandle<Project>,
631 _: PathBuf,
632 _: &mut ViewContext<Self>,
633 ) -> Task<Result<()>> {
634 unreachable!()
635 }
636
637 fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> {
638 Editor::to_item_events(event)
639 }
640
641 fn set_nav_history(&mut self, nav_history: ItemNavHistory, cx: &mut ViewContext<Self>) {
642 self.editor.update(cx, |editor, _| {
643 editor.set_nav_history(Some(nav_history));
644 });
645 }
646
647 fn clone_on_split(
648 &self,
649 _workspace_id: workspace::WorkspaceId,
650 cx: &mut ViewContext<Self>,
651 ) -> Option<Self>
652 where
653 Self: Sized,
654 {
655 Some(ProjectDiagnosticsEditor::new(
656 self.project.clone(),
657 self.workspace.clone(),
658 cx,
659 ))
660 }
661
662 fn act_as_type<'a>(
663 &'a self,
664 type_id: TypeId,
665 self_handle: &'a ViewHandle<Self>,
666 _: &'a AppContext,
667 ) -> Option<&AnyViewHandle> {
668 if type_id == TypeId::of::<Self>() {
669 Some(self_handle)
670 } else if type_id == TypeId::of::<Editor>() {
671 Some(&self.editor)
672 } else {
673 None
674 }
675 }
676
677 fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
678 self.editor.update(cx, |editor, cx| editor.deactivated(cx));
679 }
680
681 fn serialized_item_kind() -> Option<&'static str> {
682 Some("diagnostics")
683 }
684
685 fn breadcrumbs(&self, theme: &theme::Theme, cx: &AppContext) -> Option<Vec<BreadcrumbText>> {
686 self.editor.breadcrumbs(theme, cx)
687 }
688
689 fn breadcrumb_location(&self) -> ToolbarItemLocation {
690 ToolbarItemLocation::PrimaryLeft { flex: None }
691 }
692
693 fn deserialize(
694 project: ModelHandle<Project>,
695 workspace: WeakViewHandle<Workspace>,
696 _workspace_id: workspace::WorkspaceId,
697 _item_id: workspace::ItemId,
698 cx: &mut ViewContext<Pane>,
699 ) -> Task<Result<ViewHandle<Self>>> {
700 Task::ready(Ok(cx.add_view(|cx| Self::new(project, workspace, cx))))
701 }
702}
703
704fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
705 let (message, highlights) = highlight_diagnostic_message(Vec::new(), &diagnostic.message);
706 Arc::new(move |cx| {
707 let settings = settings::get::<ThemeSettings>(cx);
708 let theme = &settings.theme.editor;
709 let style = theme.diagnostic_header.clone();
710 let font_size = (style.text_scale_factor * settings.buffer_font_size(cx)).round();
711 let icon_width = cx.em_width * style.icon_width_factor;
712 let icon = if diagnostic.severity == DiagnosticSeverity::ERROR {
713 Svg::new("icons/error.svg").with_color(theme.error_diagnostic.message.text.color)
714 } else {
715 Svg::new("icons/warning.svg").with_color(theme.warning_diagnostic.message.text.color)
716 };
717
718 Flex::row()
719 .with_child(
720 icon.constrained()
721 .with_width(icon_width)
722 .aligned()
723 .contained()
724 .with_margin_right(cx.gutter_padding),
725 )
726 .with_children(diagnostic.source.as_ref().map(|source| {
727 Label::new(
728 format!("{source}: "),
729 style.source.label.clone().with_font_size(font_size),
730 )
731 .contained()
732 .with_style(style.message.container)
733 .aligned()
734 }))
735 .with_child(
736 Label::new(
737 message.clone(),
738 style.message.label.clone().with_font_size(font_size),
739 )
740 .with_highlights(highlights.clone())
741 .contained()
742 .with_style(style.message.container)
743 .aligned(),
744 )
745 .with_children(diagnostic.code.clone().map(|code| {
746 Label::new(code, style.code.text.clone().with_font_size(font_size))
747 .contained()
748 .with_style(style.code.container)
749 .aligned()
750 }))
751 .contained()
752 .with_style(style.container)
753 .with_padding_left(cx.gutter_padding)
754 .with_padding_right(cx.gutter_padding)
755 .expanded()
756 .into_any_named("diagnostic header")
757 })
758}
759
760pub(crate) fn render_summary<T: 'static>(
761 summary: &DiagnosticSummary,
762 text_style: &TextStyle,
763 theme: &theme::ProjectDiagnostics,
764) -> AnyElement<T> {
765 if summary.error_count == 0 && summary.warning_count == 0 {
766 Label::new("No problems", text_style.clone()).into_any()
767 } else {
768 let icon_width = theme.tab_icon_width;
769 let icon_spacing = theme.tab_icon_spacing;
770 let summary_spacing = theme.tab_summary_spacing;
771 Flex::row()
772 .with_child(
773 Svg::new("icons/error.svg")
774 .with_color(text_style.color)
775 .constrained()
776 .with_width(icon_width)
777 .aligned()
778 .contained()
779 .with_margin_right(icon_spacing),
780 )
781 .with_child(
782 Label::new(
783 summary.error_count.to_string(),
784 LabelStyle {
785 text: text_style.clone(),
786 highlight_text: None,
787 },
788 )
789 .aligned(),
790 )
791 .with_child(
792 Svg::new("icons/warning.svg")
793 .with_color(text_style.color)
794 .constrained()
795 .with_width(icon_width)
796 .aligned()
797 .contained()
798 .with_margin_left(summary_spacing)
799 .with_margin_right(icon_spacing),
800 )
801 .with_child(
802 Label::new(
803 summary.warning_count.to_string(),
804 LabelStyle {
805 text: text_style.clone(),
806 highlight_text: None,
807 },
808 )
809 .aligned(),
810 )
811 .into_any()
812 }
813}
814
815fn compare_diagnostics<L: language::ToOffset, R: language::ToOffset>(
816 lhs: &DiagnosticEntry<L>,
817 rhs: &DiagnosticEntry<R>,
818 snapshot: &language::BufferSnapshot,
819) -> Ordering {
820 lhs.range
821 .start
822 .to_offset(snapshot)
823 .cmp(&rhs.range.start.to_offset(snapshot))
824 .then_with(|| {
825 lhs.range
826 .end
827 .to_offset(snapshot)
828 .cmp(&rhs.range.end.to_offset(snapshot))
829 })
830 .then_with(|| lhs.diagnostic.message.cmp(&rhs.diagnostic.message))
831}
832
833#[cfg(test)]
834mod tests {
835 use super::*;
836 use editor::{
837 display_map::{BlockContext, TransformBlock},
838 DisplayPoint,
839 };
840 use gpui::{TestAppContext, WindowContext};
841 use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16, Unclipped};
842 use project::FakeFs;
843 use serde_json::json;
844 use settings::SettingsStore;
845 use unindent::Unindent as _;
846
847 #[gpui::test]
848 async fn test_diagnostics(cx: &mut TestAppContext) {
849 init_test(cx);
850
851 let fs = FakeFs::new(cx.background());
852 fs.insert_tree(
853 "/test",
854 json!({
855 "consts.rs": "
856 const a: i32 = 'a';
857 const b: i32 = c;
858 "
859 .unindent(),
860
861 "main.rs": "
862 fn main() {
863 let x = vec![];
864 let y = vec![];
865 a(x);
866 b(y);
867 // comment 1
868 // comment 2
869 c(y);
870 d(x);
871 }
872 "
873 .unindent(),
874 }),
875 )
876 .await;
877
878 let language_server_id = LanguageServerId(0);
879 let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
880 let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
881 let workspace = window.root(cx);
882
883 // Create some diagnostics
884 project.update(cx, |project, cx| {
885 project
886 .update_diagnostic_entries(
887 language_server_id,
888 PathBuf::from("/test/main.rs"),
889 None,
890 vec![
891 DiagnosticEntry {
892 range: Unclipped(PointUtf16::new(1, 8))..Unclipped(PointUtf16::new(1, 9)),
893 diagnostic: Diagnostic {
894 message:
895 "move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait"
896 .to_string(),
897 severity: DiagnosticSeverity::INFORMATION,
898 is_primary: false,
899 is_disk_based: true,
900 group_id: 1,
901 ..Default::default()
902 },
903 },
904 DiagnosticEntry {
905 range: Unclipped(PointUtf16::new(2, 8))..Unclipped(PointUtf16::new(2, 9)),
906 diagnostic: Diagnostic {
907 message:
908 "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait"
909 .to_string(),
910 severity: DiagnosticSeverity::INFORMATION,
911 is_primary: false,
912 is_disk_based: true,
913 group_id: 0,
914 ..Default::default()
915 },
916 },
917 DiagnosticEntry {
918 range: Unclipped(PointUtf16::new(3, 6))..Unclipped(PointUtf16::new(3, 7)),
919 diagnostic: Diagnostic {
920 message: "value moved here".to_string(),
921 severity: DiagnosticSeverity::INFORMATION,
922 is_primary: false,
923 is_disk_based: true,
924 group_id: 1,
925 ..Default::default()
926 },
927 },
928 DiagnosticEntry {
929 range: Unclipped(PointUtf16::new(4, 6))..Unclipped(PointUtf16::new(4, 7)),
930 diagnostic: Diagnostic {
931 message: "value moved here".to_string(),
932 severity: DiagnosticSeverity::INFORMATION,
933 is_primary: false,
934 is_disk_based: true,
935 group_id: 0,
936 ..Default::default()
937 },
938 },
939 DiagnosticEntry {
940 range: Unclipped(PointUtf16::new(7, 6))..Unclipped(PointUtf16::new(7, 7)),
941 diagnostic: Diagnostic {
942 message: "use of moved value\nvalue used here after move".to_string(),
943 severity: DiagnosticSeverity::ERROR,
944 is_primary: true,
945 is_disk_based: true,
946 group_id: 0,
947 ..Default::default()
948 },
949 },
950 DiagnosticEntry {
951 range: Unclipped(PointUtf16::new(8, 6))..Unclipped(PointUtf16::new(8, 7)),
952 diagnostic: Diagnostic {
953 message: "use of moved value\nvalue used here after move".to_string(),
954 severity: DiagnosticSeverity::ERROR,
955 is_primary: true,
956 is_disk_based: true,
957 group_id: 1,
958 ..Default::default()
959 },
960 },
961 ],
962 cx,
963 )
964 .unwrap();
965 });
966
967 // Open the project diagnostics view while there are already diagnostics.
968 let view = window.add_view(cx, |cx| {
969 ProjectDiagnosticsEditor::new(project.clone(), workspace.downgrade(), cx)
970 });
971
972 view.next_notification(cx).await;
973 view.update(cx, |view, cx| {
974 assert_eq!(
975 editor_blocks(&view.editor, cx),
976 [
977 (0, "path header block".into()),
978 (2, "diagnostic header".into()),
979 (15, "collapsed context".into()),
980 (16, "diagnostic header".into()),
981 (25, "collapsed context".into()),
982 ]
983 );
984 assert_eq!(
985 view.editor.update(cx, |editor, cx| editor.display_text(cx)),
986 concat!(
987 //
988 // main.rs
989 //
990 "\n", // filename
991 "\n", // padding
992 // diagnostic group 1
993 "\n", // primary message
994 "\n", // padding
995 " let x = vec![];\n",
996 " let y = vec![];\n",
997 "\n", // supporting diagnostic
998 " a(x);\n",
999 " b(y);\n",
1000 "\n", // supporting diagnostic
1001 " // comment 1\n",
1002 " // comment 2\n",
1003 " c(y);\n",
1004 "\n", // supporting diagnostic
1005 " d(x);\n",
1006 "\n", // context ellipsis
1007 // diagnostic group 2
1008 "\n", // primary message
1009 "\n", // padding
1010 "fn main() {\n",
1011 " let x = vec![];\n",
1012 "\n", // supporting diagnostic
1013 " let y = vec![];\n",
1014 " a(x);\n",
1015 "\n", // supporting diagnostic
1016 " b(y);\n",
1017 "\n", // context ellipsis
1018 " c(y);\n",
1019 " d(x);\n",
1020 "\n", // supporting diagnostic
1021 "}"
1022 )
1023 );
1024
1025 // Cursor is at the first diagnostic
1026 view.editor.update(cx, |editor, cx| {
1027 assert_eq!(
1028 editor.selections.display_ranges(cx),
1029 [DisplayPoint::new(12, 6)..DisplayPoint::new(12, 6)]
1030 );
1031 });
1032 });
1033
1034 // Diagnostics are added for another earlier path.
1035 project.update(cx, |project, cx| {
1036 project.disk_based_diagnostics_started(language_server_id, cx);
1037 project
1038 .update_diagnostic_entries(
1039 language_server_id,
1040 PathBuf::from("/test/consts.rs"),
1041 None,
1042 vec![DiagnosticEntry {
1043 range: Unclipped(PointUtf16::new(0, 15))..Unclipped(PointUtf16::new(0, 15)),
1044 diagnostic: Diagnostic {
1045 message: "mismatched types\nexpected `usize`, found `char`".to_string(),
1046 severity: DiagnosticSeverity::ERROR,
1047 is_primary: true,
1048 is_disk_based: true,
1049 group_id: 0,
1050 ..Default::default()
1051 },
1052 }],
1053 cx,
1054 )
1055 .unwrap();
1056 project.disk_based_diagnostics_finished(language_server_id, cx);
1057 });
1058
1059 view.next_notification(cx).await;
1060 view.update(cx, |view, cx| {
1061 assert_eq!(
1062 editor_blocks(&view.editor, cx),
1063 [
1064 (0, "path header block".into()),
1065 (2, "diagnostic header".into()),
1066 (7, "path header block".into()),
1067 (9, "diagnostic header".into()),
1068 (22, "collapsed context".into()),
1069 (23, "diagnostic header".into()),
1070 (32, "collapsed context".into()),
1071 ]
1072 );
1073 assert_eq!(
1074 view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1075 concat!(
1076 //
1077 // consts.rs
1078 //
1079 "\n", // filename
1080 "\n", // padding
1081 // diagnostic group 1
1082 "\n", // primary message
1083 "\n", // padding
1084 "const a: i32 = 'a';\n",
1085 "\n", // supporting diagnostic
1086 "const b: i32 = c;\n",
1087 //
1088 // main.rs
1089 //
1090 "\n", // filename
1091 "\n", // padding
1092 // diagnostic group 1
1093 "\n", // primary message
1094 "\n", // padding
1095 " let x = vec![];\n",
1096 " let y = vec![];\n",
1097 "\n", // supporting diagnostic
1098 " a(x);\n",
1099 " b(y);\n",
1100 "\n", // supporting diagnostic
1101 " // comment 1\n",
1102 " // comment 2\n",
1103 " c(y);\n",
1104 "\n", // supporting diagnostic
1105 " d(x);\n",
1106 "\n", // collapsed context
1107 // diagnostic group 2
1108 "\n", // primary message
1109 "\n", // filename
1110 "fn main() {\n",
1111 " let x = vec![];\n",
1112 "\n", // supporting diagnostic
1113 " let y = vec![];\n",
1114 " a(x);\n",
1115 "\n", // supporting diagnostic
1116 " b(y);\n",
1117 "\n", // context ellipsis
1118 " c(y);\n",
1119 " d(x);\n",
1120 "\n", // supporting diagnostic
1121 "}"
1122 )
1123 );
1124
1125 // Cursor keeps its position.
1126 view.editor.update(cx, |editor, cx| {
1127 assert_eq!(
1128 editor.selections.display_ranges(cx),
1129 [DisplayPoint::new(19, 6)..DisplayPoint::new(19, 6)]
1130 );
1131 });
1132 });
1133
1134 // Diagnostics are added to the first path
1135 project.update(cx, |project, cx| {
1136 project.disk_based_diagnostics_started(language_server_id, cx);
1137 project
1138 .update_diagnostic_entries(
1139 language_server_id,
1140 PathBuf::from("/test/consts.rs"),
1141 None,
1142 vec![
1143 DiagnosticEntry {
1144 range: Unclipped(PointUtf16::new(0, 15))
1145 ..Unclipped(PointUtf16::new(0, 15)),
1146 diagnostic: Diagnostic {
1147 message: "mismatched types\nexpected `usize`, found `char`"
1148 .to_string(),
1149 severity: DiagnosticSeverity::ERROR,
1150 is_primary: true,
1151 is_disk_based: true,
1152 group_id: 0,
1153 ..Default::default()
1154 },
1155 },
1156 DiagnosticEntry {
1157 range: Unclipped(PointUtf16::new(1, 15))
1158 ..Unclipped(PointUtf16::new(1, 15)),
1159 diagnostic: Diagnostic {
1160 message: "unresolved name `c`".to_string(),
1161 severity: DiagnosticSeverity::ERROR,
1162 is_primary: true,
1163 is_disk_based: true,
1164 group_id: 1,
1165 ..Default::default()
1166 },
1167 },
1168 ],
1169 cx,
1170 )
1171 .unwrap();
1172 project.disk_based_diagnostics_finished(language_server_id, cx);
1173 });
1174
1175 view.next_notification(cx).await;
1176 view.update(cx, |view, cx| {
1177 assert_eq!(
1178 editor_blocks(&view.editor, cx),
1179 [
1180 (0, "path header block".into()),
1181 (2, "diagnostic header".into()),
1182 (7, "collapsed context".into()),
1183 (8, "diagnostic header".into()),
1184 (13, "path header block".into()),
1185 (15, "diagnostic header".into()),
1186 (28, "collapsed context".into()),
1187 (29, "diagnostic header".into()),
1188 (38, "collapsed context".into()),
1189 ]
1190 );
1191 assert_eq!(
1192 view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1193 concat!(
1194 //
1195 // consts.rs
1196 //
1197 "\n", // filename
1198 "\n", // padding
1199 // diagnostic group 1
1200 "\n", // primary message
1201 "\n", // padding
1202 "const a: i32 = 'a';\n",
1203 "\n", // supporting diagnostic
1204 "const b: i32 = c;\n",
1205 "\n", // context ellipsis
1206 // diagnostic group 2
1207 "\n", // primary message
1208 "\n", // padding
1209 "const a: i32 = 'a';\n",
1210 "const b: i32 = c;\n",
1211 "\n", // supporting diagnostic
1212 //
1213 // main.rs
1214 //
1215 "\n", // filename
1216 "\n", // padding
1217 // diagnostic group 1
1218 "\n", // primary message
1219 "\n", // padding
1220 " let x = vec![];\n",
1221 " let y = vec![];\n",
1222 "\n", // supporting diagnostic
1223 " a(x);\n",
1224 " b(y);\n",
1225 "\n", // supporting diagnostic
1226 " // comment 1\n",
1227 " // comment 2\n",
1228 " c(y);\n",
1229 "\n", // supporting diagnostic
1230 " d(x);\n",
1231 "\n", // context ellipsis
1232 // diagnostic group 2
1233 "\n", // primary message
1234 "\n", // filename
1235 "fn main() {\n",
1236 " let x = vec![];\n",
1237 "\n", // supporting diagnostic
1238 " let y = vec![];\n",
1239 " a(x);\n",
1240 "\n", // supporting diagnostic
1241 " b(y);\n",
1242 "\n", // context ellipsis
1243 " c(y);\n",
1244 " d(x);\n",
1245 "\n", // supporting diagnostic
1246 "}"
1247 )
1248 );
1249 });
1250 }
1251
1252 #[gpui::test]
1253 async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
1254 init_test(cx);
1255
1256 let fs = FakeFs::new(cx.background());
1257 fs.insert_tree(
1258 "/test",
1259 json!({
1260 "main.js": "
1261 a();
1262 b();
1263 c();
1264 d();
1265 e();
1266 ".unindent()
1267 }),
1268 )
1269 .await;
1270
1271 let server_id_1 = LanguageServerId(100);
1272 let server_id_2 = LanguageServerId(101);
1273 let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
1274 let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
1275 let workspace = window.root(cx);
1276
1277 let view = window.add_view(cx, |cx| {
1278 ProjectDiagnosticsEditor::new(project.clone(), workspace.downgrade(), cx)
1279 });
1280
1281 // Two language servers start updating diagnostics
1282 project.update(cx, |project, cx| {
1283 project.disk_based_diagnostics_started(server_id_1, cx);
1284 project.disk_based_diagnostics_started(server_id_2, cx);
1285 project
1286 .update_diagnostic_entries(
1287 server_id_1,
1288 PathBuf::from("/test/main.js"),
1289 None,
1290 vec![DiagnosticEntry {
1291 range: Unclipped(PointUtf16::new(0, 0))..Unclipped(PointUtf16::new(0, 1)),
1292 diagnostic: Diagnostic {
1293 message: "error 1".to_string(),
1294 severity: DiagnosticSeverity::WARNING,
1295 is_primary: true,
1296 is_disk_based: true,
1297 group_id: 1,
1298 ..Default::default()
1299 },
1300 }],
1301 cx,
1302 )
1303 .unwrap();
1304 project
1305 .update_diagnostic_entries(
1306 server_id_2,
1307 PathBuf::from("/test/main.js"),
1308 None,
1309 vec![DiagnosticEntry {
1310 range: Unclipped(PointUtf16::new(1, 0))..Unclipped(PointUtf16::new(1, 1)),
1311 diagnostic: Diagnostic {
1312 message: "warning 1".to_string(),
1313 severity: DiagnosticSeverity::ERROR,
1314 is_primary: true,
1315 is_disk_based: true,
1316 group_id: 2,
1317 ..Default::default()
1318 },
1319 }],
1320 cx,
1321 )
1322 .unwrap();
1323 });
1324
1325 // The first language server finishes
1326 project.update(cx, |project, cx| {
1327 project.disk_based_diagnostics_finished(server_id_1, cx);
1328 });
1329
1330 // Only the first language server's diagnostics are shown.
1331 cx.foreground().run_until_parked();
1332 view.update(cx, |view, cx| {
1333 assert_eq!(
1334 editor_blocks(&view.editor, cx),
1335 [
1336 (0, "path header block".into()),
1337 (2, "diagnostic header".into()),
1338 ]
1339 );
1340 assert_eq!(
1341 view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1342 concat!(
1343 "\n", // filename
1344 "\n", // padding
1345 // diagnostic group 1
1346 "\n", // primary message
1347 "\n", // padding
1348 "a();\n", //
1349 "b();",
1350 )
1351 );
1352 });
1353
1354 // The second language server finishes
1355 project.update(cx, |project, cx| {
1356 project.disk_based_diagnostics_finished(server_id_2, cx);
1357 });
1358
1359 // Both language server's diagnostics are shown.
1360 cx.foreground().run_until_parked();
1361 view.update(cx, |view, cx| {
1362 assert_eq!(
1363 editor_blocks(&view.editor, cx),
1364 [
1365 (0, "path header block".into()),
1366 (2, "diagnostic header".into()),
1367 (6, "collapsed context".into()),
1368 (7, "diagnostic header".into()),
1369 ]
1370 );
1371 assert_eq!(
1372 view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1373 concat!(
1374 "\n", // filename
1375 "\n", // padding
1376 // diagnostic group 1
1377 "\n", // primary message
1378 "\n", // padding
1379 "a();\n", // location
1380 "b();\n", //
1381 "\n", // collapsed context
1382 // diagnostic group 2
1383 "\n", // primary message
1384 "\n", // padding
1385 "a();\n", // context
1386 "b();\n", //
1387 "c();", // context
1388 )
1389 );
1390 });
1391
1392 // Both language servers start updating diagnostics, and the first server finishes.
1393 project.update(cx, |project, cx| {
1394 project.disk_based_diagnostics_started(server_id_1, cx);
1395 project.disk_based_diagnostics_started(server_id_2, cx);
1396 project
1397 .update_diagnostic_entries(
1398 server_id_1,
1399 PathBuf::from("/test/main.js"),
1400 None,
1401 vec![DiagnosticEntry {
1402 range: Unclipped(PointUtf16::new(2, 0))..Unclipped(PointUtf16::new(2, 1)),
1403 diagnostic: Diagnostic {
1404 message: "warning 2".to_string(),
1405 severity: DiagnosticSeverity::WARNING,
1406 is_primary: true,
1407 is_disk_based: true,
1408 group_id: 1,
1409 ..Default::default()
1410 },
1411 }],
1412 cx,
1413 )
1414 .unwrap();
1415 project
1416 .update_diagnostic_entries(
1417 server_id_2,
1418 PathBuf::from("/test/main.rs"),
1419 None,
1420 vec![],
1421 cx,
1422 )
1423 .unwrap();
1424 project.disk_based_diagnostics_finished(server_id_1, cx);
1425 });
1426
1427 // Only the first language server's diagnostics are updated.
1428 cx.foreground().run_until_parked();
1429 view.update(cx, |view, cx| {
1430 assert_eq!(
1431 editor_blocks(&view.editor, cx),
1432 [
1433 (0, "path header block".into()),
1434 (2, "diagnostic header".into()),
1435 (7, "collapsed context".into()),
1436 (8, "diagnostic header".into()),
1437 ]
1438 );
1439 assert_eq!(
1440 view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1441 concat!(
1442 "\n", // filename
1443 "\n", // padding
1444 // diagnostic group 1
1445 "\n", // primary message
1446 "\n", // padding
1447 "a();\n", // location
1448 "b();\n", //
1449 "c();\n", // context
1450 "\n", // collapsed context
1451 // diagnostic group 2
1452 "\n", // primary message
1453 "\n", // padding
1454 "b();\n", // context
1455 "c();\n", //
1456 "d();", // context
1457 )
1458 );
1459 });
1460
1461 // The second language server finishes.
1462 project.update(cx, |project, cx| {
1463 project
1464 .update_diagnostic_entries(
1465 server_id_2,
1466 PathBuf::from("/test/main.js"),
1467 None,
1468 vec![DiagnosticEntry {
1469 range: Unclipped(PointUtf16::new(3, 0))..Unclipped(PointUtf16::new(3, 1)),
1470 diagnostic: Diagnostic {
1471 message: "warning 2".to_string(),
1472 severity: DiagnosticSeverity::WARNING,
1473 is_primary: true,
1474 is_disk_based: true,
1475 group_id: 1,
1476 ..Default::default()
1477 },
1478 }],
1479 cx,
1480 )
1481 .unwrap();
1482 project.disk_based_diagnostics_finished(server_id_2, cx);
1483 });
1484
1485 // Both language servers' diagnostics are updated.
1486 cx.foreground().run_until_parked();
1487 view.update(cx, |view, cx| {
1488 assert_eq!(
1489 editor_blocks(&view.editor, cx),
1490 [
1491 (0, "path header block".into()),
1492 (2, "diagnostic header".into()),
1493 (7, "collapsed context".into()),
1494 (8, "diagnostic header".into()),
1495 ]
1496 );
1497 assert_eq!(
1498 view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1499 concat!(
1500 "\n", // filename
1501 "\n", // padding
1502 // diagnostic group 1
1503 "\n", // primary message
1504 "\n", // padding
1505 "b();\n", // location
1506 "c();\n", //
1507 "d();\n", // context
1508 "\n", // collapsed context
1509 // diagnostic group 2
1510 "\n", // primary message
1511 "\n", // padding
1512 "c();\n", // context
1513 "d();\n", //
1514 "e();", // context
1515 )
1516 );
1517 });
1518 }
1519
1520 fn init_test(cx: &mut TestAppContext) {
1521 cx.update(|cx| {
1522 cx.set_global(SettingsStore::test(cx));
1523 theme::init((), cx);
1524 language::init(cx);
1525 client::init_settings(cx);
1526 workspace::init_settings(cx);
1527 Project::init_settings(cx);
1528 crate::init(cx);
1529 });
1530 }
1531
1532 fn editor_blocks(editor: &ViewHandle<Editor>, cx: &mut WindowContext) -> Vec<(u32, String)> {
1533 editor.update(cx, |editor, cx| {
1534 let snapshot = editor.snapshot(cx);
1535 snapshot
1536 .blocks_in_range(0..snapshot.max_point().row())
1537 .enumerate()
1538 .filter_map(|(ix, (row, block))| {
1539 let name = match block {
1540 TransformBlock::Custom(block) => block
1541 .render(&mut BlockContext {
1542 view_context: cx,
1543 anchor_x: 0.,
1544 scroll_x: 0.,
1545 gutter_padding: 0.,
1546 gutter_width: 0.,
1547 line_height: 0.,
1548 em_width: 0.,
1549 block_id: ix,
1550 })
1551 .name()?
1552 .to_string(),
1553 TransformBlock::ExcerptHeader {
1554 starts_new_buffer, ..
1555 } => {
1556 if *starts_new_buffer {
1557 "path header block".to_string()
1558 } else {
1559 "collapsed context".to_string()
1560 }
1561 }
1562 };
1563
1564 Some((row, name))
1565 })
1566 .collect()
1567 })
1568 }
1569}