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: View>(
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: View>(
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_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
859
860 // Create some diagnostics
861 project.update(cx, |project, cx| {
862 project
863 .update_diagnostic_entries(
864 language_server_id,
865 PathBuf::from("/test/main.rs"),
866 None,
867 vec![
868 DiagnosticEntry {
869 range: Unclipped(PointUtf16::new(1, 8))..Unclipped(PointUtf16::new(1, 9)),
870 diagnostic: Diagnostic {
871 message:
872 "move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait"
873 .to_string(),
874 severity: DiagnosticSeverity::INFORMATION,
875 is_primary: false,
876 is_disk_based: true,
877 group_id: 1,
878 ..Default::default()
879 },
880 },
881 DiagnosticEntry {
882 range: Unclipped(PointUtf16::new(2, 8))..Unclipped(PointUtf16::new(2, 9)),
883 diagnostic: Diagnostic {
884 message:
885 "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait"
886 .to_string(),
887 severity: DiagnosticSeverity::INFORMATION,
888 is_primary: false,
889 is_disk_based: true,
890 group_id: 0,
891 ..Default::default()
892 },
893 },
894 DiagnosticEntry {
895 range: Unclipped(PointUtf16::new(3, 6))..Unclipped(PointUtf16::new(3, 7)),
896 diagnostic: Diagnostic {
897 message: "value moved here".to_string(),
898 severity: DiagnosticSeverity::INFORMATION,
899 is_primary: false,
900 is_disk_based: true,
901 group_id: 1,
902 ..Default::default()
903 },
904 },
905 DiagnosticEntry {
906 range: Unclipped(PointUtf16::new(4, 6))..Unclipped(PointUtf16::new(4, 7)),
907 diagnostic: Diagnostic {
908 message: "value moved here".to_string(),
909 severity: DiagnosticSeverity::INFORMATION,
910 is_primary: false,
911 is_disk_based: true,
912 group_id: 0,
913 ..Default::default()
914 },
915 },
916 DiagnosticEntry {
917 range: Unclipped(PointUtf16::new(7, 6))..Unclipped(PointUtf16::new(7, 7)),
918 diagnostic: Diagnostic {
919 message: "use of moved value\nvalue used here after move".to_string(),
920 severity: DiagnosticSeverity::ERROR,
921 is_primary: true,
922 is_disk_based: true,
923 group_id: 0,
924 ..Default::default()
925 },
926 },
927 DiagnosticEntry {
928 range: Unclipped(PointUtf16::new(8, 6))..Unclipped(PointUtf16::new(8, 7)),
929 diagnostic: Diagnostic {
930 message: "use of moved value\nvalue used here after move".to_string(),
931 severity: DiagnosticSeverity::ERROR,
932 is_primary: true,
933 is_disk_based: true,
934 group_id: 1,
935 ..Default::default()
936 },
937 },
938 ],
939 cx,
940 )
941 .unwrap();
942 });
943
944 // Open the project diagnostics view while there are already diagnostics.
945 let view = cx.add_view(window_id, |cx| {
946 ProjectDiagnosticsEditor::new(project.clone(), workspace.downgrade(), cx)
947 });
948
949 view.next_notification(cx).await;
950 view.update(cx, |view, cx| {
951 assert_eq!(
952 editor_blocks(&view.editor, cx),
953 [
954 (0, "path header block".into()),
955 (2, "diagnostic header".into()),
956 (15, "collapsed context".into()),
957 (16, "diagnostic header".into()),
958 (25, "collapsed context".into()),
959 ]
960 );
961 assert_eq!(
962 view.editor.update(cx, |editor, cx| editor.display_text(cx)),
963 concat!(
964 //
965 // main.rs
966 //
967 "\n", // filename
968 "\n", // padding
969 // diagnostic group 1
970 "\n", // primary message
971 "\n", // padding
972 " let x = vec![];\n",
973 " let y = vec![];\n",
974 "\n", // supporting diagnostic
975 " a(x);\n",
976 " b(y);\n",
977 "\n", // supporting diagnostic
978 " // comment 1\n",
979 " // comment 2\n",
980 " c(y);\n",
981 "\n", // supporting diagnostic
982 " d(x);\n",
983 "\n", // context ellipsis
984 // diagnostic group 2
985 "\n", // primary message
986 "\n", // padding
987 "fn main() {\n",
988 " let x = vec![];\n",
989 "\n", // supporting diagnostic
990 " let y = vec![];\n",
991 " a(x);\n",
992 "\n", // supporting diagnostic
993 " b(y);\n",
994 "\n", // context ellipsis
995 " c(y);\n",
996 " d(x);\n",
997 "\n", // supporting diagnostic
998 "}"
999 )
1000 );
1001
1002 // Cursor is at the first diagnostic
1003 view.editor.update(cx, |editor, cx| {
1004 assert_eq!(
1005 editor.selections.display_ranges(cx),
1006 [DisplayPoint::new(12, 6)..DisplayPoint::new(12, 6)]
1007 );
1008 });
1009 });
1010
1011 // Diagnostics are added for another earlier path.
1012 project.update(cx, |project, cx| {
1013 project.disk_based_diagnostics_started(language_server_id, cx);
1014 project
1015 .update_diagnostic_entries(
1016 language_server_id,
1017 PathBuf::from("/test/consts.rs"),
1018 None,
1019 vec![DiagnosticEntry {
1020 range: Unclipped(PointUtf16::new(0, 15))..Unclipped(PointUtf16::new(0, 15)),
1021 diagnostic: Diagnostic {
1022 message: "mismatched types\nexpected `usize`, found `char`".to_string(),
1023 severity: DiagnosticSeverity::ERROR,
1024 is_primary: true,
1025 is_disk_based: true,
1026 group_id: 0,
1027 ..Default::default()
1028 },
1029 }],
1030 cx,
1031 )
1032 .unwrap();
1033 project.disk_based_diagnostics_finished(language_server_id, cx);
1034 });
1035
1036 view.next_notification(cx).await;
1037 view.update(cx, |view, cx| {
1038 assert_eq!(
1039 editor_blocks(&view.editor, cx),
1040 [
1041 (0, "path header block".into()),
1042 (2, "diagnostic header".into()),
1043 (7, "path header block".into()),
1044 (9, "diagnostic header".into()),
1045 (22, "collapsed context".into()),
1046 (23, "diagnostic header".into()),
1047 (32, "collapsed context".into()),
1048 ]
1049 );
1050 assert_eq!(
1051 view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1052 concat!(
1053 //
1054 // consts.rs
1055 //
1056 "\n", // filename
1057 "\n", // padding
1058 // diagnostic group 1
1059 "\n", // primary message
1060 "\n", // padding
1061 "const a: i32 = 'a';\n",
1062 "\n", // supporting diagnostic
1063 "const b: i32 = c;\n",
1064 //
1065 // main.rs
1066 //
1067 "\n", // filename
1068 "\n", // padding
1069 // diagnostic group 1
1070 "\n", // primary message
1071 "\n", // padding
1072 " let x = vec![];\n",
1073 " let y = vec![];\n",
1074 "\n", // supporting diagnostic
1075 " a(x);\n",
1076 " b(y);\n",
1077 "\n", // supporting diagnostic
1078 " // comment 1\n",
1079 " // comment 2\n",
1080 " c(y);\n",
1081 "\n", // supporting diagnostic
1082 " d(x);\n",
1083 "\n", // collapsed context
1084 // diagnostic group 2
1085 "\n", // primary message
1086 "\n", // filename
1087 "fn main() {\n",
1088 " let x = vec![];\n",
1089 "\n", // supporting diagnostic
1090 " let y = vec![];\n",
1091 " a(x);\n",
1092 "\n", // supporting diagnostic
1093 " b(y);\n",
1094 "\n", // context ellipsis
1095 " c(y);\n",
1096 " d(x);\n",
1097 "\n", // supporting diagnostic
1098 "}"
1099 )
1100 );
1101
1102 // Cursor keeps its position.
1103 view.editor.update(cx, |editor, cx| {
1104 assert_eq!(
1105 editor.selections.display_ranges(cx),
1106 [DisplayPoint::new(19, 6)..DisplayPoint::new(19, 6)]
1107 );
1108 });
1109 });
1110
1111 // Diagnostics are added to the first path
1112 project.update(cx, |project, cx| {
1113 project.disk_based_diagnostics_started(language_server_id, cx);
1114 project
1115 .update_diagnostic_entries(
1116 language_server_id,
1117 PathBuf::from("/test/consts.rs"),
1118 None,
1119 vec![
1120 DiagnosticEntry {
1121 range: Unclipped(PointUtf16::new(0, 15))
1122 ..Unclipped(PointUtf16::new(0, 15)),
1123 diagnostic: Diagnostic {
1124 message: "mismatched types\nexpected `usize`, found `char`"
1125 .to_string(),
1126 severity: DiagnosticSeverity::ERROR,
1127 is_primary: true,
1128 is_disk_based: true,
1129 group_id: 0,
1130 ..Default::default()
1131 },
1132 },
1133 DiagnosticEntry {
1134 range: Unclipped(PointUtf16::new(1, 15))
1135 ..Unclipped(PointUtf16::new(1, 15)),
1136 diagnostic: Diagnostic {
1137 message: "unresolved name `c`".to_string(),
1138 severity: DiagnosticSeverity::ERROR,
1139 is_primary: true,
1140 is_disk_based: true,
1141 group_id: 1,
1142 ..Default::default()
1143 },
1144 },
1145 ],
1146 cx,
1147 )
1148 .unwrap();
1149 project.disk_based_diagnostics_finished(language_server_id, cx);
1150 });
1151
1152 view.next_notification(cx).await;
1153 view.update(cx, |view, cx| {
1154 assert_eq!(
1155 editor_blocks(&view.editor, cx),
1156 [
1157 (0, "path header block".into()),
1158 (2, "diagnostic header".into()),
1159 (7, "collapsed context".into()),
1160 (8, "diagnostic header".into()),
1161 (13, "path header block".into()),
1162 (15, "diagnostic header".into()),
1163 (28, "collapsed context".into()),
1164 (29, "diagnostic header".into()),
1165 (38, "collapsed context".into()),
1166 ]
1167 );
1168 assert_eq!(
1169 view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1170 concat!(
1171 //
1172 // consts.rs
1173 //
1174 "\n", // filename
1175 "\n", // padding
1176 // diagnostic group 1
1177 "\n", // primary message
1178 "\n", // padding
1179 "const a: i32 = 'a';\n",
1180 "\n", // supporting diagnostic
1181 "const b: i32 = c;\n",
1182 "\n", // context ellipsis
1183 // diagnostic group 2
1184 "\n", // primary message
1185 "\n", // padding
1186 "const a: i32 = 'a';\n",
1187 "const b: i32 = c;\n",
1188 "\n", // supporting diagnostic
1189 //
1190 // main.rs
1191 //
1192 "\n", // filename
1193 "\n", // padding
1194 // diagnostic group 1
1195 "\n", // primary message
1196 "\n", // padding
1197 " let x = vec![];\n",
1198 " let y = vec![];\n",
1199 "\n", // supporting diagnostic
1200 " a(x);\n",
1201 " b(y);\n",
1202 "\n", // supporting diagnostic
1203 " // comment 1\n",
1204 " // comment 2\n",
1205 " c(y);\n",
1206 "\n", // supporting diagnostic
1207 " d(x);\n",
1208 "\n", // context ellipsis
1209 // diagnostic group 2
1210 "\n", // primary message
1211 "\n", // filename
1212 "fn main() {\n",
1213 " let x = vec![];\n",
1214 "\n", // supporting diagnostic
1215 " let y = vec![];\n",
1216 " a(x);\n",
1217 "\n", // supporting diagnostic
1218 " b(y);\n",
1219 "\n", // context ellipsis
1220 " c(y);\n",
1221 " d(x);\n",
1222 "\n", // supporting diagnostic
1223 "}"
1224 )
1225 );
1226 });
1227 }
1228
1229 #[gpui::test]
1230 async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
1231 init_test(cx);
1232
1233 let fs = FakeFs::new(cx.background());
1234 fs.insert_tree(
1235 "/test",
1236 json!({
1237 "main.js": "
1238 a();
1239 b();
1240 c();
1241 d();
1242 e();
1243 ".unindent()
1244 }),
1245 )
1246 .await;
1247
1248 let server_id_1 = LanguageServerId(100);
1249 let server_id_2 = LanguageServerId(101);
1250 let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
1251 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
1252
1253 let view = cx.add_view(window_id, |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 .filter_map(|(row, block)| {
1513 let name = match block {
1514 TransformBlock::Custom(block) => block
1515 .render(&mut BlockContext {
1516 view_context: cx,
1517 anchor_x: 0.,
1518 scroll_x: 0.,
1519 gutter_padding: 0.,
1520 gutter_width: 0.,
1521 line_height: 0.,
1522 em_width: 0.,
1523 })
1524 .name()?
1525 .to_string(),
1526 TransformBlock::ExcerptHeader {
1527 starts_new_buffer, ..
1528 } => {
1529 if *starts_new_buffer {
1530 "path header block".to_string()
1531 } else {
1532 "collapsed context".to_string()
1533 }
1534 }
1535 };
1536
1537 Some((row, name))
1538 })
1539 .collect()
1540 })
1541 }
1542}