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/circle_x_mark_12.svg")
690 .with_color(theme.error_diagnostic.message.text.color)
691 } else {
692 Svg::new("icons/triangle_exclamation_12.svg")
693 .with_color(theme.warning_diagnostic.message.text.color)
694 };
695
696 Flex::row()
697 .with_child(
698 icon.constrained()
699 .with_width(icon_width)
700 .aligned()
701 .contained()
702 .with_margin_right(cx.gutter_padding),
703 )
704 .with_children(diagnostic.source.as_ref().map(|source| {
705 Label::new(
706 format!("{source}: "),
707 style.source.label.clone().with_font_size(font_size),
708 )
709 .contained()
710 .with_style(style.message.container)
711 .aligned()
712 }))
713 .with_child(
714 Label::new(
715 message.clone(),
716 style.message.label.clone().with_font_size(font_size),
717 )
718 .with_highlights(highlights.clone())
719 .contained()
720 .with_style(style.message.container)
721 .aligned(),
722 )
723 .with_children(diagnostic.code.clone().map(|code| {
724 Label::new(code, style.code.text.clone().with_font_size(font_size))
725 .contained()
726 .with_style(style.code.container)
727 .aligned()
728 }))
729 .contained()
730 .with_style(style.container)
731 .with_padding_left(cx.gutter_padding)
732 .with_padding_right(cx.gutter_padding)
733 .expanded()
734 .into_any_named("diagnostic header")
735 })
736}
737
738pub(crate) fn render_summary<T: 'static>(
739 summary: &DiagnosticSummary,
740 text_style: &TextStyle,
741 theme: &theme::ProjectDiagnostics,
742) -> AnyElement<T> {
743 if summary.error_count == 0 && summary.warning_count == 0 {
744 Label::new("No problems", text_style.clone()).into_any()
745 } else {
746 let icon_width = theme.tab_icon_width;
747 let icon_spacing = theme.tab_icon_spacing;
748 let summary_spacing = theme.tab_summary_spacing;
749 Flex::row()
750 .with_child(
751 Svg::new("icons/circle_x_mark_12.svg")
752 .with_color(text_style.color)
753 .constrained()
754 .with_width(icon_width)
755 .aligned()
756 .contained()
757 .with_margin_right(icon_spacing),
758 )
759 .with_child(
760 Label::new(
761 summary.error_count.to_string(),
762 LabelStyle {
763 text: text_style.clone(),
764 highlight_text: None,
765 },
766 )
767 .aligned(),
768 )
769 .with_child(
770 Svg::new("icons/triangle_exclamation_12.svg")
771 .with_color(text_style.color)
772 .constrained()
773 .with_width(icon_width)
774 .aligned()
775 .contained()
776 .with_margin_left(summary_spacing)
777 .with_margin_right(icon_spacing),
778 )
779 .with_child(
780 Label::new(
781 summary.warning_count.to_string(),
782 LabelStyle {
783 text: text_style.clone(),
784 highlight_text: None,
785 },
786 )
787 .aligned(),
788 )
789 .into_any()
790 }
791}
792
793fn compare_diagnostics<L: language::ToOffset, R: language::ToOffset>(
794 lhs: &DiagnosticEntry<L>,
795 rhs: &DiagnosticEntry<R>,
796 snapshot: &language::BufferSnapshot,
797) -> Ordering {
798 lhs.range
799 .start
800 .to_offset(snapshot)
801 .cmp(&rhs.range.start.to_offset(snapshot))
802 .then_with(|| {
803 lhs.range
804 .end
805 .to_offset(snapshot)
806 .cmp(&rhs.range.end.to_offset(snapshot))
807 })
808 .then_with(|| lhs.diagnostic.message.cmp(&rhs.diagnostic.message))
809}
810
811#[cfg(test)]
812mod tests {
813 use super::*;
814 use editor::{
815 display_map::{BlockContext, TransformBlock},
816 DisplayPoint,
817 };
818 use gpui::{TestAppContext, WindowContext};
819 use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16, Unclipped};
820 use project::FakeFs;
821 use serde_json::json;
822 use settings::SettingsStore;
823 use unindent::Unindent as _;
824
825 #[gpui::test]
826 async fn test_diagnostics(cx: &mut TestAppContext) {
827 init_test(cx);
828
829 let fs = FakeFs::new(cx.background());
830 fs.insert_tree(
831 "/test",
832 json!({
833 "consts.rs": "
834 const a: i32 = 'a';
835 const b: i32 = c;
836 "
837 .unindent(),
838
839 "main.rs": "
840 fn main() {
841 let x = vec![];
842 let y = vec![];
843 a(x);
844 b(y);
845 // comment 1
846 // comment 2
847 c(y);
848 d(x);
849 }
850 "
851 .unindent(),
852 }),
853 )
854 .await;
855
856 let language_server_id = LanguageServerId(0);
857 let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
858 let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
859 let workspace = window.root(cx);
860
861 // Create some diagnostics
862 project.update(cx, |project, cx| {
863 project
864 .update_diagnostic_entries(
865 language_server_id,
866 PathBuf::from("/test/main.rs"),
867 None,
868 vec![
869 DiagnosticEntry {
870 range: Unclipped(PointUtf16::new(1, 8))..Unclipped(PointUtf16::new(1, 9)),
871 diagnostic: Diagnostic {
872 message:
873 "move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait"
874 .to_string(),
875 severity: DiagnosticSeverity::INFORMATION,
876 is_primary: false,
877 is_disk_based: true,
878 group_id: 1,
879 ..Default::default()
880 },
881 },
882 DiagnosticEntry {
883 range: Unclipped(PointUtf16::new(2, 8))..Unclipped(PointUtf16::new(2, 9)),
884 diagnostic: Diagnostic {
885 message:
886 "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait"
887 .to_string(),
888 severity: DiagnosticSeverity::INFORMATION,
889 is_primary: false,
890 is_disk_based: true,
891 group_id: 0,
892 ..Default::default()
893 },
894 },
895 DiagnosticEntry {
896 range: Unclipped(PointUtf16::new(3, 6))..Unclipped(PointUtf16::new(3, 7)),
897 diagnostic: Diagnostic {
898 message: "value moved here".to_string(),
899 severity: DiagnosticSeverity::INFORMATION,
900 is_primary: false,
901 is_disk_based: true,
902 group_id: 1,
903 ..Default::default()
904 },
905 },
906 DiagnosticEntry {
907 range: Unclipped(PointUtf16::new(4, 6))..Unclipped(PointUtf16::new(4, 7)),
908 diagnostic: Diagnostic {
909 message: "value moved here".to_string(),
910 severity: DiagnosticSeverity::INFORMATION,
911 is_primary: false,
912 is_disk_based: true,
913 group_id: 0,
914 ..Default::default()
915 },
916 },
917 DiagnosticEntry {
918 range: Unclipped(PointUtf16::new(7, 6))..Unclipped(PointUtf16::new(7, 7)),
919 diagnostic: Diagnostic {
920 message: "use of moved value\nvalue used here after move".to_string(),
921 severity: DiagnosticSeverity::ERROR,
922 is_primary: true,
923 is_disk_based: true,
924 group_id: 0,
925 ..Default::default()
926 },
927 },
928 DiagnosticEntry {
929 range: Unclipped(PointUtf16::new(8, 6))..Unclipped(PointUtf16::new(8, 7)),
930 diagnostic: Diagnostic {
931 message: "use of moved value\nvalue used here after move".to_string(),
932 severity: DiagnosticSeverity::ERROR,
933 is_primary: true,
934 is_disk_based: true,
935 group_id: 1,
936 ..Default::default()
937 },
938 },
939 ],
940 cx,
941 )
942 .unwrap();
943 });
944
945 // Open the project diagnostics view while there are already diagnostics.
946 let view = window.add_view(cx, |cx| {
947 ProjectDiagnosticsEditor::new(project.clone(), workspace.downgrade(), cx)
948 });
949
950 view.next_notification(cx).await;
951 view.update(cx, |view, cx| {
952 assert_eq!(
953 editor_blocks(&view.editor, cx),
954 [
955 (0, "path header block".into()),
956 (2, "diagnostic header".into()),
957 (15, "collapsed context".into()),
958 (16, "diagnostic header".into()),
959 (25, "collapsed context".into()),
960 ]
961 );
962 assert_eq!(
963 view.editor.update(cx, |editor, cx| editor.display_text(cx)),
964 concat!(
965 //
966 // main.rs
967 //
968 "\n", // filename
969 "\n", // padding
970 // diagnostic group 1
971 "\n", // primary message
972 "\n", // padding
973 " let x = vec![];\n",
974 " let y = vec![];\n",
975 "\n", // supporting diagnostic
976 " a(x);\n",
977 " b(y);\n",
978 "\n", // supporting diagnostic
979 " // comment 1\n",
980 " // comment 2\n",
981 " c(y);\n",
982 "\n", // supporting diagnostic
983 " d(x);\n",
984 "\n", // context ellipsis
985 // diagnostic group 2
986 "\n", // primary message
987 "\n", // padding
988 "fn main() {\n",
989 " let x = vec![];\n",
990 "\n", // supporting diagnostic
991 " let y = vec![];\n",
992 " a(x);\n",
993 "\n", // supporting diagnostic
994 " b(y);\n",
995 "\n", // context ellipsis
996 " c(y);\n",
997 " d(x);\n",
998 "\n", // supporting diagnostic
999 "}"
1000 )
1001 );
1002
1003 // Cursor is at the first diagnostic
1004 view.editor.update(cx, |editor, cx| {
1005 assert_eq!(
1006 editor.selections.display_ranges(cx),
1007 [DisplayPoint::new(12, 6)..DisplayPoint::new(12, 6)]
1008 );
1009 });
1010 });
1011
1012 // Diagnostics are added for another earlier path.
1013 project.update(cx, |project, cx| {
1014 project.disk_based_diagnostics_started(language_server_id, cx);
1015 project
1016 .update_diagnostic_entries(
1017 language_server_id,
1018 PathBuf::from("/test/consts.rs"),
1019 None,
1020 vec![DiagnosticEntry {
1021 range: Unclipped(PointUtf16::new(0, 15))..Unclipped(PointUtf16::new(0, 15)),
1022 diagnostic: Diagnostic {
1023 message: "mismatched types\nexpected `usize`, found `char`".to_string(),
1024 severity: DiagnosticSeverity::ERROR,
1025 is_primary: true,
1026 is_disk_based: true,
1027 group_id: 0,
1028 ..Default::default()
1029 },
1030 }],
1031 cx,
1032 )
1033 .unwrap();
1034 project.disk_based_diagnostics_finished(language_server_id, cx);
1035 });
1036
1037 view.next_notification(cx).await;
1038 view.update(cx, |view, cx| {
1039 assert_eq!(
1040 editor_blocks(&view.editor, cx),
1041 [
1042 (0, "path header block".into()),
1043 (2, "diagnostic header".into()),
1044 (7, "path header block".into()),
1045 (9, "diagnostic header".into()),
1046 (22, "collapsed context".into()),
1047 (23, "diagnostic header".into()),
1048 (32, "collapsed context".into()),
1049 ]
1050 );
1051 assert_eq!(
1052 view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1053 concat!(
1054 //
1055 // consts.rs
1056 //
1057 "\n", // filename
1058 "\n", // padding
1059 // diagnostic group 1
1060 "\n", // primary message
1061 "\n", // padding
1062 "const a: i32 = 'a';\n",
1063 "\n", // supporting diagnostic
1064 "const b: i32 = c;\n",
1065 //
1066 // main.rs
1067 //
1068 "\n", // filename
1069 "\n", // padding
1070 // diagnostic group 1
1071 "\n", // primary message
1072 "\n", // padding
1073 " let x = vec![];\n",
1074 " let y = vec![];\n",
1075 "\n", // supporting diagnostic
1076 " a(x);\n",
1077 " b(y);\n",
1078 "\n", // supporting diagnostic
1079 " // comment 1\n",
1080 " // comment 2\n",
1081 " c(y);\n",
1082 "\n", // supporting diagnostic
1083 " d(x);\n",
1084 "\n", // collapsed context
1085 // diagnostic group 2
1086 "\n", // primary message
1087 "\n", // filename
1088 "fn main() {\n",
1089 " let x = vec![];\n",
1090 "\n", // supporting diagnostic
1091 " let y = vec![];\n",
1092 " a(x);\n",
1093 "\n", // supporting diagnostic
1094 " b(y);\n",
1095 "\n", // context ellipsis
1096 " c(y);\n",
1097 " d(x);\n",
1098 "\n", // supporting diagnostic
1099 "}"
1100 )
1101 );
1102
1103 // Cursor keeps its position.
1104 view.editor.update(cx, |editor, cx| {
1105 assert_eq!(
1106 editor.selections.display_ranges(cx),
1107 [DisplayPoint::new(19, 6)..DisplayPoint::new(19, 6)]
1108 );
1109 });
1110 });
1111
1112 // Diagnostics are added to the first path
1113 project.update(cx, |project, cx| {
1114 project.disk_based_diagnostics_started(language_server_id, cx);
1115 project
1116 .update_diagnostic_entries(
1117 language_server_id,
1118 PathBuf::from("/test/consts.rs"),
1119 None,
1120 vec![
1121 DiagnosticEntry {
1122 range: Unclipped(PointUtf16::new(0, 15))
1123 ..Unclipped(PointUtf16::new(0, 15)),
1124 diagnostic: Diagnostic {
1125 message: "mismatched types\nexpected `usize`, found `char`"
1126 .to_string(),
1127 severity: DiagnosticSeverity::ERROR,
1128 is_primary: true,
1129 is_disk_based: true,
1130 group_id: 0,
1131 ..Default::default()
1132 },
1133 },
1134 DiagnosticEntry {
1135 range: Unclipped(PointUtf16::new(1, 15))
1136 ..Unclipped(PointUtf16::new(1, 15)),
1137 diagnostic: Diagnostic {
1138 message: "unresolved name `c`".to_string(),
1139 severity: DiagnosticSeverity::ERROR,
1140 is_primary: true,
1141 is_disk_based: true,
1142 group_id: 1,
1143 ..Default::default()
1144 },
1145 },
1146 ],
1147 cx,
1148 )
1149 .unwrap();
1150 project.disk_based_diagnostics_finished(language_server_id, cx);
1151 });
1152
1153 view.next_notification(cx).await;
1154 view.update(cx, |view, cx| {
1155 assert_eq!(
1156 editor_blocks(&view.editor, cx),
1157 [
1158 (0, "path header block".into()),
1159 (2, "diagnostic header".into()),
1160 (7, "collapsed context".into()),
1161 (8, "diagnostic header".into()),
1162 (13, "path header block".into()),
1163 (15, "diagnostic header".into()),
1164 (28, "collapsed context".into()),
1165 (29, "diagnostic header".into()),
1166 (38, "collapsed context".into()),
1167 ]
1168 );
1169 assert_eq!(
1170 view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1171 concat!(
1172 //
1173 // consts.rs
1174 //
1175 "\n", // filename
1176 "\n", // padding
1177 // diagnostic group 1
1178 "\n", // primary message
1179 "\n", // padding
1180 "const a: i32 = 'a';\n",
1181 "\n", // supporting diagnostic
1182 "const b: i32 = c;\n",
1183 "\n", // context ellipsis
1184 // diagnostic group 2
1185 "\n", // primary message
1186 "\n", // padding
1187 "const a: i32 = 'a';\n",
1188 "const b: i32 = c;\n",
1189 "\n", // supporting diagnostic
1190 //
1191 // main.rs
1192 //
1193 "\n", // filename
1194 "\n", // padding
1195 // diagnostic group 1
1196 "\n", // primary message
1197 "\n", // padding
1198 " let x = vec![];\n",
1199 " let y = vec![];\n",
1200 "\n", // supporting diagnostic
1201 " a(x);\n",
1202 " b(y);\n",
1203 "\n", // supporting diagnostic
1204 " // comment 1\n",
1205 " // comment 2\n",
1206 " c(y);\n",
1207 "\n", // supporting diagnostic
1208 " d(x);\n",
1209 "\n", // context ellipsis
1210 // diagnostic group 2
1211 "\n", // primary message
1212 "\n", // filename
1213 "fn main() {\n",
1214 " let x = vec![];\n",
1215 "\n", // supporting diagnostic
1216 " let y = vec![];\n",
1217 " a(x);\n",
1218 "\n", // supporting diagnostic
1219 " b(y);\n",
1220 "\n", // context ellipsis
1221 " c(y);\n",
1222 " d(x);\n",
1223 "\n", // supporting diagnostic
1224 "}"
1225 )
1226 );
1227 });
1228 }
1229
1230 #[gpui::test]
1231 async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
1232 init_test(cx);
1233
1234 let fs = FakeFs::new(cx.background());
1235 fs.insert_tree(
1236 "/test",
1237 json!({
1238 "main.js": "
1239 a();
1240 b();
1241 c();
1242 d();
1243 e();
1244 ".unindent()
1245 }),
1246 )
1247 .await;
1248
1249 let server_id_1 = LanguageServerId(100);
1250 let server_id_2 = LanguageServerId(101);
1251 let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
1252 let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
1253 let workspace = window.root(cx);
1254
1255 let view = window.add_view(cx, |cx| {
1256 ProjectDiagnosticsEditor::new(project.clone(), workspace.downgrade(), cx)
1257 });
1258
1259 // Two language servers start updating diagnostics
1260 project.update(cx, |project, cx| {
1261 project.disk_based_diagnostics_started(server_id_1, cx);
1262 project.disk_based_diagnostics_started(server_id_2, cx);
1263 project
1264 .update_diagnostic_entries(
1265 server_id_1,
1266 PathBuf::from("/test/main.js"),
1267 None,
1268 vec![DiagnosticEntry {
1269 range: Unclipped(PointUtf16::new(0, 0))..Unclipped(PointUtf16::new(0, 1)),
1270 diagnostic: Diagnostic {
1271 message: "error 1".to_string(),
1272 severity: DiagnosticSeverity::WARNING,
1273 is_primary: true,
1274 is_disk_based: true,
1275 group_id: 1,
1276 ..Default::default()
1277 },
1278 }],
1279 cx,
1280 )
1281 .unwrap();
1282 project
1283 .update_diagnostic_entries(
1284 server_id_2,
1285 PathBuf::from("/test/main.js"),
1286 None,
1287 vec![DiagnosticEntry {
1288 range: Unclipped(PointUtf16::new(1, 0))..Unclipped(PointUtf16::new(1, 1)),
1289 diagnostic: Diagnostic {
1290 message: "warning 1".to_string(),
1291 severity: DiagnosticSeverity::ERROR,
1292 is_primary: true,
1293 is_disk_based: true,
1294 group_id: 2,
1295 ..Default::default()
1296 },
1297 }],
1298 cx,
1299 )
1300 .unwrap();
1301 });
1302
1303 // The first language server finishes
1304 project.update(cx, |project, cx| {
1305 project.disk_based_diagnostics_finished(server_id_1, cx);
1306 });
1307
1308 // Only the first language server's diagnostics are shown.
1309 cx.foreground().run_until_parked();
1310 view.update(cx, |view, cx| {
1311 assert_eq!(
1312 editor_blocks(&view.editor, cx),
1313 [
1314 (0, "path header block".into()),
1315 (2, "diagnostic header".into()),
1316 ]
1317 );
1318 assert_eq!(
1319 view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1320 concat!(
1321 "\n", // filename
1322 "\n", // padding
1323 // diagnostic group 1
1324 "\n", // primary message
1325 "\n", // padding
1326 "a();\n", //
1327 "b();",
1328 )
1329 );
1330 });
1331
1332 // The second language server finishes
1333 project.update(cx, |project, cx| {
1334 project.disk_based_diagnostics_finished(server_id_2, cx);
1335 });
1336
1337 // Both language server's diagnostics are shown.
1338 cx.foreground().run_until_parked();
1339 view.update(cx, |view, cx| {
1340 assert_eq!(
1341 editor_blocks(&view.editor, cx),
1342 [
1343 (0, "path header block".into()),
1344 (2, "diagnostic header".into()),
1345 (6, "collapsed context".into()),
1346 (7, "diagnostic header".into()),
1347 ]
1348 );
1349 assert_eq!(
1350 view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1351 concat!(
1352 "\n", // filename
1353 "\n", // padding
1354 // diagnostic group 1
1355 "\n", // primary message
1356 "\n", // padding
1357 "a();\n", // location
1358 "b();\n", //
1359 "\n", // collapsed context
1360 // diagnostic group 2
1361 "\n", // primary message
1362 "\n", // padding
1363 "a();\n", // context
1364 "b();\n", //
1365 "c();", // context
1366 )
1367 );
1368 });
1369
1370 // Both language servers start updating diagnostics, and the first server finishes.
1371 project.update(cx, |project, cx| {
1372 project.disk_based_diagnostics_started(server_id_1, cx);
1373 project.disk_based_diagnostics_started(server_id_2, cx);
1374 project
1375 .update_diagnostic_entries(
1376 server_id_1,
1377 PathBuf::from("/test/main.js"),
1378 None,
1379 vec![DiagnosticEntry {
1380 range: Unclipped(PointUtf16::new(2, 0))..Unclipped(PointUtf16::new(2, 1)),
1381 diagnostic: Diagnostic {
1382 message: "warning 2".to_string(),
1383 severity: DiagnosticSeverity::WARNING,
1384 is_primary: true,
1385 is_disk_based: true,
1386 group_id: 1,
1387 ..Default::default()
1388 },
1389 }],
1390 cx,
1391 )
1392 .unwrap();
1393 project
1394 .update_diagnostic_entries(
1395 server_id_2,
1396 PathBuf::from("/test/main.rs"),
1397 None,
1398 vec![],
1399 cx,
1400 )
1401 .unwrap();
1402 project.disk_based_diagnostics_finished(server_id_1, cx);
1403 });
1404
1405 // Only the first language server's diagnostics are updated.
1406 cx.foreground().run_until_parked();
1407 view.update(cx, |view, cx| {
1408 assert_eq!(
1409 editor_blocks(&view.editor, cx),
1410 [
1411 (0, "path header block".into()),
1412 (2, "diagnostic header".into()),
1413 (7, "collapsed context".into()),
1414 (8, "diagnostic header".into()),
1415 ]
1416 );
1417 assert_eq!(
1418 view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1419 concat!(
1420 "\n", // filename
1421 "\n", // padding
1422 // diagnostic group 1
1423 "\n", // primary message
1424 "\n", // padding
1425 "a();\n", // location
1426 "b();\n", //
1427 "c();\n", // context
1428 "\n", // collapsed context
1429 // diagnostic group 2
1430 "\n", // primary message
1431 "\n", // padding
1432 "b();\n", // context
1433 "c();\n", //
1434 "d();", // context
1435 )
1436 );
1437 });
1438
1439 // The second language server finishes.
1440 project.update(cx, |project, cx| {
1441 project
1442 .update_diagnostic_entries(
1443 server_id_2,
1444 PathBuf::from("/test/main.js"),
1445 None,
1446 vec![DiagnosticEntry {
1447 range: Unclipped(PointUtf16::new(3, 0))..Unclipped(PointUtf16::new(3, 1)),
1448 diagnostic: Diagnostic {
1449 message: "warning 2".to_string(),
1450 severity: DiagnosticSeverity::WARNING,
1451 is_primary: true,
1452 is_disk_based: true,
1453 group_id: 1,
1454 ..Default::default()
1455 },
1456 }],
1457 cx,
1458 )
1459 .unwrap();
1460 project.disk_based_diagnostics_finished(server_id_2, cx);
1461 });
1462
1463 // Both language servers' diagnostics are updated.
1464 cx.foreground().run_until_parked();
1465 view.update(cx, |view, cx| {
1466 assert_eq!(
1467 editor_blocks(&view.editor, cx),
1468 [
1469 (0, "path header block".into()),
1470 (2, "diagnostic header".into()),
1471 (7, "collapsed context".into()),
1472 (8, "diagnostic header".into()),
1473 ]
1474 );
1475 assert_eq!(
1476 view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1477 concat!(
1478 "\n", // filename
1479 "\n", // padding
1480 // diagnostic group 1
1481 "\n", // primary message
1482 "\n", // padding
1483 "b();\n", // location
1484 "c();\n", //
1485 "d();\n", // context
1486 "\n", // collapsed context
1487 // diagnostic group 2
1488 "\n", // primary message
1489 "\n", // padding
1490 "c();\n", // context
1491 "d();\n", //
1492 "e();", // context
1493 )
1494 );
1495 });
1496 }
1497
1498 fn init_test(cx: &mut TestAppContext) {
1499 cx.update(|cx| {
1500 cx.set_global(SettingsStore::test(cx));
1501 theme::init((), cx);
1502 language::init(cx);
1503 client::init_settings(cx);
1504 workspace::init_settings(cx);
1505 Project::init_settings(cx);
1506 });
1507 }
1508
1509 fn editor_blocks(editor: &ViewHandle<Editor>, cx: &mut WindowContext) -> Vec<(u32, String)> {
1510 editor.update(cx, |editor, cx| {
1511 let snapshot = editor.snapshot(cx);
1512 snapshot
1513 .blocks_in_range(0..snapshot.max_point().row())
1514 .enumerate()
1515 .filter_map(|(ix, (row, block))| {
1516 let name = match block {
1517 TransformBlock::Custom(block) => block
1518 .render(&mut BlockContext {
1519 view_context: cx,
1520 anchor_x: 0.,
1521 scroll_x: 0.,
1522 gutter_padding: 0.,
1523 gutter_width: 0.,
1524 line_height: 0.,
1525 em_width: 0.,
1526 block_id: ix,
1527 })
1528 .name()?
1529 .to_string(),
1530 TransformBlock::ExcerptHeader {
1531 starts_new_buffer, ..
1532 } => {
1533 if *starts_new_buffer {
1534 "path header block".to_string()
1535 } else {
1536 "collapsed context".to_string()
1537 }
1538 }
1539 };
1540
1541 Some((row, name))
1542 })
1543 .collect()
1544 })
1545 }
1546}