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, 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 cx,
446 );
447
448 let mut block_ids = block_ids.into_iter();
449 for group_state in &mut groups_to_add {
450 group_state.blocks = block_ids.by_ref().take(group_state.block_count).collect();
451 }
452 });
453
454 for ix in group_ixs_to_remove.into_iter().rev() {
455 path_state.diagnostic_groups.remove(ix);
456 }
457 path_state.diagnostic_groups.extend(groups_to_add);
458 path_state.diagnostic_groups.sort_unstable_by(|a, b| {
459 let range_a = &a.primary_diagnostic.range;
460 let range_b = &b.primary_diagnostic.range;
461 range_a
462 .start
463 .cmp(&range_b.start, &snapshot)
464 .then_with(|| range_a.end.cmp(&range_b.end, &snapshot))
465 });
466
467 if path_state.diagnostic_groups.is_empty() {
468 self.path_states.remove(path_ix);
469 }
470
471 self.editor.update(cx, |editor, cx| {
472 let groups;
473 let mut selections;
474 let new_excerpt_ids_by_selection_id;
475 if was_empty {
476 groups = self.path_states.first()?.diagnostic_groups.as_slice();
477 new_excerpt_ids_by_selection_id = [(0, ExcerptId::min())].into_iter().collect();
478 selections = vec![Selection {
479 id: 0,
480 start: 0,
481 end: 0,
482 reversed: false,
483 goal: SelectionGoal::None,
484 }];
485 } else {
486 groups = self.path_states.get(path_ix)?.diagnostic_groups.as_slice();
487 new_excerpt_ids_by_selection_id =
488 editor.change_selections(Some(Autoscroll::fit()), cx, |s| s.refresh());
489 selections = editor.selections.all::<usize>(cx);
490 }
491
492 // If any selection has lost its position, move it to start of the next primary diagnostic.
493 let snapshot = editor.snapshot(cx);
494 for selection in &mut selections {
495 if let Some(new_excerpt_id) = new_excerpt_ids_by_selection_id.get(&selection.id) {
496 let group_ix = match groups.binary_search_by(|probe| {
497 probe
498 .excerpts
499 .last()
500 .unwrap()
501 .cmp(new_excerpt_id, &snapshot.buffer_snapshot)
502 }) {
503 Ok(ix) | Err(ix) => ix,
504 };
505 if let Some(group) = groups.get(group_ix) {
506 let offset = excerpts_snapshot
507 .anchor_in_excerpt(
508 group.excerpts[group.primary_excerpt_ix].clone(),
509 group.primary_diagnostic.range.start,
510 )
511 .to_offset(&excerpts_snapshot);
512 selection.start = offset;
513 selection.end = offset;
514 }
515 }
516 }
517 editor.change_selections(None, cx, |s| {
518 s.select(selections);
519 });
520 Some(())
521 });
522
523 if self.path_states.is_empty() {
524 if self.editor.is_focused(cx) {
525 cx.focus_self();
526 }
527 } else if cx.handle().is_focused(cx) {
528 cx.focus(&self.editor);
529 }
530 cx.notify();
531 }
532
533 fn update_title(&mut self, cx: &mut ViewContext<Self>) {
534 self.summary = self.project.read(cx).diagnostic_summary(cx);
535 cx.emit(Event::TitleChanged);
536 }
537}
538
539impl Item for ProjectDiagnosticsEditor {
540 fn tab_content<T: View>(
541 &self,
542 _detail: Option<usize>,
543 style: &theme::Tab,
544 cx: &AppContext,
545 ) -> AnyElement<T> {
546 render_summary(
547 &self.summary,
548 &style.label.text,
549 &theme::current(cx).project_diagnostics,
550 )
551 }
552
553 fn for_each_project_item(&self, cx: &AppContext, f: &mut dyn FnMut(usize, &dyn project::Item)) {
554 self.editor.for_each_project_item(cx, f)
555 }
556
557 fn is_singleton(&self, _: &AppContext) -> bool {
558 false
559 }
560
561 fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext<Self>) {
562 self.editor
563 .update(cx, |editor, cx| editor.added_to_workspace(workspace, cx));
564 }
565
566 fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> bool {
567 self.editor
568 .update(cx, |editor, cx| editor.navigate(data, cx))
569 }
570
571 fn tab_tooltip_text(&self, _: &AppContext) -> Option<Cow<str>> {
572 Some("Project Diagnostics".into())
573 }
574
575 fn is_dirty(&self, cx: &AppContext) -> bool {
576 self.excerpts.read(cx).is_dirty(cx)
577 }
578
579 fn has_conflict(&self, cx: &AppContext) -> bool {
580 self.excerpts.read(cx).has_conflict(cx)
581 }
582
583 fn can_save(&self, _: &AppContext) -> bool {
584 true
585 }
586
587 fn save(
588 &mut self,
589 project: ModelHandle<Project>,
590 cx: &mut ViewContext<Self>,
591 ) -> Task<Result<()>> {
592 self.editor.save(project, cx)
593 }
594
595 fn reload(
596 &mut self,
597 project: ModelHandle<Project>,
598 cx: &mut ViewContext<Self>,
599 ) -> Task<Result<()>> {
600 self.editor.reload(project, cx)
601 }
602
603 fn save_as(
604 &mut self,
605 _: ModelHandle<Project>,
606 _: PathBuf,
607 _: &mut ViewContext<Self>,
608 ) -> Task<Result<()>> {
609 unreachable!()
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 = settings::get::<ThemeSettings>(cx);
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(cx)).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 settings::SettingsStore;
822 use unindent::Unindent as _;
823
824 #[gpui::test]
825 async fn test_diagnostics(cx: &mut TestAppContext) {
826 init_test(cx);
827
828 let fs = FakeFs::new(cx.background());
829 fs.insert_tree(
830 "/test",
831 json!({
832 "consts.rs": "
833 const a: i32 = 'a';
834 const b: i32 = c;
835 "
836 .unindent(),
837
838 "main.rs": "
839 fn main() {
840 let x = vec![];
841 let y = vec![];
842 a(x);
843 b(y);
844 // comment 1
845 // comment 2
846 c(y);
847 d(x);
848 }
849 "
850 .unindent(),
851 }),
852 )
853 .await;
854
855 let language_server_id = LanguageServerId(0);
856 let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
857 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), 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 = cx.add_view(window_id, |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_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
1251
1252 let view = cx.add_view(window_id, |cx| {
1253 ProjectDiagnosticsEditor::new(project.clone(), workspace.downgrade(), cx)
1254 });
1255
1256 // Two language servers start updating diagnostics
1257 project.update(cx, |project, cx| {
1258 project.disk_based_diagnostics_started(server_id_1, cx);
1259 project.disk_based_diagnostics_started(server_id_2, cx);
1260 project
1261 .update_diagnostic_entries(
1262 server_id_1,
1263 PathBuf::from("/test/main.js"),
1264 None,
1265 vec![DiagnosticEntry {
1266 range: Unclipped(PointUtf16::new(0, 0))..Unclipped(PointUtf16::new(0, 1)),
1267 diagnostic: Diagnostic {
1268 message: "error 1".to_string(),
1269 severity: DiagnosticSeverity::WARNING,
1270 is_primary: true,
1271 is_disk_based: true,
1272 group_id: 1,
1273 ..Default::default()
1274 },
1275 }],
1276 cx,
1277 )
1278 .unwrap();
1279 project
1280 .update_diagnostic_entries(
1281 server_id_2,
1282 PathBuf::from("/test/main.js"),
1283 None,
1284 vec![DiagnosticEntry {
1285 range: Unclipped(PointUtf16::new(1, 0))..Unclipped(PointUtf16::new(1, 1)),
1286 diagnostic: Diagnostic {
1287 message: "warning 1".to_string(),
1288 severity: DiagnosticSeverity::ERROR,
1289 is_primary: true,
1290 is_disk_based: true,
1291 group_id: 2,
1292 ..Default::default()
1293 },
1294 }],
1295 cx,
1296 )
1297 .unwrap();
1298 });
1299
1300 // The first language server finishes
1301 project.update(cx, |project, cx| {
1302 project.disk_based_diagnostics_finished(server_id_1, cx);
1303 });
1304
1305 // Only the first language server's diagnostics are shown.
1306 cx.foreground().run_until_parked();
1307 view.update(cx, |view, cx| {
1308 assert_eq!(
1309 editor_blocks(&view.editor, cx),
1310 [
1311 (0, "path header block".into()),
1312 (2, "diagnostic header".into()),
1313 ]
1314 );
1315 assert_eq!(
1316 view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1317 concat!(
1318 "\n", // filename
1319 "\n", // padding
1320 // diagnostic group 1
1321 "\n", // primary message
1322 "\n", // padding
1323 "a();\n", //
1324 "b();",
1325 )
1326 );
1327 });
1328
1329 // The second language server finishes
1330 project.update(cx, |project, cx| {
1331 project.disk_based_diagnostics_finished(server_id_2, cx);
1332 });
1333
1334 // Both language server's diagnostics are shown.
1335 cx.foreground().run_until_parked();
1336 view.update(cx, |view, cx| {
1337 assert_eq!(
1338 editor_blocks(&view.editor, cx),
1339 [
1340 (0, "path header block".into()),
1341 (2, "diagnostic header".into()),
1342 (6, "collapsed context".into()),
1343 (7, "diagnostic header".into()),
1344 ]
1345 );
1346 assert_eq!(
1347 view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1348 concat!(
1349 "\n", // filename
1350 "\n", // padding
1351 // diagnostic group 1
1352 "\n", // primary message
1353 "\n", // padding
1354 "a();\n", // location
1355 "b();\n", //
1356 "\n", // collapsed context
1357 // diagnostic group 2
1358 "\n", // primary message
1359 "\n", // padding
1360 "a();\n", // context
1361 "b();\n", //
1362 "c();", // context
1363 )
1364 );
1365 });
1366
1367 // Both language servers start updating diagnostics, and the first server finishes.
1368 project.update(cx, |project, cx| {
1369 project.disk_based_diagnostics_started(server_id_1, cx);
1370 project.disk_based_diagnostics_started(server_id_2, cx);
1371 project
1372 .update_diagnostic_entries(
1373 server_id_1,
1374 PathBuf::from("/test/main.js"),
1375 None,
1376 vec![DiagnosticEntry {
1377 range: Unclipped(PointUtf16::new(2, 0))..Unclipped(PointUtf16::new(2, 1)),
1378 diagnostic: Diagnostic {
1379 message: "warning 2".to_string(),
1380 severity: DiagnosticSeverity::WARNING,
1381 is_primary: true,
1382 is_disk_based: true,
1383 group_id: 1,
1384 ..Default::default()
1385 },
1386 }],
1387 cx,
1388 )
1389 .unwrap();
1390 project
1391 .update_diagnostic_entries(
1392 server_id_2,
1393 PathBuf::from("/test/main.rs"),
1394 None,
1395 vec![],
1396 cx,
1397 )
1398 .unwrap();
1399 project.disk_based_diagnostics_finished(server_id_1, cx);
1400 });
1401
1402 // Only the first language server's diagnostics are updated.
1403 cx.foreground().run_until_parked();
1404 view.update(cx, |view, cx| {
1405 assert_eq!(
1406 editor_blocks(&view.editor, cx),
1407 [
1408 (0, "path header block".into()),
1409 (2, "diagnostic header".into()),
1410 (7, "collapsed context".into()),
1411 (8, "diagnostic header".into()),
1412 ]
1413 );
1414 assert_eq!(
1415 view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1416 concat!(
1417 "\n", // filename
1418 "\n", // padding
1419 // diagnostic group 1
1420 "\n", // primary message
1421 "\n", // padding
1422 "a();\n", // location
1423 "b();\n", //
1424 "c();\n", // context
1425 "\n", // collapsed context
1426 // diagnostic group 2
1427 "\n", // primary message
1428 "\n", // padding
1429 "b();\n", // context
1430 "c();\n", //
1431 "d();", // context
1432 )
1433 );
1434 });
1435
1436 // The second language server finishes.
1437 project.update(cx, |project, cx| {
1438 project
1439 .update_diagnostic_entries(
1440 server_id_2,
1441 PathBuf::from("/test/main.js"),
1442 None,
1443 vec![DiagnosticEntry {
1444 range: Unclipped(PointUtf16::new(3, 0))..Unclipped(PointUtf16::new(3, 1)),
1445 diagnostic: Diagnostic {
1446 message: "warning 2".to_string(),
1447 severity: DiagnosticSeverity::WARNING,
1448 is_primary: true,
1449 is_disk_based: true,
1450 group_id: 1,
1451 ..Default::default()
1452 },
1453 }],
1454 cx,
1455 )
1456 .unwrap();
1457 project.disk_based_diagnostics_finished(server_id_2, cx);
1458 });
1459
1460 // Both language servers' diagnostics are updated.
1461 cx.foreground().run_until_parked();
1462 view.update(cx, |view, cx| {
1463 assert_eq!(
1464 editor_blocks(&view.editor, cx),
1465 [
1466 (0, "path header block".into()),
1467 (2, "diagnostic header".into()),
1468 (7, "collapsed context".into()),
1469 (8, "diagnostic header".into()),
1470 ]
1471 );
1472 assert_eq!(
1473 view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1474 concat!(
1475 "\n", // filename
1476 "\n", // padding
1477 // diagnostic group 1
1478 "\n", // primary message
1479 "\n", // padding
1480 "b();\n", // location
1481 "c();\n", //
1482 "d();\n", // context
1483 "\n", // collapsed context
1484 // diagnostic group 2
1485 "\n", // primary message
1486 "\n", // padding
1487 "c();\n", // context
1488 "d();\n", //
1489 "e();", // context
1490 )
1491 );
1492 });
1493 }
1494
1495 fn init_test(cx: &mut TestAppContext) {
1496 cx.update(|cx| {
1497 cx.set_global(SettingsStore::test(cx));
1498 theme::init((), cx);
1499 language::init(cx);
1500 client::init_settings(cx);
1501 workspace::init_settings(cx);
1502 Project::init_settings(cx);
1503 });
1504 }
1505
1506 fn editor_blocks(editor: &ViewHandle<Editor>, cx: &mut WindowContext) -> Vec<(u32, String)> {
1507 editor.update(cx, |editor, cx| {
1508 let snapshot = editor.snapshot(cx);
1509 snapshot
1510 .blocks_in_range(0..snapshot.max_point().row())
1511 .filter_map(|(row, block)| {
1512 let name = match block {
1513 TransformBlock::Custom(block) => block
1514 .render(&mut BlockContext {
1515 view_context: cx,
1516 anchor_x: 0.,
1517 scroll_x: 0.,
1518 gutter_padding: 0.,
1519 gutter_width: 0.,
1520 line_height: 0.,
1521 em_width: 0.,
1522 })
1523 .name()?
1524 .to_string(),
1525 TransformBlock::ExcerptHeader {
1526 starts_new_buffer, ..
1527 } => {
1528 if *starts_new_buffer {
1529 "path header block".to_string()
1530 } else {
1531 "collapsed context".to_string()
1532 }
1533 }
1534 };
1535
1536 Some((row, name))
1537 })
1538 .collect()
1539 })
1540 }
1541}