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, 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 Label::new("No problems in workspace", theme.empty_message.clone())
94 .aligned()
95 .contained()
96 .with_style(theme.container)
97 .into_any()
98 } else {
99 ChildView::new(&self.editor, cx).into_any()
100 }
101 }
102
103 fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
104 if cx.is_self_focused() && !self.path_states.is_empty() {
105 cx.focus(&self.editor);
106 }
107 }
108
109 fn debug_json(&self, cx: &AppContext) -> serde_json::Value {
110 let project = self.project.read(cx);
111 json!({
112 "project": json!({
113 "language_servers": project.language_server_statuses().collect::<Vec<_>>(),
114 "summary": project.diagnostic_summary(cx),
115 }),
116 "summary": self.summary,
117 "paths_to_update": self.paths_to_update.iter().map(|(path, server_id)|
118 (path.path.to_string_lossy(), server_id.0)
119 ).collect::<Vec<_>>(),
120 "paths_states": self.path_states.iter().map(|state|
121 json!({
122 "path": state.path.path.to_string_lossy(),
123 "groups": state.diagnostic_groups.iter().map(|group|
124 json!({
125 "block_count": group.blocks.len(),
126 "excerpt_count": group.excerpts.len(),
127 })
128 ).collect::<Vec<_>>(),
129 })
130 ).collect::<Vec<_>>(),
131 })
132 }
133}
134
135impl ProjectDiagnosticsEditor {
136 fn new(
137 project_handle: ModelHandle<Project>,
138 workspace: WeakViewHandle<Workspace>,
139 cx: &mut ViewContext<Self>,
140 ) -> Self {
141 cx.subscribe(&project_handle, |this, _, event, cx| match event {
142 project::Event::DiskBasedDiagnosticsFinished { language_server_id } => {
143 this.update_excerpts(Some(*language_server_id), cx);
144 this.update_title(cx);
145 }
146 project::Event::DiagnosticsUpdated {
147 language_server_id,
148 path,
149 } => {
150 this.paths_to_update
151 .insert((path.clone(), *language_server_id));
152 }
153 _ => {}
154 })
155 .detach();
156
157 let excerpts = cx.add_model(|cx| MultiBuffer::new(project_handle.read(cx).replica_id()));
158 let editor = cx.add_view(|cx| {
159 let mut editor =
160 Editor::for_multibuffer(excerpts.clone(), Some(project_handle.clone()), cx);
161 editor.set_vertical_scroll_margin(5, cx);
162 editor
163 });
164 cx.subscribe(&editor, |_, _, event, cx| cx.emit(event.clone()))
165 .detach();
166
167 let project = project_handle.read(cx);
168 let paths_to_update = project
169 .diagnostic_summaries(cx)
170 .map(|(path, server_id, _)| (path, server_id))
171 .collect();
172 let summary = project.diagnostic_summary(cx);
173 let mut this = Self {
174 project: project_handle,
175 summary,
176 workspace,
177 excerpts,
178 editor,
179 path_states: Default::default(),
180 paths_to_update,
181 };
182 this.update_excerpts(None, cx);
183 this
184 }
185
186 fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext<Workspace>) {
187 if let Some(existing) = workspace.item_of_type::<ProjectDiagnosticsEditor>(cx) {
188 workspace.activate_item(&existing, cx);
189 } else {
190 let workspace_handle = cx.weak_handle();
191 let diagnostics = cx.add_view(|cx| {
192 ProjectDiagnosticsEditor::new(workspace.project().clone(), workspace_handle, cx)
193 });
194 workspace.add_item(Box::new(diagnostics), cx);
195 }
196 }
197
198 fn update_excerpts(
199 &mut self,
200 language_server_id: Option<LanguageServerId>,
201 cx: &mut ViewContext<Self>,
202 ) {
203 let mut paths = Vec::new();
204 self.paths_to_update.retain(|(path, server_id)| {
205 if language_server_id
206 .map_or(true, |language_server_id| language_server_id == *server_id)
207 {
208 paths.push(path.clone());
209 false
210 } else {
211 true
212 }
213 });
214 let project = self.project.clone();
215 cx.spawn(|this, mut cx| {
216 async move {
217 for path in paths {
218 let buffer = project
219 .update(&mut cx, |project, cx| project.open_buffer(path.clone(), cx))
220 .await?;
221 this.update(&mut cx, |this, cx| {
222 this.populate_excerpts(path, language_server_id, buffer, cx)
223 })?;
224 }
225 Result::<_, anyhow::Error>::Ok(())
226 }
227 .log_err()
228 })
229 .detach();
230 }
231
232 fn populate_excerpts(
233 &mut self,
234 path: ProjectPath,
235 language_server_id: Option<LanguageServerId>,
236 buffer: ModelHandle<Buffer>,
237 cx: &mut ViewContext<Self>,
238 ) {
239 let was_empty = self.path_states.is_empty();
240 let snapshot = buffer.read(cx).snapshot();
241 let path_ix = match self.path_states.binary_search_by_key(&&path, |e| &e.path) {
242 Ok(ix) => ix,
243 Err(ix) => {
244 self.path_states.insert(
245 ix,
246 PathState {
247 path: path.clone(),
248 diagnostic_groups: Default::default(),
249 },
250 );
251 ix
252 }
253 };
254
255 let mut prev_excerpt_id = if path_ix > 0 {
256 let prev_path_last_group = &self.path_states[path_ix - 1]
257 .diagnostic_groups
258 .last()
259 .unwrap();
260 prev_path_last_group.excerpts.last().unwrap().clone()
261 } else {
262 ExcerptId::min()
263 };
264
265 let path_state = &mut self.path_states[path_ix];
266 let mut groups_to_add = Vec::new();
267 let mut group_ixs_to_remove = Vec::new();
268 let mut blocks_to_add = Vec::new();
269 let mut blocks_to_remove = HashSet::default();
270 let mut first_excerpt_id = None;
271 let excerpts_snapshot = self.excerpts.update(cx, |excerpts, excerpts_cx| {
272 let mut old_groups = path_state.diagnostic_groups.iter().enumerate().peekable();
273 let mut new_groups = snapshot
274 .diagnostic_groups(language_server_id)
275 .into_iter()
276 .filter(|(_, group)| {
277 group.entries[group.primary_ix].diagnostic.severity
278 <= DiagnosticSeverity::WARNING
279 })
280 .peekable();
281 loop {
282 let mut to_insert = None;
283 let mut to_remove = None;
284 let mut to_keep = None;
285 match (old_groups.peek(), new_groups.peek()) {
286 (None, None) => break,
287 (None, Some(_)) => to_insert = new_groups.next(),
288 (Some((_, old_group)), None) => {
289 if language_server_id.map_or(true, |id| id == old_group.language_server_id)
290 {
291 to_remove = old_groups.next();
292 } else {
293 to_keep = old_groups.next();
294 }
295 }
296 (Some((_, old_group)), Some((_, new_group))) => {
297 let old_primary = &old_group.primary_diagnostic;
298 let new_primary = &new_group.entries[new_group.primary_ix];
299 match compare_diagnostics(old_primary, new_primary, &snapshot) {
300 Ordering::Less => {
301 if language_server_id
302 .map_or(true, |id| id == old_group.language_server_id)
303 {
304 to_remove = old_groups.next();
305 } else {
306 to_keep = old_groups.next();
307 }
308 }
309 Ordering::Equal => {
310 to_keep = old_groups.next();
311 new_groups.next();
312 }
313 Ordering::Greater => to_insert = new_groups.next(),
314 }
315 }
316 }
317
318 if let Some((language_server_id, group)) = to_insert {
319 let mut group_state = DiagnosticGroupState {
320 language_server_id,
321 primary_diagnostic: group.entries[group.primary_ix].clone(),
322 primary_excerpt_ix: 0,
323 excerpts: Default::default(),
324 blocks: Default::default(),
325 block_count: 0,
326 };
327 let mut pending_range: Option<(Range<Point>, usize)> = None;
328 let mut is_first_excerpt_for_group = true;
329 for (ix, entry) in group.entries.iter().map(Some).chain([None]).enumerate() {
330 let resolved_entry = entry.map(|e| e.resolve::<Point>(&snapshot));
331 if let Some((range, start_ix)) = &mut pending_range {
332 if let Some(entry) = resolved_entry.as_ref() {
333 if entry.range.start.row
334 <= range.end.row + 1 + CONTEXT_LINE_COUNT * 2
335 {
336 range.end = range.end.max(entry.range.end);
337 continue;
338 }
339 }
340
341 let excerpt_start =
342 Point::new(range.start.row.saturating_sub(CONTEXT_LINE_COUNT), 0);
343 let excerpt_end = snapshot.clip_point(
344 Point::new(range.end.row + CONTEXT_LINE_COUNT, u32::MAX),
345 Bias::Left,
346 );
347 let excerpt_id = excerpts
348 .insert_excerpts_after(
349 prev_excerpt_id,
350 buffer.clone(),
351 [ExcerptRange {
352 context: excerpt_start..excerpt_end,
353 primary: Some(range.clone()),
354 }],
355 excerpts_cx,
356 )
357 .pop()
358 .unwrap();
359
360 prev_excerpt_id = excerpt_id.clone();
361 first_excerpt_id.get_or_insert_with(|| prev_excerpt_id.clone());
362 group_state.excerpts.push(excerpt_id.clone());
363 let header_position = (excerpt_id.clone(), language::Anchor::MIN);
364
365 if is_first_excerpt_for_group {
366 is_first_excerpt_for_group = false;
367 let mut primary =
368 group.entries[group.primary_ix].diagnostic.clone();
369 primary.message =
370 primary.message.split('\n').next().unwrap().to_string();
371 group_state.block_count += 1;
372 blocks_to_add.push(BlockProperties {
373 position: header_position,
374 height: 2,
375 style: BlockStyle::Sticky,
376 render: diagnostic_header_renderer(primary),
377 disposition: BlockDisposition::Above,
378 });
379 }
380
381 for entry in &group.entries[*start_ix..ix] {
382 let mut diagnostic = entry.diagnostic.clone();
383 if diagnostic.is_primary {
384 group_state.primary_excerpt_ix = group_state.excerpts.len() - 1;
385 diagnostic.message =
386 entry.diagnostic.message.split('\n').skip(1).collect();
387 }
388
389 if !diagnostic.message.is_empty() {
390 group_state.block_count += 1;
391 blocks_to_add.push(BlockProperties {
392 position: (excerpt_id.clone(), entry.range.start),
393 height: diagnostic.message.matches('\n').count() as u8 + 1,
394 style: BlockStyle::Fixed,
395 render: diagnostic_block_renderer(diagnostic, true),
396 disposition: BlockDisposition::Below,
397 });
398 }
399 }
400
401 pending_range.take();
402 }
403
404 if let Some(entry) = resolved_entry {
405 pending_range = Some((entry.range.clone(), ix));
406 }
407 }
408
409 groups_to_add.push(group_state);
410 } else if let Some((group_ix, group_state)) = to_remove {
411 excerpts.remove_excerpts(group_state.excerpts.iter().copied(), excerpts_cx);
412 group_ixs_to_remove.push(group_ix);
413 blocks_to_remove.extend(group_state.blocks.iter().copied());
414 } else if let Some((_, group)) = to_keep {
415 prev_excerpt_id = group.excerpts.last().unwrap().clone();
416 first_excerpt_id.get_or_insert_with(|| prev_excerpt_id.clone());
417 }
418 }
419
420 excerpts.snapshot(excerpts_cx)
421 });
422
423 self.editor.update(cx, |editor, cx| {
424 editor.remove_blocks(blocks_to_remove, cx);
425 let block_ids = editor.insert_blocks(
426 blocks_to_add.into_iter().map(|block| {
427 let (excerpt_id, text_anchor) = block.position;
428 BlockProperties {
429 position: excerpts_snapshot.anchor_in_excerpt(excerpt_id, text_anchor),
430 height: block.height,
431 style: block.style,
432 render: block.render,
433 disposition: block.disposition,
434 }
435 }),
436 cx,
437 );
438
439 let mut block_ids = block_ids.into_iter();
440 for group_state in &mut groups_to_add {
441 group_state.blocks = block_ids.by_ref().take(group_state.block_count).collect();
442 }
443 });
444
445 for ix in group_ixs_to_remove.into_iter().rev() {
446 path_state.diagnostic_groups.remove(ix);
447 }
448 path_state.diagnostic_groups.extend(groups_to_add);
449 path_state.diagnostic_groups.sort_unstable_by(|a, b| {
450 let range_a = &a.primary_diagnostic.range;
451 let range_b = &b.primary_diagnostic.range;
452 range_a
453 .start
454 .cmp(&range_b.start, &snapshot)
455 .then_with(|| range_a.end.cmp(&range_b.end, &snapshot))
456 });
457
458 if path_state.diagnostic_groups.is_empty() {
459 self.path_states.remove(path_ix);
460 }
461
462 self.editor.update(cx, |editor, cx| {
463 let groups;
464 let mut selections;
465 let new_excerpt_ids_by_selection_id;
466 if was_empty {
467 groups = self.path_states.first()?.diagnostic_groups.as_slice();
468 new_excerpt_ids_by_selection_id = [(0, ExcerptId::min())].into_iter().collect();
469 selections = vec![Selection {
470 id: 0,
471 start: 0,
472 end: 0,
473 reversed: false,
474 goal: SelectionGoal::None,
475 }];
476 } else {
477 groups = self.path_states.get(path_ix)?.diagnostic_groups.as_slice();
478 new_excerpt_ids_by_selection_id =
479 editor.change_selections(Some(Autoscroll::fit()), cx, |s| s.refresh());
480 selections = editor.selections.all::<usize>(cx);
481 }
482
483 // If any selection has lost its position, move it to start of the next primary diagnostic.
484 let snapshot = editor.snapshot(cx);
485 for selection in &mut selections {
486 if let Some(new_excerpt_id) = new_excerpt_ids_by_selection_id.get(&selection.id) {
487 let group_ix = match groups.binary_search_by(|probe| {
488 probe
489 .excerpts
490 .last()
491 .unwrap()
492 .cmp(new_excerpt_id, &snapshot.buffer_snapshot)
493 }) {
494 Ok(ix) | Err(ix) => ix,
495 };
496 if let Some(group) = groups.get(group_ix) {
497 let offset = excerpts_snapshot
498 .anchor_in_excerpt(
499 group.excerpts[group.primary_excerpt_ix].clone(),
500 group.primary_diagnostic.range.start,
501 )
502 .to_offset(&excerpts_snapshot);
503 selection.start = offset;
504 selection.end = offset;
505 }
506 }
507 }
508 editor.change_selections(None, cx, |s| {
509 s.select(selections);
510 });
511 Some(())
512 });
513
514 if self.path_states.is_empty() {
515 if self.editor.is_focused(cx) {
516 cx.focus_self();
517 }
518 } else if cx.handle().is_focused(cx) {
519 cx.focus(&self.editor);
520 }
521 cx.notify();
522 }
523
524 fn update_title(&mut self, cx: &mut ViewContext<Self>) {
525 self.summary = self.project.read(cx).diagnostic_summary(cx);
526 cx.emit(Event::TitleChanged);
527 }
528}
529
530impl Item for ProjectDiagnosticsEditor {
531 fn tab_content<T: View>(
532 &self,
533 _detail: Option<usize>,
534 style: &theme::Tab,
535 cx: &AppContext,
536 ) -> AnyElement<T> {
537 render_summary(
538 &self.summary,
539 &style.label.text,
540 &theme::current(cx).project_diagnostics,
541 )
542 }
543
544 fn for_each_project_item(&self, cx: &AppContext, f: &mut dyn FnMut(usize, &dyn project::Item)) {
545 self.editor.for_each_project_item(cx, f)
546 }
547
548 fn is_singleton(&self, _: &AppContext) -> bool {
549 false
550 }
551
552 fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext<Self>) {
553 self.editor
554 .update(cx, |editor, cx| editor.added_to_workspace(workspace, cx));
555 }
556
557 fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> bool {
558 self.editor
559 .update(cx, |editor, cx| editor.navigate(data, cx))
560 }
561
562 fn tab_tooltip_text(&self, _: &AppContext) -> Option<Cow<str>> {
563 Some("Project Diagnostics".into())
564 }
565
566 fn is_dirty(&self, cx: &AppContext) -> bool {
567 self.excerpts.read(cx).is_dirty(cx)
568 }
569
570 fn has_conflict(&self, cx: &AppContext) -> bool {
571 self.excerpts.read(cx).has_conflict(cx)
572 }
573
574 fn can_save(&self, _: &AppContext) -> bool {
575 true
576 }
577
578 fn save(
579 &mut self,
580 project: ModelHandle<Project>,
581 cx: &mut ViewContext<Self>,
582 ) -> Task<Result<()>> {
583 self.editor.save(project, cx)
584 }
585
586 fn reload(
587 &mut self,
588 project: ModelHandle<Project>,
589 cx: &mut ViewContext<Self>,
590 ) -> Task<Result<()>> {
591 self.editor.reload(project, cx)
592 }
593
594 fn save_as(
595 &mut self,
596 _: ModelHandle<Project>,
597 _: PathBuf,
598 _: &mut ViewContext<Self>,
599 ) -> Task<Result<()>> {
600 unreachable!()
601 }
602
603 fn git_diff_recalc(
604 &mut self,
605 project: ModelHandle<Project>,
606 cx: &mut ViewContext<Self>,
607 ) -> Task<Result<()>> {
608 self.editor
609 .update(cx, |editor, cx| editor.git_diff_recalc(project, cx))
610 }
611
612 fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> {
613 Editor::to_item_events(event)
614 }
615
616 fn set_nav_history(&mut self, nav_history: ItemNavHistory, cx: &mut ViewContext<Self>) {
617 self.editor.update(cx, |editor, _| {
618 editor.set_nav_history(Some(nav_history));
619 });
620 }
621
622 fn clone_on_split(
623 &self,
624 _workspace_id: workspace::WorkspaceId,
625 cx: &mut ViewContext<Self>,
626 ) -> Option<Self>
627 where
628 Self: Sized,
629 {
630 Some(ProjectDiagnosticsEditor::new(
631 self.project.clone(),
632 self.workspace.clone(),
633 cx,
634 ))
635 }
636
637 fn act_as_type<'a>(
638 &'a self,
639 type_id: TypeId,
640 self_handle: &'a ViewHandle<Self>,
641 _: &'a AppContext,
642 ) -> Option<&AnyViewHandle> {
643 if type_id == TypeId::of::<Self>() {
644 Some(self_handle)
645 } else if type_id == TypeId::of::<Editor>() {
646 Some(&self.editor)
647 } else {
648 None
649 }
650 }
651
652 fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
653 self.editor.update(cx, |editor, cx| editor.deactivated(cx));
654 }
655
656 fn serialized_item_kind() -> Option<&'static str> {
657 Some("diagnostics")
658 }
659
660 fn breadcrumbs(&self, theme: &theme::Theme, cx: &AppContext) -> Option<Vec<BreadcrumbText>> {
661 self.editor.breadcrumbs(theme, cx)
662 }
663
664 fn breadcrumb_location(&self) -> ToolbarItemLocation {
665 ToolbarItemLocation::PrimaryLeft { flex: None }
666 }
667
668 fn deserialize(
669 project: ModelHandle<Project>,
670 workspace: WeakViewHandle<Workspace>,
671 _workspace_id: workspace::WorkspaceId,
672 _item_id: workspace::ItemId,
673 cx: &mut ViewContext<Pane>,
674 ) -> Task<Result<ViewHandle<Self>>> {
675 Task::ready(Ok(cx.add_view(|cx| Self::new(project, workspace, cx))))
676 }
677}
678
679fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
680 let (message, highlights) = highlight_diagnostic_message(Vec::new(), &diagnostic.message);
681 Arc::new(move |cx| {
682 let settings = 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 });
1503 }
1504
1505 fn editor_blocks(editor: &ViewHandle<Editor>, cx: &mut WindowContext) -> Vec<(u32, String)> {
1506 editor.update(cx, |editor, cx| {
1507 let snapshot = editor.snapshot(cx);
1508 snapshot
1509 .blocks_in_range(0..snapshot.max_point().row())
1510 .filter_map(|(row, block)| {
1511 let name = match block {
1512 TransformBlock::Custom(block) => block
1513 .render(&mut BlockContext {
1514 view_context: cx,
1515 anchor_x: 0.,
1516 scroll_x: 0.,
1517 gutter_padding: 0.,
1518 gutter_width: 0.,
1519 line_height: 0.,
1520 em_width: 0.,
1521 })
1522 .name()?
1523 .to_string(),
1524 TransformBlock::ExcerptHeader {
1525 starts_new_buffer, ..
1526 } => {
1527 if *starts_new_buffer {
1528 "path header block".to_string()
1529 } else {
1530 "collapsed context".to_string()
1531 }
1532 }
1533 };
1534
1535 Some((row, name))
1536 })
1537 .collect()
1538 })
1539 }
1540}