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 = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
859 let workspace = window.root(cx);
860 let window_id = window.window_id();
861
862 // Create some diagnostics
863 project.update(cx, |project, cx| {
864 project
865 .update_diagnostic_entries(
866 language_server_id,
867 PathBuf::from("/test/main.rs"),
868 None,
869 vec![
870 DiagnosticEntry {
871 range: Unclipped(PointUtf16::new(1, 8))..Unclipped(PointUtf16::new(1, 9)),
872 diagnostic: Diagnostic {
873 message:
874 "move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait"
875 .to_string(),
876 severity: DiagnosticSeverity::INFORMATION,
877 is_primary: false,
878 is_disk_based: true,
879 group_id: 1,
880 ..Default::default()
881 },
882 },
883 DiagnosticEntry {
884 range: Unclipped(PointUtf16::new(2, 8))..Unclipped(PointUtf16::new(2, 9)),
885 diagnostic: Diagnostic {
886 message:
887 "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait"
888 .to_string(),
889 severity: DiagnosticSeverity::INFORMATION,
890 is_primary: false,
891 is_disk_based: true,
892 group_id: 0,
893 ..Default::default()
894 },
895 },
896 DiagnosticEntry {
897 range: Unclipped(PointUtf16::new(3, 6))..Unclipped(PointUtf16::new(3, 7)),
898 diagnostic: Diagnostic {
899 message: "value moved here".to_string(),
900 severity: DiagnosticSeverity::INFORMATION,
901 is_primary: false,
902 is_disk_based: true,
903 group_id: 1,
904 ..Default::default()
905 },
906 },
907 DiagnosticEntry {
908 range: Unclipped(PointUtf16::new(4, 6))..Unclipped(PointUtf16::new(4, 7)),
909 diagnostic: Diagnostic {
910 message: "value moved here".to_string(),
911 severity: DiagnosticSeverity::INFORMATION,
912 is_primary: false,
913 is_disk_based: true,
914 group_id: 0,
915 ..Default::default()
916 },
917 },
918 DiagnosticEntry {
919 range: Unclipped(PointUtf16::new(7, 6))..Unclipped(PointUtf16::new(7, 7)),
920 diagnostic: Diagnostic {
921 message: "use of moved value\nvalue used here after move".to_string(),
922 severity: DiagnosticSeverity::ERROR,
923 is_primary: true,
924 is_disk_based: true,
925 group_id: 0,
926 ..Default::default()
927 },
928 },
929 DiagnosticEntry {
930 range: Unclipped(PointUtf16::new(8, 6))..Unclipped(PointUtf16::new(8, 7)),
931 diagnostic: Diagnostic {
932 message: "use of moved value\nvalue used here after move".to_string(),
933 severity: DiagnosticSeverity::ERROR,
934 is_primary: true,
935 is_disk_based: true,
936 group_id: 1,
937 ..Default::default()
938 },
939 },
940 ],
941 cx,
942 )
943 .unwrap();
944 });
945
946 // Open the project diagnostics view while there are already diagnostics.
947 let view = cx.add_view(window_id, |cx| {
948 ProjectDiagnosticsEditor::new(project.clone(), workspace.downgrade(), cx)
949 });
950
951 view.next_notification(cx).await;
952 view.update(cx, |view, cx| {
953 assert_eq!(
954 editor_blocks(&view.editor, cx),
955 [
956 (0, "path header block".into()),
957 (2, "diagnostic header".into()),
958 (15, "collapsed context".into()),
959 (16, "diagnostic header".into()),
960 (25, "collapsed context".into()),
961 ]
962 );
963 assert_eq!(
964 view.editor.update(cx, |editor, cx| editor.display_text(cx)),
965 concat!(
966 //
967 // main.rs
968 //
969 "\n", // filename
970 "\n", // padding
971 // diagnostic group 1
972 "\n", // primary message
973 "\n", // padding
974 " let x = vec![];\n",
975 " let y = vec![];\n",
976 "\n", // supporting diagnostic
977 " a(x);\n",
978 " b(y);\n",
979 "\n", // supporting diagnostic
980 " // comment 1\n",
981 " // comment 2\n",
982 " c(y);\n",
983 "\n", // supporting diagnostic
984 " d(x);\n",
985 "\n", // context ellipsis
986 // diagnostic group 2
987 "\n", // primary message
988 "\n", // padding
989 "fn main() {\n",
990 " let x = vec![];\n",
991 "\n", // supporting diagnostic
992 " let y = vec![];\n",
993 " a(x);\n",
994 "\n", // supporting diagnostic
995 " b(y);\n",
996 "\n", // context ellipsis
997 " c(y);\n",
998 " d(x);\n",
999 "\n", // supporting diagnostic
1000 "}"
1001 )
1002 );
1003
1004 // Cursor is at the first diagnostic
1005 view.editor.update(cx, |editor, cx| {
1006 assert_eq!(
1007 editor.selections.display_ranges(cx),
1008 [DisplayPoint::new(12, 6)..DisplayPoint::new(12, 6)]
1009 );
1010 });
1011 });
1012
1013 // Diagnostics are added for another earlier path.
1014 project.update(cx, |project, cx| {
1015 project.disk_based_diagnostics_started(language_server_id, cx);
1016 project
1017 .update_diagnostic_entries(
1018 language_server_id,
1019 PathBuf::from("/test/consts.rs"),
1020 None,
1021 vec![DiagnosticEntry {
1022 range: Unclipped(PointUtf16::new(0, 15))..Unclipped(PointUtf16::new(0, 15)),
1023 diagnostic: Diagnostic {
1024 message: "mismatched types\nexpected `usize`, found `char`".to_string(),
1025 severity: DiagnosticSeverity::ERROR,
1026 is_primary: true,
1027 is_disk_based: true,
1028 group_id: 0,
1029 ..Default::default()
1030 },
1031 }],
1032 cx,
1033 )
1034 .unwrap();
1035 project.disk_based_diagnostics_finished(language_server_id, cx);
1036 });
1037
1038 view.next_notification(cx).await;
1039 view.update(cx, |view, cx| {
1040 assert_eq!(
1041 editor_blocks(&view.editor, cx),
1042 [
1043 (0, "path header block".into()),
1044 (2, "diagnostic header".into()),
1045 (7, "path header block".into()),
1046 (9, "diagnostic header".into()),
1047 (22, "collapsed context".into()),
1048 (23, "diagnostic header".into()),
1049 (32, "collapsed context".into()),
1050 ]
1051 );
1052 assert_eq!(
1053 view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1054 concat!(
1055 //
1056 // consts.rs
1057 //
1058 "\n", // filename
1059 "\n", // padding
1060 // diagnostic group 1
1061 "\n", // primary message
1062 "\n", // padding
1063 "const a: i32 = 'a';\n",
1064 "\n", // supporting diagnostic
1065 "const b: i32 = c;\n",
1066 //
1067 // main.rs
1068 //
1069 "\n", // filename
1070 "\n", // padding
1071 // diagnostic group 1
1072 "\n", // primary message
1073 "\n", // padding
1074 " let x = vec![];\n",
1075 " let y = vec![];\n",
1076 "\n", // supporting diagnostic
1077 " a(x);\n",
1078 " b(y);\n",
1079 "\n", // supporting diagnostic
1080 " // comment 1\n",
1081 " // comment 2\n",
1082 " c(y);\n",
1083 "\n", // supporting diagnostic
1084 " d(x);\n",
1085 "\n", // collapsed context
1086 // diagnostic group 2
1087 "\n", // primary message
1088 "\n", // filename
1089 "fn main() {\n",
1090 " let x = vec![];\n",
1091 "\n", // supporting diagnostic
1092 " let y = vec![];\n",
1093 " a(x);\n",
1094 "\n", // supporting diagnostic
1095 " b(y);\n",
1096 "\n", // context ellipsis
1097 " c(y);\n",
1098 " d(x);\n",
1099 "\n", // supporting diagnostic
1100 "}"
1101 )
1102 );
1103
1104 // Cursor keeps its position.
1105 view.editor.update(cx, |editor, cx| {
1106 assert_eq!(
1107 editor.selections.display_ranges(cx),
1108 [DisplayPoint::new(19, 6)..DisplayPoint::new(19, 6)]
1109 );
1110 });
1111 });
1112
1113 // Diagnostics are added to the first path
1114 project.update(cx, |project, cx| {
1115 project.disk_based_diagnostics_started(language_server_id, cx);
1116 project
1117 .update_diagnostic_entries(
1118 language_server_id,
1119 PathBuf::from("/test/consts.rs"),
1120 None,
1121 vec![
1122 DiagnosticEntry {
1123 range: Unclipped(PointUtf16::new(0, 15))
1124 ..Unclipped(PointUtf16::new(0, 15)),
1125 diagnostic: Diagnostic {
1126 message: "mismatched types\nexpected `usize`, found `char`"
1127 .to_string(),
1128 severity: DiagnosticSeverity::ERROR,
1129 is_primary: true,
1130 is_disk_based: true,
1131 group_id: 0,
1132 ..Default::default()
1133 },
1134 },
1135 DiagnosticEntry {
1136 range: Unclipped(PointUtf16::new(1, 15))
1137 ..Unclipped(PointUtf16::new(1, 15)),
1138 diagnostic: Diagnostic {
1139 message: "unresolved name `c`".to_string(),
1140 severity: DiagnosticSeverity::ERROR,
1141 is_primary: true,
1142 is_disk_based: true,
1143 group_id: 1,
1144 ..Default::default()
1145 },
1146 },
1147 ],
1148 cx,
1149 )
1150 .unwrap();
1151 project.disk_based_diagnostics_finished(language_server_id, cx);
1152 });
1153
1154 view.next_notification(cx).await;
1155 view.update(cx, |view, cx| {
1156 assert_eq!(
1157 editor_blocks(&view.editor, cx),
1158 [
1159 (0, "path header block".into()),
1160 (2, "diagnostic header".into()),
1161 (7, "collapsed context".into()),
1162 (8, "diagnostic header".into()),
1163 (13, "path header block".into()),
1164 (15, "diagnostic header".into()),
1165 (28, "collapsed context".into()),
1166 (29, "diagnostic header".into()),
1167 (38, "collapsed context".into()),
1168 ]
1169 );
1170 assert_eq!(
1171 view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1172 concat!(
1173 //
1174 // consts.rs
1175 //
1176 "\n", // filename
1177 "\n", // padding
1178 // diagnostic group 1
1179 "\n", // primary message
1180 "\n", // padding
1181 "const a: i32 = 'a';\n",
1182 "\n", // supporting diagnostic
1183 "const b: i32 = c;\n",
1184 "\n", // context ellipsis
1185 // diagnostic group 2
1186 "\n", // primary message
1187 "\n", // padding
1188 "const a: i32 = 'a';\n",
1189 "const b: i32 = c;\n",
1190 "\n", // supporting diagnostic
1191 //
1192 // main.rs
1193 //
1194 "\n", // filename
1195 "\n", // padding
1196 // diagnostic group 1
1197 "\n", // primary message
1198 "\n", // padding
1199 " let x = vec![];\n",
1200 " let y = vec![];\n",
1201 "\n", // supporting diagnostic
1202 " a(x);\n",
1203 " b(y);\n",
1204 "\n", // supporting diagnostic
1205 " // comment 1\n",
1206 " // comment 2\n",
1207 " c(y);\n",
1208 "\n", // supporting diagnostic
1209 " d(x);\n",
1210 "\n", // context ellipsis
1211 // diagnostic group 2
1212 "\n", // primary message
1213 "\n", // filename
1214 "fn main() {\n",
1215 " let x = vec![];\n",
1216 "\n", // supporting diagnostic
1217 " let y = vec![];\n",
1218 " a(x);\n",
1219 "\n", // supporting diagnostic
1220 " b(y);\n",
1221 "\n", // context ellipsis
1222 " c(y);\n",
1223 " d(x);\n",
1224 "\n", // supporting diagnostic
1225 "}"
1226 )
1227 );
1228 });
1229 }
1230
1231 #[gpui::test]
1232 async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
1233 init_test(cx);
1234
1235 let fs = FakeFs::new(cx.background());
1236 fs.insert_tree(
1237 "/test",
1238 json!({
1239 "main.js": "
1240 a();
1241 b();
1242 c();
1243 d();
1244 e();
1245 ".unindent()
1246 }),
1247 )
1248 .await;
1249
1250 let server_id_1 = LanguageServerId(100);
1251 let server_id_2 = LanguageServerId(101);
1252 let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
1253 let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
1254 let workspace = window.root(cx);
1255 let window_id = window.window_id();
1256
1257 let view = cx.add_view(window_id, |cx| {
1258 ProjectDiagnosticsEditor::new(project.clone(), workspace.downgrade(), cx)
1259 });
1260
1261 // Two language servers start updating diagnostics
1262 project.update(cx, |project, cx| {
1263 project.disk_based_diagnostics_started(server_id_1, cx);
1264 project.disk_based_diagnostics_started(server_id_2, cx);
1265 project
1266 .update_diagnostic_entries(
1267 server_id_1,
1268 PathBuf::from("/test/main.js"),
1269 None,
1270 vec![DiagnosticEntry {
1271 range: Unclipped(PointUtf16::new(0, 0))..Unclipped(PointUtf16::new(0, 1)),
1272 diagnostic: Diagnostic {
1273 message: "error 1".to_string(),
1274 severity: DiagnosticSeverity::WARNING,
1275 is_primary: true,
1276 is_disk_based: true,
1277 group_id: 1,
1278 ..Default::default()
1279 },
1280 }],
1281 cx,
1282 )
1283 .unwrap();
1284 project
1285 .update_diagnostic_entries(
1286 server_id_2,
1287 PathBuf::from("/test/main.js"),
1288 None,
1289 vec![DiagnosticEntry {
1290 range: Unclipped(PointUtf16::new(1, 0))..Unclipped(PointUtf16::new(1, 1)),
1291 diagnostic: Diagnostic {
1292 message: "warning 1".to_string(),
1293 severity: DiagnosticSeverity::ERROR,
1294 is_primary: true,
1295 is_disk_based: true,
1296 group_id: 2,
1297 ..Default::default()
1298 },
1299 }],
1300 cx,
1301 )
1302 .unwrap();
1303 });
1304
1305 // The first language server finishes
1306 project.update(cx, |project, cx| {
1307 project.disk_based_diagnostics_finished(server_id_1, cx);
1308 });
1309
1310 // Only the first language server's diagnostics are shown.
1311 cx.foreground().run_until_parked();
1312 view.update(cx, |view, cx| {
1313 assert_eq!(
1314 editor_blocks(&view.editor, cx),
1315 [
1316 (0, "path header block".into()),
1317 (2, "diagnostic header".into()),
1318 ]
1319 );
1320 assert_eq!(
1321 view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1322 concat!(
1323 "\n", // filename
1324 "\n", // padding
1325 // diagnostic group 1
1326 "\n", // primary message
1327 "\n", // padding
1328 "a();\n", //
1329 "b();",
1330 )
1331 );
1332 });
1333
1334 // The second language server finishes
1335 project.update(cx, |project, cx| {
1336 project.disk_based_diagnostics_finished(server_id_2, cx);
1337 });
1338
1339 // Both language server's diagnostics are shown.
1340 cx.foreground().run_until_parked();
1341 view.update(cx, |view, cx| {
1342 assert_eq!(
1343 editor_blocks(&view.editor, cx),
1344 [
1345 (0, "path header block".into()),
1346 (2, "diagnostic header".into()),
1347 (6, "collapsed context".into()),
1348 (7, "diagnostic header".into()),
1349 ]
1350 );
1351 assert_eq!(
1352 view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1353 concat!(
1354 "\n", // filename
1355 "\n", // padding
1356 // diagnostic group 1
1357 "\n", // primary message
1358 "\n", // padding
1359 "a();\n", // location
1360 "b();\n", //
1361 "\n", // collapsed context
1362 // diagnostic group 2
1363 "\n", // primary message
1364 "\n", // padding
1365 "a();\n", // context
1366 "b();\n", //
1367 "c();", // context
1368 )
1369 );
1370 });
1371
1372 // Both language servers start updating diagnostics, and the first server finishes.
1373 project.update(cx, |project, cx| {
1374 project.disk_based_diagnostics_started(server_id_1, cx);
1375 project.disk_based_diagnostics_started(server_id_2, cx);
1376 project
1377 .update_diagnostic_entries(
1378 server_id_1,
1379 PathBuf::from("/test/main.js"),
1380 None,
1381 vec![DiagnosticEntry {
1382 range: Unclipped(PointUtf16::new(2, 0))..Unclipped(PointUtf16::new(2, 1)),
1383 diagnostic: Diagnostic {
1384 message: "warning 2".to_string(),
1385 severity: DiagnosticSeverity::WARNING,
1386 is_primary: true,
1387 is_disk_based: true,
1388 group_id: 1,
1389 ..Default::default()
1390 },
1391 }],
1392 cx,
1393 )
1394 .unwrap();
1395 project
1396 .update_diagnostic_entries(
1397 server_id_2,
1398 PathBuf::from("/test/main.rs"),
1399 None,
1400 vec![],
1401 cx,
1402 )
1403 .unwrap();
1404 project.disk_based_diagnostics_finished(server_id_1, cx);
1405 });
1406
1407 // Only the first language server's diagnostics are updated.
1408 cx.foreground().run_until_parked();
1409 view.update(cx, |view, cx| {
1410 assert_eq!(
1411 editor_blocks(&view.editor, cx),
1412 [
1413 (0, "path header block".into()),
1414 (2, "diagnostic header".into()),
1415 (7, "collapsed context".into()),
1416 (8, "diagnostic header".into()),
1417 ]
1418 );
1419 assert_eq!(
1420 view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1421 concat!(
1422 "\n", // filename
1423 "\n", // padding
1424 // diagnostic group 1
1425 "\n", // primary message
1426 "\n", // padding
1427 "a();\n", // location
1428 "b();\n", //
1429 "c();\n", // context
1430 "\n", // collapsed context
1431 // diagnostic group 2
1432 "\n", // primary message
1433 "\n", // padding
1434 "b();\n", // context
1435 "c();\n", //
1436 "d();", // context
1437 )
1438 );
1439 });
1440
1441 // The second language server finishes.
1442 project.update(cx, |project, cx| {
1443 project
1444 .update_diagnostic_entries(
1445 server_id_2,
1446 PathBuf::from("/test/main.js"),
1447 None,
1448 vec![DiagnosticEntry {
1449 range: Unclipped(PointUtf16::new(3, 0))..Unclipped(PointUtf16::new(3, 1)),
1450 diagnostic: Diagnostic {
1451 message: "warning 2".to_string(),
1452 severity: DiagnosticSeverity::WARNING,
1453 is_primary: true,
1454 is_disk_based: true,
1455 group_id: 1,
1456 ..Default::default()
1457 },
1458 }],
1459 cx,
1460 )
1461 .unwrap();
1462 project.disk_based_diagnostics_finished(server_id_2, cx);
1463 });
1464
1465 // Both language servers' diagnostics are updated.
1466 cx.foreground().run_until_parked();
1467 view.update(cx, |view, cx| {
1468 assert_eq!(
1469 editor_blocks(&view.editor, cx),
1470 [
1471 (0, "path header block".into()),
1472 (2, "diagnostic header".into()),
1473 (7, "collapsed context".into()),
1474 (8, "diagnostic header".into()),
1475 ]
1476 );
1477 assert_eq!(
1478 view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1479 concat!(
1480 "\n", // filename
1481 "\n", // padding
1482 // diagnostic group 1
1483 "\n", // primary message
1484 "\n", // padding
1485 "b();\n", // location
1486 "c();\n", //
1487 "d();\n", // context
1488 "\n", // collapsed context
1489 // diagnostic group 2
1490 "\n", // primary message
1491 "\n", // padding
1492 "c();\n", // context
1493 "d();\n", //
1494 "e();", // context
1495 )
1496 );
1497 });
1498 }
1499
1500 fn init_test(cx: &mut TestAppContext) {
1501 cx.update(|cx| {
1502 cx.set_global(SettingsStore::test(cx));
1503 theme::init((), cx);
1504 language::init(cx);
1505 client::init_settings(cx);
1506 workspace::init_settings(cx);
1507 Project::init_settings(cx);
1508 });
1509 }
1510
1511 fn editor_blocks(editor: &ViewHandle<Editor>, cx: &mut WindowContext) -> Vec<(u32, String)> {
1512 editor.update(cx, |editor, cx| {
1513 let snapshot = editor.snapshot(cx);
1514 snapshot
1515 .blocks_in_range(0..snapshot.max_point().row())
1516 .enumerate()
1517 .filter_map(|(ix, (row, block))| {
1518 let name = match block {
1519 TransformBlock::Custom(block) => block
1520 .render(&mut BlockContext {
1521 view_context: cx,
1522 anchor_x: 0.,
1523 scroll_x: 0.,
1524 gutter_padding: 0.,
1525 gutter_width: 0.,
1526 line_height: 0.,
1527 em_width: 0.,
1528 block_id: ix,
1529 })
1530 .name()?
1531 .to_string(),
1532 TransformBlock::ExcerptHeader {
1533 starts_new_buffer, ..
1534 } => {
1535 if *starts_new_buffer {
1536 "path header block".to_string()
1537 } else {
1538 "collapsed context".to_string()
1539 }
1540 }
1541 };
1542
1543 Some((row, name))
1544 })
1545 .collect()
1546 })
1547 }
1548}