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