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