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 settings::Settings;
24use smallvec::SmallVec;
25use std::{
26 any::{Any, TypeId},
27 borrow::Cow,
28 cmp::Ordering,
29 ops::Range,
30 path::PathBuf,
31 sync::Arc,
32};
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 = &cx.global::<Settings>().theme.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 &cx.global::<Settings>().theme.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 = cx.global::<Settings>();
683 let theme = &settings.theme.editor;
684 let style = theme.diagnostic_header.clone();
685 let font_size = (style.text_scale_factor
686 * settings::font_size_for_setting(settings.buffer_font_size, cx))
687 .round();
688 let icon_width = cx.em_width * style.icon_width_factor;
689 let icon = if diagnostic.severity == DiagnosticSeverity::ERROR {
690 Svg::new("icons/circle_x_mark_12.svg")
691 .with_color(theme.error_diagnostic.message.text.color)
692 } else {
693 Svg::new("icons/triangle_exclamation_12.svg")
694 .with_color(theme.warning_diagnostic.message.text.color)
695 };
696
697 Flex::row()
698 .with_child(
699 icon.constrained()
700 .with_width(icon_width)
701 .aligned()
702 .contained()
703 .with_margin_right(cx.gutter_padding),
704 )
705 .with_children(diagnostic.source.as_ref().map(|source| {
706 Label::new(
707 format!("{source}: "),
708 style.source.label.clone().with_font_size(font_size),
709 )
710 .contained()
711 .with_style(style.message.container)
712 .aligned()
713 }))
714 .with_child(
715 Label::new(
716 message.clone(),
717 style.message.label.clone().with_font_size(font_size),
718 )
719 .with_highlights(highlights.clone())
720 .contained()
721 .with_style(style.message.container)
722 .aligned(),
723 )
724 .with_children(diagnostic.code.clone().map(|code| {
725 Label::new(code, style.code.text.clone().with_font_size(font_size))
726 .contained()
727 .with_style(style.code.container)
728 .aligned()
729 }))
730 .contained()
731 .with_style(style.container)
732 .with_padding_left(cx.gutter_padding)
733 .with_padding_right(cx.gutter_padding)
734 .expanded()
735 .into_any_named("diagnostic header")
736 })
737}
738
739pub(crate) fn render_summary<T: View>(
740 summary: &DiagnosticSummary,
741 text_style: &TextStyle,
742 theme: &theme::ProjectDiagnostics,
743) -> AnyElement<T> {
744 if summary.error_count == 0 && summary.warning_count == 0 {
745 Label::new("No problems", text_style.clone()).into_any()
746 } else {
747 let icon_width = theme.tab_icon_width;
748 let icon_spacing = theme.tab_icon_spacing;
749 let summary_spacing = theme.tab_summary_spacing;
750 Flex::row()
751 .with_child(
752 Svg::new("icons/circle_x_mark_12.svg")
753 .with_color(text_style.color)
754 .constrained()
755 .with_width(icon_width)
756 .aligned()
757 .contained()
758 .with_margin_right(icon_spacing),
759 )
760 .with_child(
761 Label::new(
762 summary.error_count.to_string(),
763 LabelStyle {
764 text: text_style.clone(),
765 highlight_text: None,
766 },
767 )
768 .aligned(),
769 )
770 .with_child(
771 Svg::new("icons/triangle_exclamation_12.svg")
772 .with_color(text_style.color)
773 .constrained()
774 .with_width(icon_width)
775 .aligned()
776 .contained()
777 .with_margin_left(summary_spacing)
778 .with_margin_right(icon_spacing),
779 )
780 .with_child(
781 Label::new(
782 summary.warning_count.to_string(),
783 LabelStyle {
784 text: text_style.clone(),
785 highlight_text: None,
786 },
787 )
788 .aligned(),
789 )
790 .into_any()
791 }
792}
793
794fn compare_diagnostics<L: language::ToOffset, R: language::ToOffset>(
795 lhs: &DiagnosticEntry<L>,
796 rhs: &DiagnosticEntry<R>,
797 snapshot: &language::BufferSnapshot,
798) -> Ordering {
799 lhs.range
800 .start
801 .to_offset(snapshot)
802 .cmp(&rhs.range.start.to_offset(snapshot))
803 .then_with(|| {
804 lhs.range
805 .end
806 .to_offset(snapshot)
807 .cmp(&rhs.range.end.to_offset(snapshot))
808 })
809 .then_with(|| lhs.diagnostic.message.cmp(&rhs.diagnostic.message))
810}
811
812#[cfg(test)]
813mod tests {
814 use super::*;
815 use editor::{
816 display_map::{BlockContext, TransformBlock},
817 DisplayPoint,
818 };
819 use gpui::{TestAppContext, WindowContext};
820 use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16, Unclipped};
821 use project::FakeFs;
822 use serde_json::json;
823 use settings::SettingsStore;
824 use unindent::Unindent as _;
825
826 #[gpui::test]
827 async fn test_diagnostics(cx: &mut TestAppContext) {
828 init_test(cx);
829
830 let fs = FakeFs::new(cx.background());
831 fs.insert_tree(
832 "/test",
833 json!({
834 "consts.rs": "
835 const a: i32 = 'a';
836 const b: i32 = c;
837 "
838 .unindent(),
839
840 "main.rs": "
841 fn main() {
842 let x = vec![];
843 let y = vec![];
844 a(x);
845 b(y);
846 // comment 1
847 // comment 2
848 c(y);
849 d(x);
850 }
851 "
852 .unindent(),
853 }),
854 )
855 .await;
856
857 let language_server_id = LanguageServerId(0);
858 let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
859 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), 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 = cx.add_view(window_id, |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_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
1253
1254 let view = cx.add_view(window_id, |cx| {
1255 ProjectDiagnosticsEditor::new(project.clone(), workspace.downgrade(), cx)
1256 });
1257
1258 // Two language servers start updating diagnostics
1259 project.update(cx, |project, cx| {
1260 project.disk_based_diagnostics_started(server_id_1, cx);
1261 project.disk_based_diagnostics_started(server_id_2, cx);
1262 project
1263 .update_diagnostic_entries(
1264 server_id_1,
1265 PathBuf::from("/test/main.js"),
1266 None,
1267 vec![DiagnosticEntry {
1268 range: Unclipped(PointUtf16::new(0, 0))..Unclipped(PointUtf16::new(0, 1)),
1269 diagnostic: Diagnostic {
1270 message: "error 1".to_string(),
1271 severity: DiagnosticSeverity::WARNING,
1272 is_primary: true,
1273 is_disk_based: true,
1274 group_id: 1,
1275 ..Default::default()
1276 },
1277 }],
1278 cx,
1279 )
1280 .unwrap();
1281 project
1282 .update_diagnostic_entries(
1283 server_id_2,
1284 PathBuf::from("/test/main.js"),
1285 None,
1286 vec![DiagnosticEntry {
1287 range: Unclipped(PointUtf16::new(1, 0))..Unclipped(PointUtf16::new(1, 1)),
1288 diagnostic: Diagnostic {
1289 message: "warning 1".to_string(),
1290 severity: DiagnosticSeverity::ERROR,
1291 is_primary: true,
1292 is_disk_based: true,
1293 group_id: 2,
1294 ..Default::default()
1295 },
1296 }],
1297 cx,
1298 )
1299 .unwrap();
1300 });
1301
1302 // The first language server finishes
1303 project.update(cx, |project, cx| {
1304 project.disk_based_diagnostics_finished(server_id_1, cx);
1305 });
1306
1307 // Only the first language server's diagnostics are shown.
1308 cx.foreground().run_until_parked();
1309 view.update(cx, |view, cx| {
1310 assert_eq!(
1311 editor_blocks(&view.editor, cx),
1312 [
1313 (0, "path header block".into()),
1314 (2, "diagnostic header".into()),
1315 ]
1316 );
1317 assert_eq!(
1318 view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1319 concat!(
1320 "\n", // filename
1321 "\n", // padding
1322 // diagnostic group 1
1323 "\n", // primary message
1324 "\n", // padding
1325 "a();\n", //
1326 "b();",
1327 )
1328 );
1329 });
1330
1331 // The second language server finishes
1332 project.update(cx, |project, cx| {
1333 project.disk_based_diagnostics_finished(server_id_2, cx);
1334 });
1335
1336 // Both language server's diagnostics are shown.
1337 cx.foreground().run_until_parked();
1338 view.update(cx, |view, cx| {
1339 assert_eq!(
1340 editor_blocks(&view.editor, cx),
1341 [
1342 (0, "path header block".into()),
1343 (2, "diagnostic header".into()),
1344 (6, "collapsed context".into()),
1345 (7, "diagnostic header".into()),
1346 ]
1347 );
1348 assert_eq!(
1349 view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1350 concat!(
1351 "\n", // filename
1352 "\n", // padding
1353 // diagnostic group 1
1354 "\n", // primary message
1355 "\n", // padding
1356 "a();\n", // location
1357 "b();\n", //
1358 "\n", // collapsed context
1359 // diagnostic group 2
1360 "\n", // primary message
1361 "\n", // padding
1362 "a();\n", // context
1363 "b();\n", //
1364 "c();", // context
1365 )
1366 );
1367 });
1368
1369 // Both language servers start updating diagnostics, and the first server finishes.
1370 project.update(cx, |project, cx| {
1371 project.disk_based_diagnostics_started(server_id_1, cx);
1372 project.disk_based_diagnostics_started(server_id_2, cx);
1373 project
1374 .update_diagnostic_entries(
1375 server_id_1,
1376 PathBuf::from("/test/main.js"),
1377 None,
1378 vec![DiagnosticEntry {
1379 range: Unclipped(PointUtf16::new(2, 0))..Unclipped(PointUtf16::new(2, 1)),
1380 diagnostic: Diagnostic {
1381 message: "warning 2".to_string(),
1382 severity: DiagnosticSeverity::WARNING,
1383 is_primary: true,
1384 is_disk_based: true,
1385 group_id: 1,
1386 ..Default::default()
1387 },
1388 }],
1389 cx,
1390 )
1391 .unwrap();
1392 project
1393 .update_diagnostic_entries(
1394 server_id_2,
1395 PathBuf::from("/test/main.rs"),
1396 None,
1397 vec![],
1398 cx,
1399 )
1400 .unwrap();
1401 project.disk_based_diagnostics_finished(server_id_1, cx);
1402 });
1403
1404 // Only the first language server's diagnostics are updated.
1405 cx.foreground().run_until_parked();
1406 view.update(cx, |view, cx| {
1407 assert_eq!(
1408 editor_blocks(&view.editor, cx),
1409 [
1410 (0, "path header block".into()),
1411 (2, "diagnostic header".into()),
1412 (7, "collapsed context".into()),
1413 (8, "diagnostic header".into()),
1414 ]
1415 );
1416 assert_eq!(
1417 view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1418 concat!(
1419 "\n", // filename
1420 "\n", // padding
1421 // diagnostic group 1
1422 "\n", // primary message
1423 "\n", // padding
1424 "a();\n", // location
1425 "b();\n", //
1426 "c();\n", // context
1427 "\n", // collapsed context
1428 // diagnostic group 2
1429 "\n", // primary message
1430 "\n", // padding
1431 "b();\n", // context
1432 "c();\n", //
1433 "d();", // context
1434 )
1435 );
1436 });
1437
1438 // The second language server finishes.
1439 project.update(cx, |project, cx| {
1440 project
1441 .update_diagnostic_entries(
1442 server_id_2,
1443 PathBuf::from("/test/main.js"),
1444 None,
1445 vec![DiagnosticEntry {
1446 range: Unclipped(PointUtf16::new(3, 0))..Unclipped(PointUtf16::new(3, 1)),
1447 diagnostic: Diagnostic {
1448 message: "warning 2".to_string(),
1449 severity: DiagnosticSeverity::WARNING,
1450 is_primary: true,
1451 is_disk_based: true,
1452 group_id: 1,
1453 ..Default::default()
1454 },
1455 }],
1456 cx,
1457 )
1458 .unwrap();
1459 project.disk_based_diagnostics_finished(server_id_2, cx);
1460 });
1461
1462 // Both language servers' diagnostics are updated.
1463 cx.foreground().run_until_parked();
1464 view.update(cx, |view, cx| {
1465 assert_eq!(
1466 editor_blocks(&view.editor, cx),
1467 [
1468 (0, "path header block".into()),
1469 (2, "diagnostic header".into()),
1470 (7, "collapsed context".into()),
1471 (8, "diagnostic header".into()),
1472 ]
1473 );
1474 assert_eq!(
1475 view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1476 concat!(
1477 "\n", // filename
1478 "\n", // padding
1479 // diagnostic group 1
1480 "\n", // primary message
1481 "\n", // padding
1482 "b();\n", // location
1483 "c();\n", //
1484 "d();\n", // context
1485 "\n", // collapsed context
1486 // diagnostic group 2
1487 "\n", // primary message
1488 "\n", // padding
1489 "c();\n", // context
1490 "d();\n", //
1491 "e();", // context
1492 )
1493 );
1494 });
1495 }
1496
1497 fn init_test(cx: &mut TestAppContext) {
1498 cx.update(|cx| {
1499 cx.set_global(Settings::test(cx));
1500 cx.set_global(SettingsStore::test(cx));
1501 language::init(cx);
1502 client::init_settings(cx);
1503 workspace::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}